Skip to content

Commit 056b7fd

Browse files
authored
Diagnose the use of a generic clause on @Test, @Suite, and @Tag. (#1650)
For example: ```swift @test<[Int]>(arguments: [1, 2, 3]) func f(i: Int) { ... } ``` We never intended for this to be valid syntax. It appears some folks _are_ using it, so deprecate for now and make it an error in the next language mode. The diagnostic looks like this in the Swift 6 language mode: > ⚠️ Generic argument clause of attribute 'Test' is unsupported; this is an error in the Swift 7 language mode And in the Swift 7 language mode, it's an error. (In the future, if we find that this syntax is useful and want to build out some sort of support for it, we can of course remove the diagnostic.) ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 862937a commit 056b7fd

7 files changed

Lines changed: 99 additions & 9 deletions

File tree

Sources/TestingMacros/SuiteDeclarationMacro.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//
1010

1111
import SwiftDiagnostics
12+
import SwiftIfConfig
1213
public import SwiftSyntax
1314
import SwiftSyntaxBuilder
1415
public import SwiftSyntaxMacros
@@ -83,6 +84,11 @@ public struct SuiteDeclarationMacro: PeerMacro, Sendable {
8384
}
8485
}
8586

87+
// @Suite should not use a generic argument clause.
88+
if let genericArgumentClause = suiteAttribute.genericArgumentClause {
89+
diagnostics.append(.genericAttributeNotSupported(suiteAttribute, on: declaration, becauseOf: genericArgumentClause, languageMode: context.buildConfiguration?.languageVersion))
90+
}
91+
8692
return !diagnostics.lazy.map(\.severity).contains(.error)
8793
}
8894

Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,14 @@ extension WithAttributesSyntax {
137137
}
138138

139139
extension AttributeSyntax {
140-
/// The text of this attribute's name.
141-
var attributeNameText: String {
142-
attributeName
143-
.tokens(viewMode: .fixedUp)
144-
.map(\.textWithoutBackticks)
145-
.joined()
140+
/// The generic argument clause of this attribute's name, if any.
141+
var genericArgumentClause: GenericArgumentClauseSyntax? {
142+
if let type = attributeName.as(IdentifierTypeSyntax.self) {
143+
return type.genericArgumentClause
144+
} else if let type = attributeName.as(MemberTypeSyntax.self) {
145+
return type.genericArgumentClause
146+
}
147+
return nil
146148
}
147149
}
148150

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//
1010

1111
import SwiftDiagnostics
12+
import SwiftIfConfig
1213
import SwiftParser
1314
import SwiftSyntax
1415
import SwiftSyntaxBuilder
@@ -94,8 +95,19 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
9495
/// - Returns: The name of the macro as understood by a developer, such as
9596
/// `"'@Test'"`. Include single quotes.
9697
private static func _macroName(_ attribute: AttributeSyntax) -> String {
98+
var attributeName = attribute.attributeName
99+
if let type = attributeName.as(IdentifierTypeSyntax.self) {
100+
attributeName = TypeSyntax(type.with(\.genericArgumentClause, nil))
101+
} else if let type = attributeName.as(MemberTypeSyntax.self) {
102+
attributeName = TypeSyntax(type.with(\.genericArgumentClause, nil))
103+
}
104+
let attributeNameText = attributeName
105+
.tokens(viewMode: .fixedUp)
106+
.map(\.textWithoutBackticks)
107+
.joined()
108+
97109
// SEE: https://github.com/swiftlang/swift/blob/main/docs/Diagnostics.md?plain=1#L44
98-
"'\(attribute.attributeNameText)'"
110+
return "'\(attributeNameText)'"
99111
}
100112

101113
/// Get a string corresponding to the specified syntax node (for instance,
@@ -197,6 +209,39 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
197209
}
198210
}
199211

212+
/// Create a diagnostic message stating that the given attribute has an unused
213+
/// generic argument clause (e.g. `@Test<T>`).
214+
///
215+
/// - Parameters:
216+
/// - attribute: The `@Test` or `@Suite` attribute.
217+
/// - decl: The generic declaration in question.
218+
/// - genericClause: The child node on `attribute` that makes it generic.
219+
///
220+
/// - Returns: A diagnostic message.
221+
static func genericAttributeNotSupported(_ attribute: AttributeSyntax, on decl: some SyntaxProtocol, becauseOf genericClause: some SyntaxProtocol, languageMode: VersionTuple?) -> Self {
222+
let fixIts: [FixIt] = [
223+
FixIt(
224+
message: MacroExpansionFixItMessage("Remove generic attribute clause from \(_macroName(attribute))"),
225+
changes: [.replace(oldNode: Syntax(genericClause), newNode: Syntax("" as ExprSyntax))]
226+
),
227+
]
228+
if let languageMode, languageMode >= .init(7, 0) {
229+
return Self(
230+
syntax: Syntax(genericClause),
231+
message: "Generic argument clause of attribute \(_macroName(attribute)) is unsupported",
232+
severity: .error,
233+
fixIts: fixIts
234+
)
235+
} else {
236+
return Self(
237+
syntax: Syntax(genericClause),
238+
message: "Generic argument clause of attribute \(_macroName(attribute)) is unsupported; this is an error in the Swift 7 language mode",
239+
severity: .warning,
240+
fixIts: fixIts
241+
)
242+
}
243+
}
244+
200245
/// Create a diagnostic message stating that the `@Test` or `@Suite` attribute
201246
/// cannot be applied to a type that also has an availability attribute.
202247
///

