Skip to content

Commit e92bee7

Browse files
authored
Block tests/suites in extensions to syntactic sugar types ([T], [T:U], T?) (#361)
This PR blocks tests and suites in extensions to types declared using syntactic sugar. For example, the following is invalid: ```swift extension [Int] { @test func f() {} } ``` I doubt this is going to be a common pattern, but it's just as invalid as saying `extension Array<Int>` for the same reasons: if it successfully compiles, it produces metadata for a test that we cannot instantiate at runtime (because the type we can see in the metadata is still generic, not specialized.) There is a guard symbol emitted by our macro already that triggers a diagnostic if a test or suite is in a generic type, but `extension [T]` is fully specialized and does not trigger the diagnostic, so it's important that we catch this issue and make sure some diagnostic is emitted. With this change, when using swift-syntax-600, you should now see: ```swift extension [Int] { @test func f() {} // 🛑 Attribute 'Test' cannot be applied to a function // within a generic extension to type '[Int]' } ``` This PR also introduces an environment variable you can set when running `swift test` to opt into a different swift-syntax version, which makes testing with swift-syntax-600 easier. This diagnostic will be removed as redundant if/when #338 is merged. Finally, this PR fixes some unrelated unit tests that fail when using swift-syntax-600. ### 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 6814f43 commit e92bee7

File tree

7 files changed

+94
-32
lines changed

7 files changed

+94
-32
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ let package = Package(
3333
],
3434

3535
dependencies: [
36-
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"),
36+
.package(url: "https://github.com/apple/swift-syntax.git", from: Version(stringLiteral: Context.environment["SWT_SWIFT_SYNTAX_VERSION"] ?? "510.0.1")),
3737
],
3838

3939
targets: [

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ let package = Package(
3333
],
3434

3535
dependencies: [
36-
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"),
36+
.package(url: "https://github.com/apple/swift-syntax.git", from: Version(stringLiteral: Context.environment["SWT_SWIFT_SYNTAX_VERSION"] ?? "510.0.1")),
3737
],
3838

3939
targets: [

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,14 @@ func diagnoseIssuesWithLexicalContext(
122122

123123
// Generic suites are not supported.
124124
if let genericClause = lexicalContext.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause {
125-
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: genericClause))
125+
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: genericClause, on: lexicalContext))
126126
} else if let whereClause = lexicalContext.genericWhereClause {
127-
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: whereClause))
127+
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: whereClause, on: lexicalContext))
128+
} else if [.arrayType, .dictionaryType, .optionalType, .implicitlyUnwrappedOptionalType].contains(lexicalContext.type.kind) {
129+
// These types are all syntactic sugar over generic types (Array<T>,
130+
// Dictionary<T>, and Optional<T>) and are just as unsupported. T! is
131+
// unsupported in this position, but it's still forbidden so don't even try!
132+
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: lexicalContext.type, on: lexicalContext))
128133
}
129134

