Skip to content

Commit 336fad6

Browse files
authored
[6.1] Diagnose when using a non-escapable type as suite. (#992)
- **Explanation**: Presents a custom diagnostic if you try to put a test function in a non-escapable type since that's not supported by the language yet. - **Scope**: Tests in non-escapable types. - **Issues**: N/A - **Original PRs**: #988 - **Risk**: Low (shouldn't be any code out there doing this since it doesn't compile.) - **Testing**: Unit test coverage is in place. - **Reviewers**: @briancroom @stmontgomery
1 parent c13feaf commit 336fad6

File tree

4 files changed

+48
-33
lines changed

4 files changed

+48
-33
lines changed

Diff for: Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ extension TypeSyntaxProtocol {
5454
let nameWithoutGenericParameters = tokens(viewMode: .fixedUp)
5555
.prefix { $0.tokenKind != .leftAngle }
5656
.filter { $0.tokenKind != .period }
57+
.filter { $0.tokenKind != .leftParen && $0.tokenKind != .rightParen }
5758
.map(\.textWithoutBackticks)
5859
.joined(separator: ".")
5960

Diff for: Sources/TestingMacros/Support/DiagnosticMessage.swift

+23-31
Original file line numberDiff line numberDiff line change
@@ -321,65 +321,57 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
321321
/// generic.
322322
/// - attribute: The `@Test` or `@Suite` attribute.
323323
/// - decl: The declaration in question (contained in `node`.)
324+
/// - escapableNonConformance: The suppressed conformance to `Escapable` for
325+
/// `decl`, if present.
324326
///
325327
/// - Returns: A diagnostic message.
326-
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
328+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol, withSuppressedConformanceToEscapable escapableNonConformance: SuppressedTypeSyntax? = nil) -> Self {
327329
// Avoid using a syntax node from a lexical context (it won't have source
328330
// location information.)
329331
let syntax: Syntax = if let genericClause, attribute.root == genericClause.root {
330332
// Prefer the generic clause if available as the root cause.
331333
genericClause
334+
} else if let escapableNonConformance, attribute.root == escapableNonConformance.root {
335+
// Then the ~Escapable conformance if present.
336+
Syntax(escapableNonConformance)
332337
} else if attribute.root == node.root {
333-
// Second choice is the unsupported containing node.
338+
// Next best choice is the unsupported containing node.
334339
Syntax(node)
335340
} else {
336341
// Finally, fall back to the attribute, which we assume is not detached.
337342
Syntax(attribute)
338343
}
344+
345+
// Figure out the message to present.
346+
var message = "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true))"
339347
let generic = if genericClause != nil {
340348
" generic"
341349
} else {
342350
""
343351
}
344352
if let functionDecl = node.as(FunctionDeclSyntax.self) {
345-
let functionName = functionDecl.completeName
346-
return Self(
347-
syntax: syntax,
348-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) function '\(functionName)'",
349-
severity: .error
350-
)
353+
message += " within\(generic) function '\(functionDecl.completeName)'"
351354
} else if let namedDecl = node.asProtocol((any NamedDeclSyntax).self) {
352-
let declName = namedDecl.name.textWithoutBackticks
353-
return Self(
354-
syntax: syntax,
355-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) \(_kindString(for: node)) '\(declName)'",
356-
severity: .error
357-
)
355+
message += " within\(generic) \(_kindString(for: node)) '\(namedDecl.name.textWithoutBackticks)'"
358356
} else if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
359357
// Subtly different phrasing from the NamedDeclSyntax case above.
360-
let nodeKind = if genericClause != nil {
361-
"a generic extension to type"
358+
if genericClause != nil {
359+
message += " within a generic extension to type '\(extensionDecl.extendedType.trimmedDescription)'"
362360
} else {
363-
"an extension to type"
361+
message += " within an extension to type '\(extensionDecl.extendedType.trimmedDescription)'"
364362
}
365-
let declGroupName = extensionDecl.extendedType.trimmedDescription
366-
return Self(
367-
syntax: syntax,
368-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind) '\(declGroupName)'",
369-
severity: .error
370-
)
371363
} else {
372-
let nodeKind = if genericClause != nil {
373-
"a generic \(_kindString(for: node))"
364+
if genericClause != nil {
365+
message += " within a generic \(_kindString(for: node))"
374366
} else {
375-
_kindString(for: node, includeA: true)
367+
message += " within \(_kindString(for: node, includeA: true))"
376368
}
377-
return Self(
378-
syntax: syntax,
379-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind)",
380-
severity: .error
381-
)
382369
}
370+
if escapableNonConformance != nil {
371+
message += " because its conformance to 'Escapable' has been suppressed"
372+
}
373+
374+
return Self(syntax: syntax, message: message, severity: .error)
383375
}
384376

