Skip to content

Commit 52a4565

Browse files
authored
Disallow test/suite declarations in protocols. (#333)
This change is similar to #332 except that the diagnostic is applied when a test or suite is declared inside a protocol rather than inside a non-final class. Without this change, the compiler will fail to build a test target with such a test _anyway_, but the diagnostics produced will refer to the emitted macro expansion and are opaque. For example, given the declaration: ```swift protocol P { @test func f() } ``` The compiler produces these diagnostics which don't really explain the problem very well: > 🛑 'private' modifier cannot be used in protocols > 🛑 Protocol methods must not have bodies > 🛑 Type '$s12TestingTests1PP1f4TestfMp_37__🟠$test_container__function__funcf__fMu_' cannot be nested in protocol 'P' > 🛑 Use of protocol 'P' as a type must be written 'any P' > 🛑 Cannot find '$s12TestingTests1PP1f4TestfMp_7funcf__fMu0_' in scope This is because the swift-testing macro target is unaware that the enclosing lexical context is a protocol, so it tries to emit code that would only make sense inside a concrete type. With this change, you instead get: > 🛑 Attribute 'Test' cannot be applied to a function within protocol 'P' I also took the liberty of improving the diagnostic emitted if a test/suite is declared inside a closure. > [!NOTE] > As with #332, this change only takes effect when swift-syntax-600 is used. ### 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 d349ba7 commit 52a4565

File tree

3 files changed

+34
-10
lines changed

3 files changed

+34
-10
lines changed

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ func diagnoseIssuesWithLexicalContext(
114114
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
115115
diagnostics.append(.containingNodeUnsupported(classDecl, whenUsing: attribute, on: decl))
116116
}
117+
} else if let protocolDecl = lexicalContext.as(ProtocolDeclSyntax.self) {
118+
diagnostics.append(.containingNodeUnsupported(protocolDecl, whenUsing: attribute, on: decl))
117119
}
118120
}
119121

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
149149
result = ("macro", "a")
150150
case .protocolDecl:
151151
result = ("protocol", "a")
152+
case .closureExpr:
153+
result = ("closure", "a")
152154
default:
153155
result = ("declaration", "this")
154156
}
@@ -244,16 +246,28 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
244246
// It would be great if the diagnostic pointed to the containing lexical
245247
// context that was unsupported, but that node may be synthesized and does
246248
// not have reliable location information.
247-
switch node.kind {
248-
case .classDecl:
249+
if let functionDecl = node.as(FunctionDeclSyntax.self) {
250+
let functionName = functionDecl.completeName
251+
return Self(
252+
syntax: Syntax(attribute),
253+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within function '\(functionName)'",
254+
severity: .error
255+
)
256+
} else if let namedDecl = node.asProtocol((any NamedDeclSyntax).self) {
249257
// Special-case class declarations as implicitly non-final (since we would
250258
// only diagnose a class here if it were non-final.)
259+
let nonFinal = if node.is(ClassDeclSyntax.self) {
260+
" non-final"
261+
} else {
262+
""
263+
}
264+
let declName = namedDecl.name.textWithoutBackticks
251265
return Self(
252266
syntax: Syntax(attribute),
253-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within a non-final class",
267+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(nonFinal) \(_kindString(for: node)) '\(declName)'",
254268
severity: .error
255269
)
256-
default:
270+
} else {
257271
return Self(
258272
syntax: Syntax(attribute),
259273
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(_kindString(for: node, includeA: true))",

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,22 @@ struct TestDeclarationMacroTests {
119119
#if canImport(SwiftSyntax600)
120120
@Test("Error diagnostics emitted for invalid lexical contexts",
121121
arguments: [
122-
"struct S { func f() { @Test func f() {} } }":
123-
"Attribute 'Test' cannot be applied to a function within a function",
124-
"struct S { func f() { @Suite struct S { } } }":
125-
"Attribute 'Suite' cannot be applied to a structure within a function",
122+
"struct S { func f() { @Test func g() {} } }":
123+
"Attribute 'Test' cannot be applied to a function within function 'f()'",
124+
"struct S { func f(x: Int) { @Suite struct S { } } }":
125+
"Attribute 'Suite' cannot be applied to a structure within function 'f(x:)'",
126126
"class C { @Test func f() {} }":
127-
"Attribute 'Test' cannot be applied to a function within a non-final class",
127+
"Attribute 'Test' cannot be applied to a function within non-final class 'C'",
128128
"class C { @Suite struct S {} }":
129-
"Attribute 'Suite' cannot be applied to a structure within a non-final class",
129+
"Attribute 'Suite' cannot be applied to a structure within non-final class 'C'",
130+
"protocol P { @Test func f() {} }":
131+
"Attribute 'Test' cannot be applied to a function within protocol 'P'",
132+
"protocol P { @Suite struct S {} }":
133+
"Attribute 'Suite' cannot be applied to a structure within protocol 'P'",
134+
"{ _ in @Test func f() {} }":
135+
"Attribute 'Test' cannot be applied to a function within a closure",
136+
"{ _ in @Suite struct S {} }":
137+
"Attribute 'Suite' cannot be applied to a structure within a closure",
130138
]
131139
)
132140
func invalidLexicalContext(input: String, expectedMessage: String) throws {

0 commit comments

Comments
 (0)