diff --git a/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift b/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift index e6e381e94..e1bd346ed 100644 --- a/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TypeSyntaxProtocolAdditions.swift @@ -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: ".") diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index 7d8a83c20..420e216ad 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -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 diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 0726aa099..d52baa0d2 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -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)) } @@ -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) } diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index c75166c66..6fbadf2ec 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -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 {