Sources/TestingMacros/TagMacro.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11+
import SwiftIfConfig
1112
public import SwiftSyntax
1213
import SwiftSyntaxBuilder
1314
public import SwiftSyntaxMacros
@@ -46,6 +47,10 @@ public struct TagMacro: PeerMacro, AccessorMacro, Sendable {
4647
return _fallbackAccessorDecls
4748
}
4849

50+
if let genericArgumentClause = node.genericArgumentClause {
51+
context.diagnose(.genericAttributeNotSupported(node, on: declaration, becauseOf: genericArgumentClause, languageMode: context.buildConfiguration?.languageVersion))
52+
}
53+
4954
// Check that the tag is declared within Tag's namespace.
5055
let typeNameTokens: [String] = type.tokens(viewMode: .fixedUp).lazy
5156
.filter { $0.tokenKind != .period }

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//
1010

1111
import SwiftDiagnostics
12+
import SwiftIfConfig
1213
public import SwiftSyntax
1314
import SwiftSyntaxBuilder
1415
public import SwiftSyntaxMacros
@@ -130,6 +131,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
130131
}
131132
}
132133

134+
// @Test should not use a generic argument clause.
135+
if let genericArgumentClause = testAttribute.genericArgumentClause {
136+
diagnostics.append(.genericAttributeNotSupported(testAttribute, on: function, becauseOf: genericArgumentClause, languageMode: context.buildConfiguration?.languageVersion))
137+
}
138+
133139
return !diagnostics.lazy.map(\.severity).contains(.error)
134140
}
135141

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Testing
1313

1414
import SwiftBasicFormat
1515
import SwiftDiagnostics
16+
import SwiftIfConfig
1617
import SwiftParser
1718
import SwiftSyntax
1819
import SwiftSyntaxBuilder
@@ -351,6 +352,26 @@ struct TestDeclarationMacroTests {
351352
}
352353
}
353354

355+
@Test("Error diagnostics emitted dependent on language mode",
356+
arguments: [
357+
("@Suite<T> struct S {}", "Generic argument clause of attribute 'Suite' is unsupported; this is an error in the Swift 7 language mode", 6, DiagnosticSeverity.warning),
358+
("@Suite<T> struct S {}", "Generic argument clause of attribute 'Suite' is unsupported", 7, DiagnosticSeverity.error),
359+
("@Test<T> func f() {}", "Generic argument clause of attribute 'Test' is unsupported; this is an error in the Swift 7 language mode", 6, DiagnosticSeverity.warning),
360+
("@Test<T> func f() {}", "Generic argument clause of attribute 'Test' is unsupported", 7, DiagnosticSeverity.error),
361+
("extension Tag { @Tag<T> static var f: Self }", "Generic argument clause of attribute 'Tag' is unsupported; this is an error in the Swift 7 language mode", 6, DiagnosticSeverity.warning),
362+
("extension Tag { @Tag<T> static var f: Self }", "Generic argument clause of attribute 'Tag' is unsupported", 7, DiagnosticSeverity.error),
363+
]
364+
)
365+
func languageModeDependentDiagnostics(input: String, expectedMessage: String, languageMode: Int, severity: DiagnosticSeverity) throws {
366+
let (_, diagnostics) = try parse(input, languageMode: VersionTuple(languageMode))
367+
368+
#expect(diagnostics.count > 0)
369+
for diagnostic in diagnostics {
370+
#expect(diagnostic.diagMessage.severity == severity)
371+
#expect(diagnostic.message == expectedMessage)
372+
}
373+
}
374+
354375
@Test("Raw identifier is detected")
355376
func rawIdentifier() {
356377
#expect(TokenSyntax.identifier("`hello`").rawIdentifier == nil)

Tests/TestingMacrosTests/TestSupport/Parse.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import SwiftBasicFormat
1414
import SwiftDiagnostics
15+
import SwiftIfConfig
1516
import SwiftOperators
1617
import SwiftParser
1718
import SwiftSyntax
@@ -34,16 +35,20 @@ fileprivate let allMacros: [String: any (Macro & Sendable).Type] = [
3435
"__testing": PragmaMacro.self,
3536
]
3637

37-
func parse(_ sourceCode: String, activeMacros activeMacroNames: [String] = [], removeWhitespace: Bool = false) throws -> (sourceCode: String, diagnostics: [Diagnostic]) {
38+
func parse(_ sourceCode: String, activeMacros activeMacroNames: [String] = [], removeWhitespace: Bool = false, languageMode: VersionTuple? = nil) throws -> (sourceCode: String, diagnostics: [Diagnostic]) {
3839
let activeMacros: [String: any Macro.Type]
3940
if activeMacroNames.isEmpty {
4041
activeMacros = allMacros
4142
} else {
4243
activeMacros = allMacros.filter { activeMacroNames.contains($0.key) }
4344
}
45+
var buildConfiguration: StaticBuildConfiguration?
46+
if let languageMode {
47+
buildConfiguration = StaticBuildConfiguration(languageVersion: languageMode, compilerVersion: VersionTuple(99, 0))
48+
}
4449
let operatorTable = OperatorTable.standardOperators
4550
let originalSyntax = try operatorTable.foldAll(Parser.parse(source: sourceCode))
46-
let context = BasicMacroExpansionContext(lexicalContext: [], expansionDiscriminator: "", sourceFiles: [:])
51+
let context = BasicMacroExpansionContext(lexicalContext: [], expansionDiscriminator: "", sourceFiles: [:], buildConfiguration: buildConfiguration)
4752
let syntax = try operatorTable.foldAll(
4853
originalSyntax.expand(macros: activeMacros) { syntax in
4954
BasicMacroExpansionContext(sharingWith: context, lexicalContext: syntax.allMacroLexicalContexts())

0 commit comments

Comments
 (0)