Skip to content

Commit 622429f

Browse files
authored
Prevent adding tests to extensions to generic types. (#337)
When a test is added to a type extension, we can't see any information about the type being extended, so we cannot report invalid lexical contexts such as protocols or non-final classes. One of the invalid cases is a generic type, because swift-testing does not know how to realize the type—what generic arguments is it supposed to pass? This PR causes the compiler to emit a diagnostic when a generic type contains a test or suite. **How does it work?**, you ask? Generic types do not support static stored properties, while non-static types _do_ support them. So we just add a static stored property (of type `Void`, because the value is unimportant.) In a non-generic type, it will "just work." In a generic type, a diagnostic is emitted: > 🛑 Static stored properties not supported in generic types The diagnostic is non-optimal (it's not fine-tuned for swift-testing) but it prevents building an invalid test that can't be resolved at runtime, so it's better than nothing. This change works with swift-syntax-600 _and_ swift-syntax-510. ### 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 948b59d commit 622429f

File tree

3 files changed

+46
-1
lines changed

3 files changed

+46
-1
lines changed

Sources/TestingMacros/SuiteDeclarationMacro.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
113113
return []
114114
}
115115

116+
if let genericGuardDecl = makeGenericGuardDecl(guardingAgainst: declaration, in: context) {
117+
result.append(genericGuardDecl)
118+
}
119+
116120
// Parse the @Suite attribute.
117121
let attributeInfo = AttributeInfo(byParsing: suiteAttribute, on: declaration, in: context)
118122

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ func diagnoseIssuesWithLexicalContext(
173173
/// Diagnose issues with the lexical context containing a declaration.
174174
///
175175
/// - Parameters:
176+
/// - lexicalContext: The lexical context to inspect.
176177
/// - decl: The declaration to inspect.
177178
/// - attribute: The `@Test` or `@Suite` attribute applied to `decl`.
178-
/// - context: The macro context in which the expression is being parsed.
179179
///
180180
/// - Returns: An array of zero or more diagnostic messages related to the
181181
/// lexical context containing `decl`.
@@ -189,3 +189,40 @@ func diagnoseIssuesWithLexicalContext(
189189
.reduce(into: [], +=)
190190
}
191191
#endif
192+
193+
/// Create a declaration that prevents compilation if it is generic.
194+
///
195+
/// - Parameters:
196+
/// - decl: The declaration that should not be generic.
197+
/// - context: The macro context in which the expression is being parsed.
198+
///
199+
/// - Returns: A declaration that will fail to compile if `decl` is generic. The
200+
/// result declares a static member that should be added to the type
201+
/// containing `decl`. If `decl` is known not to be contained within a type
202+
/// extension, the result is `nil`.
203+
///
204+
/// This function disables the use of tests and suites inside extensions to
205+
/// generic types by adding a static property declaration (which generic types
206+
/// do not support.) This produces a compile-time error (not the perfect
207+
/// diagnostic to emit, but better than building successfully and failing
208+
/// silently at runtime.) ([126018850](rdar://126018850))
209+
func makeGenericGuardDecl(
210+
guardingAgainst decl: some DeclSyntaxProtocol,
211+
in context: some MacroExpansionContext
212+
) -> DeclSyntax? {
213+
#if canImport(SwiftSyntax600)
214+
guard context.lexicalContext.lazy.map(\.kind).contains(.extensionDecl) else {
215+
// Don't bother emitting a member if the declaration is not in an extension
216+
// because we'll already be able to emit a better error.
217+
return nil
218+
}
219+
#endif
220+
let genericGuardName = if let functionDecl = decl.as(FunctionDeclSyntax.self) {
221+
context.makeUniqueName(thunking: functionDecl)
222+
} else {
223+
context.makeUniqueName("")
224+
}
225+
return """
226+
private static let \(genericGuardName): Void = ()
227+
"""
228+
}

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
426426
}
427427
#endif
428428

429+
if typeName != nil, let genericGuardDecl = makeGenericGuardDecl(guardingAgainst: functionDecl, in: context) {
430+
result.append(genericGuardDecl)
431+
}
432+
429433
// Parse the @Test attribute.
430434
let attributeInfo = AttributeInfo(byParsing: testAttribute, on: functionDecl, in: context)
431435
if attributeInfo.hasFunctionArguments != !functionDecl.signature.parameterClause.parameters.isEmpty {

0 commit comments

Comments
 (0)