385377
/// Create a diagnostic message stating that the given attribute cannot be

Diff for: Sources/TestingMacros/TestDeclarationMacro.swift

+18-2
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
6161
}
6262

6363
// Check if the lexical context is appropriate for a suite or test.
64-
diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: testAttribute)
64+
let lexicalContext = context.lexicalContext
65+
diagnostics += diagnoseIssuesWithLexicalContext(lexicalContext, containing: declaration, attribute: testAttribute)
6566

6667
// Suites inheriting from XCTestCase are not supported. We are a bit
6768
// conservative here in this check and only check the immediate context.
6869
// Presumably, if there's an intermediate lexical context that is *not* a
6970
// type declaration, then it must be a function or closure (disallowed
7071
// elsewhere) and thus the test function is not a member of any type.
71-
if let containingTypeDecl = context.lexicalContext.first?.asProtocol((any DeclGroupSyntax).self),
72+
if let containingTypeDecl = lexicalContext.first?.asProtocol((any DeclGroupSyntax).self),
7273
containingTypeDecl.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") {
7374
diagnostics.append(.containingNodeUnsupported(containingTypeDecl, whenUsing: testAttribute, on: declaration))
7475
}
@@ -118,6 +119,21 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
118119
}
119120
}
120121

122+
// Disallow non-escapable types as suites. In order to support them, the
123+
// compiler team needs to finish implementing the lifetime dependency
124+
// feature so that `init()`, ``__requiringTry()`, and `__requiringAwait()`
125+
// can be correctly expressed.
126+
if let containingType = lexicalContext.first?.asProtocol((any DeclGroupSyntax).self),
127+
let inheritedTypes = containingType.inheritanceClause?.inheritedTypes {
128+
let escapableNonConformances = inheritedTypes
129+
.map(\.type)
130+
.compactMap { $0.as(SuppressedTypeSyntax.self) }
131+
.filter { $0.type.isNamed("Escapable", inModuleNamed: "Swift") }
132+
for escapableNonConformance in escapableNonConformances {
133+
diagnostics.append(.containingNodeUnsupported(containingType, whenUsing: testAttribute, on: function, withSuppressedConformanceToEscapable: escapableNonConformance))
134+
}
135+
}
136+
121137
return !diagnostics.lazy.map(\.severity).contains(.error)
122138
}
123139

Diff for: Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

+6
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ struct TestDeclarationMacroTests {
140140
"Attribute 'Test' cannot be applied to a function within a generic extension to type 'T!'",
141141
"extension T! { @Suite struct S {} }":
142142
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type 'T!'",
143+
"struct S: ~Escapable { @Test func f() {} }":
144+
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
145+
"struct S: ~Swift.Escapable { @Test func f() {} }":
146+
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
147+
"struct S: ~(Escapable) { @Test func f() {} }":
148+
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
143149
]
144150
)
145151
func apiMisuseErrors(input: String, expectedMessage: String) throws {

0 commit comments

Comments
 (0)