130135
// Suites that are classes must be final.

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
146146
///
147147
/// - Parameters:
148148
/// - attributes: The conflicting attributes. This array must not be empty.
149-
/// - decl: The generic declaration in question.
149+
/// - decl: The declaration in question.
150150
///
151151
/// - Returns: A diagnostic message.
152152
static func multipleAttributesNotSupported(_ attributes: [AttributeSyntax], on decl: some SyntaxProtocol) -> Self {
@@ -164,18 +164,24 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
164164
/// - Parameters:
165165
/// - decl: The generic declaration in question.
166166
/// - attribute: The `@Test` or `@Suite` attribute.
167-
/// - genericClause: The child node on `decl` that makes it generic.
167+
/// - genericClause: The child node on `genericDecl` that makes it generic.
168+
/// - genericDecl: The generic declaration to which `genericClause` is
169+
/// attached, possibly equal to `decl`.
168170
///
169171
/// - Returns: A diagnostic message.
170-
static func genericDeclarationNotSupported(_ decl: some SyntaxProtocol, whenUsing attribute: AttributeSyntax, becauseOf genericClause: some SyntaxProtocol) -> Self {
171-
// Avoid using a syntax node from a lexical context (it won't have source
172-
// location information.)
173-
let syntax = (genericClause.root != decl.root) ? Syntax(decl) : Syntax(genericClause)
174-
return Self(
175-
syntax: syntax,
176-
message: "Attribute \(_macroName(attribute)) cannot be applied to a generic \(_kindString(for: decl))",
177-
severity: .error
178-
)
172+
static func genericDeclarationNotSupported(_ decl: some SyntaxProtocol, whenUsing attribute: AttributeSyntax, becauseOf genericClause: some SyntaxProtocol, on genericDecl: some SyntaxProtocol) -> Self {
173+
if Syntax(decl) != Syntax(genericDecl), genericDecl.isProtocol((any DeclGroupSyntax).self) {
174+
return .containingNodeUnsupported(genericDecl, genericBecauseOf: Syntax(genericClause), whenUsing: attribute, on: decl)
175+
} else {
176+
// Avoid using a syntax node from a lexical context (it won't have source
177+
// location information.)
178+
let syntax = (genericClause.root != decl.root) ? Syntax(decl) : Syntax(genericClause)
179+
return Self(
180+
syntax: syntax,
181+
message: "Attribute \(_macroName(attribute)) cannot be applied to a generic \(_kindString(for: decl))",
182+
severity: .error
183+
)
184+
}
179185
}
180186

181187
/// Create a diagnostic message stating that the `@Test` or `@Suite` attribute
@@ -322,19 +328,35 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
322328
///
323329
/// - Parameters:
324330
/// - node: The lexical context preventing the use of `attribute`.
331+
/// - genericClause: If not `nil`, a syntax node that causes `node` to be
332+
/// generic.
325333
/// - attribute: The `@Test` or `@Suite` attribute.
326334
/// - decl: The declaration in question (contained in `node`.)
327335
///
328336
/// - Returns: A diagnostic message.
329-
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
330-
// It would be great if the diagnostic pointed to the containing lexical
331-
// context that was unsupported, but that node may be synthesized and does
332-
// not have reliable location information.
337+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
338+
// Avoid using a syntax node from a lexical context (it won't have source
339+
// location information.)
340+
let syntax: Syntax = if let genericClause, attribute.root == genericClause.root {
341+
// Prefer the generic clause if available as the root cause.
342+
genericClause
343+
} else if attribute.root == node.root {
344+
// Second choice is the unsupported containing node.
345+
Syntax(node)
346+
} else {
347+
// Finally, fall back to the attribute, which we assume is not detached.
348+
Syntax(attribute)
349+
}
350+
let generic = if genericClause != nil {
351+
" generic"
352+
} else {
353+
""
354+
}
333355
if let functionDecl = node.as(FunctionDeclSyntax.self) {
334356
let functionName = functionDecl.completeName
335357
return Self(
336-
syntax: Syntax(attribute),
337-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within function '\(functionName)'",
358+
syntax: syntax,
359+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) function '\(functionName)'",
338360
severity: .error
339361
)
340362
} else if let namedDecl = node.asProtocol((any NamedDeclSyntax).self) {
@@ -347,14 +369,32 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
347369
}
348370
let declName = namedDecl.name.textWithoutBackticks
349371
return Self(
350-
syntax: Syntax(attribute),
351-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(nonFinal) \(_kindString(for: node)) '\(declName)'",
372+
syntax: syntax,
373+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic)\(nonFinal) \(_kindString(for: node)) '\(declName)'",
374+
severity: .error
375+
)
376+
} else if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
377+
// Subtly different phrasing from the NamedDeclSyntax case above.
378+
let nodeKind = if genericClause != nil {
379+
"a generic extension to type"
380+
} else {
381+
"an extension to type"
382+
}
383+
let declGroupName = extensionDecl.extendedType.trimmedDescription
384+
return Self(
385+
syntax: syntax,
386+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind) '\(declGroupName)'",
352387
severity: .error
353388
)
354389
} else {
390+
let nodeKind = if genericClause != nil {
391+
"a generic \(_kindString(for: node))"
392+
} else {
393+
_kindString(for: node, includeA: true)
394+
}
355395
return Self(
356-
syntax: Syntax(attribute),
357-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(_kindString(for: node, includeA: true))",
396+
syntax: syntax,
397+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind)",
358398
severity: .error
359399
)
360400
}

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
101101
// generic functions when they are parameterized and the types line up, we
102102
// have not identified a need for them.
103103
if let genericClause = function.genericParameterClause {
104-
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: genericClause))
104+
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: genericClause, on: function))
105105
} else if let whereClause = function.genericWhereClause {
106-
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: whereClause))
106+
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: whereClause, on: function))
107107
} else {
108108
for parameter in parameterList {
109109
if parameter.type.isSome {
110-
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: parameter))
110+
diagnostics.append(.genericDeclarationNotSupported(function, whenUsing: testAttribute, becauseOf: parameter, on: function))
111111
}
112112
}
113113
}

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ struct TestDeclarationMacroTests {
124124
"struct S { func f(x: Int) { @Suite struct S { } } }":
125125
"Attribute 'Suite' cannot be applied to a structure within function 'f(x:)'",
126126
"struct S<T> { @Test func f() {} }":
127-
"Attribute 'Test' cannot be applied to a generic function",
127+
"Attribute 'Test' cannot be applied to a function within generic structure 'S'",
128128
"struct S<T> { @Suite struct S {} }":
129-
"Attribute 'Suite' cannot be applied to a generic structure",
129+
"Attribute 'Suite' cannot be applied to a structure within generic structure 'S'",
130130
"class C { @Test func f() {} }":
131131
"Attribute 'Test' cannot be applied to a function within non-final class 'C'",
132132
"class C { @Suite struct S {} }":
@@ -143,6 +143,22 @@ struct TestDeclarationMacroTests {
143143
"Attribute 'Test' cannot be applied to this function because it has been marked '@available(*, noasync)'",
144144
"@available(*, noasync) struct S { @Suite struct S {} }":
145145
"Attribute 'Suite' cannot be applied to this structure because it has been marked '@available(*, noasync)'",
146+
"extension [T] { @Test func f() {} }":
147+
"Attribute 'Test' cannot be applied to a function within a generic extension to type '[T]'",
148+
"extension [T] { @Suite struct S {} }":
149+
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type '[T]'",
150+
"extension [T:U] { @Test func f() {} }":
151+
"Attribute 'Test' cannot be applied to a function within a generic extension to type '[T:U]'",
152+
"extension [T:U] { @Suite struct S {} }":
153+
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type '[T:U]'",
154+
"extension T? { @Test func f() {} }":
155+
"Attribute 'Test' cannot be applied to a function within a generic extension to type 'T?'",
156+
"extension T? { @Suite struct S {} }":
157+
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type 'T?'",
158+
"extension T! { @Test func f() {} }":
159+
"Attribute 'Test' cannot be applied to a function within a generic extension to type 'T!'",
160+
"extension T! { @Suite struct S {} }":
161+
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type 'T!'",
146162
]
147163
)
148164
func invalidLexicalContext(input: String, expectedMessage: String) throws {
@@ -221,9 +237,10 @@ struct TestDeclarationMacroTests {
221237
]
222238
)
223239
func availabilityAttributeCapture(input: String, expectedOutputs: [String]) throws {
224-
let (actualOutput, _) = try parse(input)
240+
let (actualOutput, _) = try parse(input, removeWhitespace: true)
225241

226242
for expectedOutput in expectedOutputs {
243+
let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true)
227244
#expect(actualOutput.contains(expectedOutput))
228245
}
229246
}

Tests/TestingTests/Traits/TagListTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ struct TagListTests {
222222
#expect(Tag.Color.rgb(0, 0, 0) < .rgb(100, 100, 100))
223223
}
224224

225-
#if !SWT_NO_EXIT_TESTS && SWIFT_PM_SUPPORTS_SWIFT_TESTING
225+
#if !SWT_NO_EXIT_TESTS && SWIFT_PM_SUPPORTS_SWIFT_TESTING && !canImport(SwiftSyntax600)
226226
@Test("Invalid symbolic tag declaration")
227227
func invalidSymbolicTag() async {
228228
await #expect(exitsWith: .failure) {

0 commit comments

Comments
 (0)