@@ -23,11 +23,10 @@ public import SwiftSyntaxMacros
23
23
/// - traitExprs: An array of trait expressions to examine.
24
24
/// - attribute: The `@Test` or `@Suite` attribute.
25
25
/// - context: The macro context in which the expression is being parsed.
26
- func diagnoseIssuesWithTags( in traitExprs: [ ExprSyntax ] , addedTo attribute: AttributeSyntax , in context: some MacroExpansionContext ) {
27
- // Find tags that are in an unsupported format (only .member and "literal"
28
- // are allowed.)
26
+ func diagnoseIssuesWithTraits( in traitExprs: [ ExprSyntax ] , addedTo attribute: AttributeSyntax , in context: some MacroExpansionContext ) {
29
27
for traitExpr in traitExprs {
30
- // At this time, we are only looking for .tags() traits in this function.
28
+ // At this time, we are only looking for .tags() and .bug() traits in this
29
+ // function.
31
30
guard let functionCallExpr = traitExpr. as ( FunctionCallExprSyntax . self) ,
32
31
let calledExpr = functionCallExpr. calledExpression. as ( MemberAccessExprSyntax . self) else {
33
32
continue
@@ -36,58 +35,122 @@ func diagnoseIssuesWithTags(in traitExprs: [ExprSyntax], addedTo attribute: Attr
36
35
// Check for .tags() traits.
37
36
switch calledExpr. tokens ( viewMode: . fixedUp) . map ( \. textWithoutBackticks) . joined ( ) {
38
37
case " .tags " , " Tag.List.tags " , " Testing.Tag.List.tags " :
39
- for tagExpr in functionCallExpr. arguments. lazy. map ( \. expression) {
40
- if tagExpr. is ( StringLiteralExprSyntax . self) {
41
- // String literals are supported tags.
42
- } else if let tagExpr = tagExpr. as ( MemberAccessExprSyntax . self) {
43
- let joinedTokens = tagExpr. tokens ( viewMode: . fixedUp) . map ( \. textWithoutBackticks) . joined ( )
44
- if joinedTokens. hasPrefix ( " . " ) || joinedTokens. hasPrefix ( " Tag. " ) || joinedTokens. hasPrefix ( " Testing.Tag. " ) {
45
- // These prefixes are all allowed as they specify a member access
46
- // into the Tag type.
47
- } else {
48
- context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
49
- continue
50
- }
38
+ _diagnoseIssuesWithTagsTrait ( functionCallExpr, addedTo: attribute, in: context)
39
+ case " .bug " , " Bug.bug " , " Testing.Bug.bug " :
40
+ _diagnoseIssuesWithBugTrait ( functionCallExpr, addedTo: attribute, in: context)
41
+ default :
42
+ // This is not a trait we can parse.
43
+ break
44
+ }
45
+ }
46
+ }
51
47
52
- // Walk all base expressions and make sure they are exclusively member
53
- // access expressions.
54
- func checkForValidDeclReferenceExpr( _ declReferenceExpr: DeclReferenceExprSyntax ) {
55
- // This is the name of a type or symbol. If there are argument names
56
- // (unexpected in this context), it's a function reference and is
57
- // unsupported.
58
- if declReferenceExpr. argumentNames != nil {
59
- context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
60
- }
61
- }
62
- func checkForValidBaseExpr( _ baseExpr: ExprSyntax ) {
63
- if let baseExpr = baseExpr. as ( MemberAccessExprSyntax . self) {
64
- checkForValidDeclReferenceExpr ( baseExpr. declName)
65
- if let baseBaseExpr = baseExpr. base {
66
- checkForValidBaseExpr ( baseBaseExpr)
67
- }
68
- } else if let baseExpr = baseExpr. as ( DeclReferenceExprSyntax . self) {
69
- checkForValidDeclReferenceExpr ( baseExpr)
70
- } else {
71
- // The base expression was some other kind of expression and is
72
- // not supported.
73
- context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
74
- }
75
- }
76
- if let baseExpr = tagExpr. base {
77
- checkForValidBaseExpr ( baseExpr)
48
+ /// Diagnose issues with a `.tags()` trait in a parsed attribute.
49
+ ///
50
+ /// - Parameters:
51
+ /// - traitExpr: The `.tags()` expression.
52
+ /// - attribute: The `@Test` or `@Suite` attribute.
53
+ /// - context: The macro context in which the expression is being parsed.
54
+ private func _diagnoseIssuesWithTagsTrait( _ traitExpr: FunctionCallExprSyntax , addedTo attribute: AttributeSyntax , in context: some MacroExpansionContext ) {
55
+ // Find tags that are in an unsupported format (only .member and "literal"
56
+ // are allowed.)
57
+ for tagExpr in traitExpr. arguments. lazy. map ( \. expression) {
58
+ if tagExpr. is ( StringLiteralExprSyntax . self) {
59
+ // String literals are supported tags.
60
+ } else if let tagExpr = tagExpr. as ( MemberAccessExprSyntax . self) {
61
+ let joinedTokens = tagExpr. tokens ( viewMode: . fixedUp) . map ( \. textWithoutBackticks) . joined ( )
62
+ if joinedTokens. hasPrefix ( " . " ) || joinedTokens. hasPrefix ( " Tag. " ) || joinedTokens. hasPrefix ( " Testing.Tag. " ) {
63
+ // These prefixes are all allowed as they specify a member access
64
+ // into the Tag type.
65
+ } else {
66
+ context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
67
+ continue
68
+ }
69
+
70
+ // Walk all base expressions and make sure they are exclusively member
71
+ // access expressions.
72
+ func checkForValidDeclReferenceExpr( _ declReferenceExpr: DeclReferenceExprSyntax ) {
73
+ // This is the name of a type or symbol. If there are argument names
74
+ // (unexpected in this context), it's a function reference and is
75
+ // unsupported.
76
+ if declReferenceExpr. argumentNames != nil {
77
+ context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
78
+ }
79
+ }
80
+ func checkForValidBaseExpr( _ baseExpr: ExprSyntax ) {
81
+ if let baseExpr = baseExpr. as ( MemberAccessExprSyntax . self) {
82
+ checkForValidDeclReferenceExpr ( baseExpr. declName)
83
+ if let baseBaseExpr = baseExpr. base {
84
+ checkForValidBaseExpr ( baseBaseExpr)
78
85
}
86
+ } else if let baseExpr = baseExpr. as ( DeclReferenceExprSyntax . self) {
87
+ checkForValidDeclReferenceExpr ( baseExpr)
79
88
} else {
80
- // This tag is not of a supported expression type.
89
+ // The base expression was some other kind of expression and is
90
+ // not supported.
81
91
context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
82
92
}
83
93
}
84
- default :
85
- // This is not a tag list (as far as we know.)
86
- break
94
+ if let baseExpr = tagExpr. base {
95
+ checkForValidBaseExpr ( baseExpr)
96
+ }
97
+ } else {
98
+ // This tag is not of a supported expression type.
99
+ context. diagnose ( . tagExprNotSupported( tagExpr, in: attribute) )
87
100
}
88
101
}
89
102
}
90
103
104
+ /// Diagnose issues with a `.bug()` trait in a parsed attribute.
105
+ ///
106
+ /// - Parameters:
107
+ /// - traitExpr: The `.bug()` expression.
108
+ /// - attribute: The `@Test` or `@Suite` attribute.
109
+ /// - context: The macro context in which the expression is being parsed.
110
+ private func _diagnoseIssuesWithBugTrait( _ traitExpr: FunctionCallExprSyntax , addedTo attribute: AttributeSyntax , in context: some MacroExpansionContext ) {
111
+ // If the first argument to the .bug() trait is unlabelled and a string
112
+ // literal, check that it can be parsed as a URL or at least as an integer.
113
+ guard let arg = traitExpr. arguments. first. map ( Argument . init) ,
114
+ arg. label == nil ,
115
+ let stringLiteralExpr = arg. expression. as ( StringLiteralExprSyntax . self) ,
116
+ let urlString = stringLiteralExpr. representedLiteralValue else {
117
+ return
118
+ }
119
+
120
+ if UInt64 ( urlString) != nil {
121
+ // The entire URL string can be parsed as an integer, so allow it. Although
122
+ // the testing library prefers valid URLs here, some bug-tracking systems
123
+ // might not provide URLs, or might provide excessively long URLs, so we
124
+ // allow numeric identifiers as a fallback.
125
+ return
126
+ }
127
+
128
+ if urlString. count > 3 && urlString. starts ( with: " FB " ) && UInt64 ( urlString. dropFirst ( 2 ) ) != nil {
129
+ // The string appears to be of the form "FBnnn...". Such strings are used by
130
+ // Apple to indicate issues filed using Feedback Assistant. Although we
131
+ // don't want to special-case every possible bug-tracking system out there,
132
+ // Feedback Assistant is very important to Apple so we're making an
133
+ // exception for it.
134
+ return
135
+ }
136
+
137
+ // We could use libcurl, libxml, or Windows' InternetCrackUrlW() to actually
138
+ // parse the string and ensure it is a valid URL, however we could get
139
+ // different results on different platforms. See the branch
140
+ // jgrynspan/type-check-bug-identifiers-with-libcurl for an implementation.
141
+ // Instead, we apply a very basic sniff test above. We intentionally don't
142
+ // use a regular expression here.
143
+
144
+ let isURLStringValid = urlString. allSatisfy ( \. isASCII)
145
+ && !urlString. contains ( where: \. isWhitespace)
146
+ && urlString. contains ( " : " )
147
+ if !isURLStringValid {
148
+ context. diagnose ( . urlExprNotValid( stringLiteralExpr, in: traitExpr, in: attribute) )
149
+ }
150
+ }
151
+
152
+ // MARK: -
153
+
91
154
/// Diagnose issues with a synthesized suite (one without an `@Suite` attribute)
92
155
/// containing a declaration.
93
156
///
0 commit comments