Skip to content

[6.1] Diagnose when using a non-escapable type as suite. #992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ extension TypeSyntaxProtocol {
let nameWithoutGenericParameters = tokens(viewMode: .fixedUp)
.prefix { $0.tokenKind != .leftAngle }
.filter { $0.tokenKind != .period }
.filter { $0.tokenKind != .leftParen && $0.tokenKind != .rightParen }
.map(\.textWithoutBackticks)
.joined(separator: ".")

Expand Down
54 changes: 23 additions & 31 deletions Sources/TestingMacros/Support/DiagnosticMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,65 +321,57 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
/// generic.
/// - attribute: The `@Test` or `@Suite` attribute.
/// - decl: The declaration in question (contained in `node`.)
/// - escapableNonConformance: The suppressed conformance to `Escapable` for
/// `decl`, if present.
///
/// - Returns: A diagnostic message.
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol) -> Self {
static func containingNodeUnsupported(_ node: some SyntaxProtocol, genericBecauseOf genericClause: Syntax? = nil, whenUsing attribute: AttributeSyntax, on decl: some SyntaxProtocol, withSuppressedConformanceToEscapable escapableNonConformance: SuppressedTypeSyntax? = nil) -> Self {
// Avoid using a syntax node from a lexical context (it won't have source
// location information.)
let syntax: Syntax = if let genericClause, attribute.root == genericClause.root {
// Prefer the generic clause if available as the root cause.
genericClause
} else if let escapableNonConformance, attribute.root == escapableNonConformance.root {
// Then the ~Escapable conformance if present.
Syntax(escapableNonConformance)
} else if attribute.root == node.root {
// Second choice is the unsupported containing node.
// Next best choice is the unsupported containing node.
Syntax(node)
} else {
// Finally, fall back to the attribute, which we assume is not detached.
Syntax(attribute)
}

// Figure out the message to present.
var message = "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true))"
let generic = if genericClause != nil {
" generic"
} else {
""
}
if let functionDecl = node.as(FunctionDeclSyntax.self) {
let functionName = functionDecl.completeName
return Self(
syntax: syntax,
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) function '\(functionName)'",
severity: .error
)
message += " within\(generic) function '\(functionDecl.completeName)'"
} else if let namedDecl = node.asProtocol((any NamedDeclSyntax).self) {
let declName = namedDecl.name.textWithoutBackticks
return Self(
syntax: syntax,
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) \(_kindString(for: node)) '\(declName)'",
severity: .error
)
message += " within\(generic) \(_kindString(for: node)) '\(namedDecl.name.textWithoutBackticks)'"
} else if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
// Subtly different phrasing from the NamedDeclSyntax case above.
let nodeKind = if genericClause != nil {
"a generic extension to type"
if genericClause != nil {
message += " within a generic extension to type '\(extensionDecl.extendedType.trimmedDescription)'"
} else {
"an extension to type"
message += " within an extension to type '\(extensionDecl.extendedType.trimmedDescription)'"
}
let declGroupName = extensionDecl.extendedType.trimmedDescription
return Self(
syntax: syntax,
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind) '\(declGroupName)'",
severity: .error
)
} else {
let nodeKind = if genericClause != nil {
"a generic \(_kindString(for: node))"
if genericClause != nil {
message += " within a generic \(_kindString(for: node))"
} else {
_kindString(for: node, includeA: true)
message += " within \(_kindString(for: node, includeA: true))"
}
return Self(
syntax: syntax,
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within \(nodeKind)",
severity: .error
)
}
if escapableNonConformance != nil {
message += " because its conformance to 'Escapable' has been suppressed"
}

return Self(syntax: syntax, message: message, severity: .error)
}

/// Create a diagnostic message stating that the given attribute cannot be
Expand Down
20 changes: 18 additions & 2 deletions Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
}

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

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

// Disallow non-escapable types as suites. In order to support them, the
// compiler team needs to finish implementing the lifetime dependency
// feature so that `init()`, ``__requiringTry()`, and `__requiringAwait()`
// can be correctly expressed.
if let containingType = lexicalContext.first?.asProtocol((any DeclGroupSyntax).self),
let inheritedTypes = containingType.inheritanceClause?.inheritedTypes {
let escapableNonConformances = inheritedTypes
.map(\.type)
.compactMap { $0.as(SuppressedTypeSyntax.self) }
.filter { $0.type.isNamed("Escapable", inModuleNamed: "Swift") }
for escapableNonConformance in escapableNonConformances {
diagnostics.append(.containingNodeUnsupported(containingType, whenUsing: testAttribute, on: function, withSuppressedConformanceToEscapable: escapableNonConformance))
}
}

return !diagnostics.lazy.map(\.severity).contains(.error)
}

Expand Down
6 changes: 6 additions & 0 deletions Tests/TestingMacrosTests/TestDeclarationMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ struct TestDeclarationMacroTests {
"Attribute 'Test' cannot be applied to a function within a generic extension to type 'T!'",
"extension T! { @Suite struct S {} }":
"Attribute 'Suite' cannot be applied to a structure within a generic extension to type 'T!'",
"struct S: ~Escapable { @Test func f() {} }":
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
"struct S: ~Swift.Escapable { @Test func f() {} }":
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
"struct S: ~(Escapable) { @Test func f() {} }":
"Attribute 'Test' cannot be applied to a function within structure 'S' because its conformance to 'Escapable' has been suppressed",
]
)
func apiMisuseErrors(input: String, expectedMessage: String) throws {
Expand Down