Skip to content

Commit d349ba7

Browse files
authored
Diagnose when a test or suite is embedded in a non-final class. (#332)
1 parent 11c8eab commit d349ba7

File tree

4 files changed

+39
-15
lines changed

4 files changed

+39
-15
lines changed

Sources/Testing/Testing.docc/OrganizingTests.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,6 @@ actor CashRegisterTests: NSObject { ... } // ✅ OK: actors are implicitly final
163163
class MenuItemTests { ... } // ❌ ERROR: this class is not final
164164
```
165165

166-
- Bug: Violations of this requirement are not consistently diagnosed at compile
167-
time, and the diagnostic produced when an issue is detected may be confusing
168-
to developers. ([105470382](rdar://105470382))
169-
170166
## Topics
171167

172168
- ``Suite(_:_:)``

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,20 @@ func diagnoseIssuesWithLexicalContext(
103103
attribute: AttributeSyntax,
104104
in context: some MacroExpansionContext
105105
) -> [DiagnosticMessage] {
106-
context.lexicalContext
107-
.filter { !$0.isProtocol((any DeclGroupSyntax).self) }
108-
.map { .containingNodeUnsupported($0, whenUsing: attribute) }
106+
var diagnostics = [DiagnosticMessage]()
107+
108+
for lexicalContext in context.lexicalContext {
109+
if !lexicalContext.isProtocol((any DeclGroupSyntax).self) {
110+
diagnostics.append(.containingNodeUnsupported(lexicalContext, whenUsing: attribute, on: decl))
111+
}
112+
113+
if let classDecl = lexicalContext.as(ClassDeclSyntax.self) {
114+
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
115+
diagnostics.append(.containingNodeUnsupported(classDecl, whenUsing: attribute, on: decl))
116+
}
117+
}
118+
}
119+
120+
return diagnostics
109121
}
110122
#endif

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,17 +237,29 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
237237
/// - Parameters:
238238
/// - node: The lexical context preventing the use of `attribute`.
239239
/// - attribute: The `@Test` or `@Suite` attribute.
240+
/// - decl: The declaration in question (contained in `node`.)
240241
///
241242
/// - Returns: A diagnostic message.
242-
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self {
243+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
243244
// It would be great if the diagnostic pointed to the containing lexical
244245
// context that was unsupported, but that node may be synthesized and does
245246
// not have reliable location information.
246-
Self(
247-
syntax: Syntax(attribute),
248-
message: "Attribute \(_macroName(attribute)) cannot be applied within \(_kindString(for: node, includeA: true)).",
249-
severity: .error
250-
)
247+
switch node.kind {
248+
case .classDecl:
249+
// Special-case class declarations as implicitly non-final (since we would
250+
// only diagnose a class here if it were non-final.)
251+
return Self(
252+
syntax: Syntax(attribute),
253+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within a non-final class",
254+
severity: .error
255+
)
256+
default:
257+
return Self(
258+
syntax: Syntax(attribute),
259+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(_kindString(for: node, includeA: true))",
260+
severity: .error
261+
)
262+
}
251263
}
252264
#endif
253265

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,13 @@ struct TestDeclarationMacroTests {
120120
@Test("Error diagnostics emitted for invalid lexical contexts",
121121
arguments: [
122122
"struct S { func f() { @Test func f() {} } }":
123-
"The @Test attribute cannot be applied within a function.",
123+
"Attribute 'Test' cannot be applied to a function within a function",
124124
"struct S { func f() { @Suite struct S { } } }":
125-
"The @Suite attribute cannot be applied within a function.",
125+
"Attribute 'Suite' cannot be applied to a structure within a function",
126+
"class C { @Test func f() {} }":
127+
"Attribute 'Test' cannot be applied to a function within a non-final class",
128+
"class C { @Suite struct S {} }":
129+
"Attribute 'Suite' cannot be applied to a structure within a non-final class",
126130
]
127131
)
128132
func invalidLexicalContext(input: String, expectedMessage: String) throws {

0 commit comments

Comments
 (0)