From a98ed0465490558d4981d7f5fbea728202cae3d5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 11 Mar 2024 17:12:39 -0400 Subject: [PATCH 1/2] Adopt `lexicalContext` from swift-syntax-6.0.0. This PR conditionally adopts the new `lexicalContext` member of `MacroExpansionContext` added in https://github.com/apple/swift-syntax/pull/1554. If the SwiftSyntax600 pseudo-module is available, then this member should be available for use and can be used to perform additional diagnostics for tests and to get the names of their containing types. With this PR, if built against an older toolchain (5.11 or earlier), the old hacky "is there leading whitespace?" mechanism is still used. A future PR will recursively perform suite-level diagnostics on the lexical contexts containing tests and suites, so that a test cannot be (easily) inserted into a type that cannot be used as a suite. Resolves rdar://109439578. --- .../TestingMacros/SuiteDeclarationMacro.swift | 22 ++++- .../FunctionDeclSyntaxAdditions.swift | 14 ++- ...ift => DiagnosticMessage+Diagnosing.swift} | 21 +++++ .../Support/DiagnosticMessage.swift | 32 ++++++- .../TestingMacros/TestDeclarationMacro.swift | 40 +++++++- .../TestDeclarationMacroTests.swift | 51 ++++++++-- .../TestSupport/Parse.swift | 9 ++ Tests/TestingTests/RunnerTests.swift | 94 +++++++++---------- 8 files changed, 214 insertions(+), 69 deletions(-) rename Sources/TestingMacros/Support/{TagConstraints.swift => DiagnosticMessage+Diagnosing.swift} (83%) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 5b9d9985b..c4eaf1239 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -21,7 +21,9 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) + guard _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) else { + return [] + } return _createTestContainerDecls(for: declaration, suiteAttribute: node, in: context) } @@ -33,7 +35,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { // The peer macro expansion of this macro is only used to diagnose misuses // on symbols that are not decl groups. if declaration.asProtocol((any DeclGroupSyntax).self) == nil { - _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) + _ = _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) } return [] } @@ -44,23 +46,31 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { /// - declaration: The type declaration to diagnose. /// - suiteAttribute: The `@Suite` attribute applied to `declaration`. /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: Whether or not macro expansion should continue (i.e. stopping + /// if a fatal error was diagnosed.) private static func _diagnoseIssues( with declaration: some SyntaxProtocol, suiteAttribute: AttributeSyntax, in context: some MacroExpansionContext - ) { + ) -> Bool { var diagnostics = [DiagnosticMessage]() defer { - diagnostics.forEach(context.diagnose) + context.diagnose(diagnostics) } // The @Suite attribute is only supported on type declarations, all of which // are DeclGroupSyntax types. guard let declaration = declaration.asProtocol((any DeclGroupSyntax).self) else { diagnostics.append(.attributeNotSupported(suiteAttribute, on: declaration)) - return + return false } +#if canImport(SwiftSyntax600) + // Check if the lexical context is appropriate for a suite or test. + diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: suiteAttribute, in: context) +#endif + // Generic suites are not supported. if let genericClause = declaration.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause { diagnostics.append(.genericDeclarationNotSupported(declaration, whenUsing: suiteAttribute, becauseOf: genericClause)) @@ -115,6 +125,8 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { diagnostics.append(.availabilityAttributeNotSupported(noasyncAttribute, on: declaration, whenUsing: suiteAttribute)) } } + + return !diagnostics.lazy.map(\.severity).contains(.error) } /// Create a declaration for a type that conforms to the `__TestContainer` diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index 085452ee8..ab2e3dfa1 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -94,9 +94,17 @@ extension FunctionDeclSyntax { if signature.effectSpecifiers?.asyncSpecifier != nil { selector += "WithCompletionHandler" colonToken = .colonToken() - } else if signature.effectSpecifiers?.throwsSpecifier != nil { - selector += "AndReturnError" - colonToken = .colonToken() + } else { + let hasThrowsSpecifier: Bool +#if canImport(SwiftSyntax600) + hasThrowsSpecifier = signature.effectSpecifiers?.throwsClause != nil +#else + hasThrowsSpecifier = signature.effectSpecifiers?.throwsSpecifier != nil +#endif + if hasThrowsSpecifier { + selector += "AndReturnError" + colonToken = .colonToken() + } } return ObjCSelectorPieceListSyntax { ObjCSelectorPieceSyntax(name: .identifier(selector), colon: colonToken) diff --git a/Sources/TestingMacros/Support/TagConstraints.swift b/Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift similarity index 83% rename from Sources/TestingMacros/Support/TagConstraints.swift rename to Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift index 0542a515b..faea21f5a 100644 --- a/Sources/TestingMacros/Support/TagConstraints.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift @@ -87,3 +87,24 @@ func diagnoseIssuesWithTags(in traitExprs: [ExprSyntax], addedTo attribute: Attr } } } + +#if canImport(SwiftSyntax600) +/// Diagnose issues with the lexical context containing a declaration. +/// +/// - Parameters: +/// - decl: The declaration to inspect. +/// - testAttribute: The `@Test` attribute applied to `decl`. +/// - context: The macro context in which the expression is being parsed. +/// +/// - Returns: An array of zero or more diagnostic messages related to the +/// lexical context containing `decl`. +func diagnoseIssuesWithLexicalContext( + containing decl: some DeclSyntaxProtocol, + attribute: AttributeSyntax, + in context: some MacroExpansionContext +) -> [DiagnosticMessage] { + context.lexicalContext + .filter { !$0.isProtocol((any DeclGroupSyntax).self) } + .map { .containingNodeUnsupported($0, whenUsing: attribute) } +} +#endif diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index a4d2a8c85..b1747aa51 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -225,6 +225,27 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { ) } +#if canImport(SwiftSyntax600) + /// Create a diagnostic message stating that the given attribute cannot be + /// used within a lexical context. + /// + /// - Parameters: + /// - node: The lexical context preventing the use of `attribute`. + /// - attribute: The `@Test` or `@Suite` attribute. + /// + /// - Returns: A diagnostic message. + static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self { + // It would be great if the diagnostic pointed to the containing lexical + // context that was unsupported, but that node may be synthesized and does + // not have reliable location information. + Self( + syntax: Syntax(attribute), + message: "The @\(attribute.attributeNameText) attribute cannot be applied within \(_kindString(for: node, includeA: true)).", + severity: .error + ) + } +#endif + /// Create a diagnostic message stating that the given attribute has no effect /// when applied to the given extension declaration. /// @@ -406,7 +427,6 @@ extension MacroExpansionContext { /// - message: The diagnostic message to emit. The `node` and `position` /// arguments to `Diagnostic.init()` are derived from the message's /// `syntax` property. - /// - fixIts: Any Fix-Its to apply. func diagnose(_ message: DiagnosticMessage) { diagnose( Diagnostic( @@ -418,6 +438,16 @@ extension MacroExpansionContext { ) } + /// Emit a sequence of diagnostic messages. + /// + /// - Parameters: + /// - messages: The diagnostic messages to emit. + func diagnose(_ messages: some Sequence) { + for message in messages { + diagnose(message) + } + } + /// Emit a diagnostic message for debugging purposes during development of the /// testing library. /// diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 98120b1fc..b031dcfb4 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -21,7 +21,9 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - _diagnoseIssues(with: declaration, testAttribute: node, in: context) + guard _diagnoseIssues(with: declaration, testAttribute: node, in: context) else { + return [] + } guard let function = declaration.as(FunctionDeclSyntax.self) else { return [] @@ -45,6 +47,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { testAttribute: AttributeSyntax, in context: some MacroExpansionContext ) -> TypeSyntax? { +#if canImport(SwiftSyntax600) + let types = context.lexicalContext + .compactMap { $0.asProtocol((any DeclGroupSyntax).self) } + .map(\.type) + .reversed() + if types.isEmpty { + return nil + } + let typeName = types.map(\.trimmedDescription).joined(separator: ".") + return "\(raw: typeName)" +#else // Find the beginning of the first attribute on the declaration, including // those embedded in #if statements, to account for patterns like // `@MainActor @Test func` where there's a space ahead of @Test, but the @@ -79,6 +92,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { return TypeSyntax(IdentifierTypeSyntax(name: .keyword(.Self))) } return nil +#endif } /// Diagnose issues with a `@Test` declaration. @@ -87,22 +101,30 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { /// - declaration: The function declaration to diagnose. /// - testAttribute: The `@Test` attribute applied to `declaration`. /// - context: The macro context in which the expression is being parsed. + /// + /// - Returns: Whether or not macro expansion should continue (i.e. stopping + /// if a fatal error was diagnosed.) private static func _diagnoseIssues( with declaration: some DeclSyntaxProtocol, testAttribute: AttributeSyntax, in context: some MacroExpansionContext - ) { + ) -> Bool { var diagnostics = [DiagnosticMessage]() defer { - diagnostics.forEach(context.diagnose) + context.diagnose(diagnostics) } // The @Test attribute is only supported on function declarations. guard let function = declaration.as(FunctionDeclSyntax.self) else { diagnostics.append(.attributeNotSupported(testAttribute, on: declaration)) - return + return false } +#if canImport(SwiftSyntax600) + // Check if the lexical context is appropriate for a suite or test. + diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: testAttribute, in: context) +#endif + // Only one @Test attribute is supported. let suiteAttributes = function.attributes(named: "Test", in: context) if suiteAttributes.count > 1 { @@ -144,6 +166,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { } } } + + return !diagnostics.lazy.map(\.severity).contains(.error) } /// Create a function call parameter list used to call a function from its @@ -406,6 +430,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { ) -> [DeclSyntax] { var result = [DeclSyntax]() +#if canImport(SwiftSyntax600) + // Get the name of the type containing the function for passing to the test + // factory function later. + let typealiasExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil" +#else // We cannot directly refer to Self here because it will end up being // resolved as the __TestContainer type we generate. Create a uniquely-named // reference to Self outside the context of the generated type, and use it @@ -415,7 +444,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // inside a static computed property instead of a typealias (where covariant // Self is disallowed.) // - // This "typealias" will not be necessary when rdar://105470382 is resolved. + // This "typealias" is not necessary when swift-syntax-6.0.0 is available. var typealiasExpr: ExprSyntax = "nil" if let typeName { let typealiasName = context.makeUniqueName(thunking: functionDecl) @@ -430,6 +459,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { typealiasExpr = "\(typealiasName)" } +#endif // Parse the @Test attribute. let attributeInfo = AttributeInfo(byParsing: testAttribute, on: functionDecl, in: context) diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 1bd5df1f1..5b205abce 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -67,7 +67,6 @@ struct TestDeclarationMacroTests { "@Test enum E {}": "Attribute 'Test' cannot be applied to an enumeration", - // Availability "@available(*, unavailable) @Suite struct S {}": "Attribute 'Suite' cannot be applied to this structure because it has been marked '@available(*, unavailable)'", @@ -117,6 +116,26 @@ struct TestDeclarationMacroTests { } } +#if canImport(SwiftSyntax600) + @Test("Error diagnostics emitted for invalid lexical contexts", + arguments: [ + "struct S { func f() { @Test func f() {} } }": + "The @Test attribute cannot be applied within a function.", + "struct S { func f() { @Suite struct S { } } }": + "The @Suite attribute cannot be applied within a function.", + ] + ) + func invalidLexicalContext(input: String, expectedMessage: String) throws { + let (_, diagnostics) = try parse(input) + + #expect(diagnostics.count > 0) + for diagnostic in diagnostics { + #expect(diagnostic.diagMessage.severity == .error) + #expect(diagnostic.message == expectedMessage) + } + } +#endif + @Test("Warning diagnostics emitted on API misuse", arguments: [ // return types @@ -189,13 +208,9 @@ struct TestDeclarationMacroTests { } } - @Test("Different kinds of functions are handled correctly", - arguments: [ + static var functionTypeInputs: [(String, String?, String?)] { + var result: [(String, String?, String?)] = [ ("@Test func f() {}", nil, nil), - ("struct S {\n\t@Test func f() {} }", "Self", "let"), - ("struct S {\n\t@Test mutating func f() {} }", "Self", "var"), - ("struct S {\n\t@Test static func f() {} }", "Self", nil), - ("final class S {\n\t@Test class func f() {} }", "Self", nil), ("@Test @available(*, noasync) @MainActor func f() {}", nil, "MainActor.run"), ("@Test @_unavailableFromAsync @MainActor func f() {}", nil, "MainActor.run"), ("@Test @available(*, noasync) func f() {}", nil, "__requiringTry"), @@ -220,7 +235,27 @@ struct TestDeclarationMacroTests { nil ), ] - ) + +#if canImport(SwiftSyntax600) + result += [ + ("struct S_NAME {\n\t@Test func f() {} }", "S_NAME", "let"), + ("struct S_NAME {\n\t@Test mutating func f() {} }", "S_NAME", "var"), + ("struct S_NAME {\n\t@Test static func f() {} }", "S_NAME", nil), + ("final class C_NAME {\n\t@Test class func f() {} }", "C_NAME", nil), + ] +#else + result += [ + ("struct S {\n\t@Test func f() {} }", "Self", "let"), + ("struct S {\n\t@Test mutating func f() {} }", "Self", "var"), + ("struct S {\n\t@Test static func f() {} }", "Self", nil), + ("final class C {\n\t@Test class func f() {} }", "Self", nil), + ] +#endif + + return result + } + + @Test("Different kinds of functions are handled correctly", arguments: functionTypeInputs) func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws { let (output, _) = try parse(input) diff --git a/Tests/TestingMacrosTests/TestSupport/Parse.swift b/Tests/TestingMacrosTests/TestSupport/Parse.swift index 093bc6e90..ff4f79d7b 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -35,8 +35,17 @@ func parse(_ sourceCode: String, activeMacros activeMacroNames: [String] = [], r } let operatorTable = OperatorTable.standardOperators let originalSyntax = try operatorTable.foldAll(Parser.parse(source: sourceCode)) +#if canImport(SwiftSyntax600) + let context = BasicMacroExpansionContext(lexicalContext: [], expansionDiscriminator: "", sourceFiles: [:]) + let syntax = try operatorTable.foldAll( + originalSyntax.expand(macros: activeMacros) { syntax in + BasicMacroExpansionContext(sharingWith: context, lexicalContext: syntax.allMacroLexicalContexts()) + } + ) +#else let context = BasicMacroExpansionContext(expansionDiscriminator: "", sourceFiles: [:]) let syntax = try operatorTable.foldAll(originalSyntax.expand(macros: activeMacros, in: context)) +#endif var sourceCode = String(describing: syntax.formatted().trimmed) if removeWhitespace { sourceCode = sourceCode.filter { !$0.isWhitespace } diff --git a/Tests/TestingTests/RunnerTests.swift b/Tests/TestingTests/RunnerTests.swift index 9b860b675..27981eadd 100644 --- a/Tests/TestingTests/RunnerTests.swift +++ b/Tests/TestingTests/RunnerTests.swift @@ -431,15 +431,15 @@ final class RunnerTests: XCTestCase { await fulfillment(of: [expectationCheckedAndPassed, expectationCheckedAndFailed], timeout: 0.0) } - func testPoundIfTrueTestFunctionRuns() async throws { - @Suite(.hidden) struct S { + @Suite(.hidden) struct PoundIfTrueTest { #if true - @Test(.hidden) func f() {} - @Test(.hidden) func g() {} + @Test(.hidden) func f() {} + @Test(.hidden) func g() {} #endif - @Test(.hidden) func h() {} - } + @Test(.hidden) func h() {} + } + func testPoundIfTrueTestFunctionRuns() async throws { let testStarted = expectation(description: "Test started") testStarted.expectedFulfillmentCount = 4 var configuration = Configuration() @@ -448,19 +448,19 @@ final class RunnerTests: XCTestCase { testStarted.fulfill() } } - await runTest(for: S.self, configuration: configuration) + await runTest(for: PoundIfTrueTest.self, configuration: configuration) await fulfillment(of: [testStarted], timeout: 0.0) } - func testPoundIfFalseTestFunctionDoesNotRun() async throws { - @Suite(.hidden) struct S { + @Suite(.hidden) struct PoundIfFalseTest { #if false - @Test(.hidden) func f() {} - @Test(.hidden) func g() {} + @Test(.hidden) func f() {} + @Test(.hidden) func g() {} #endif - @Test(.hidden) func h() {} - } + @Test(.hidden) func h() {} + } + func testPoundIfFalseTestFunctionDoesNotRun() async throws { let testStarted = expectation(description: "Test started") testStarted.expectedFulfillmentCount = 2 var configuration = Configuration() @@ -469,21 +469,21 @@ final class RunnerTests: XCTestCase { testStarted.fulfill() } } - await runTest(for: S.self, configuration: configuration) + await runTest(for: PoundIfFalseTest.self, configuration: configuration) await fulfillment(of: [testStarted], timeout: 0.0) } - func testPoundIfFalseElseTestFunctionRuns() async throws { - @Suite(.hidden) struct S { + @Suite(.hidden) struct PoundIfFalseElseTest { #if false #elseif false #else - @Test(.hidden) func f() {} - @Test(.hidden) func g() {} + @Test(.hidden) func f() {} + @Test(.hidden) func g() {} #endif - @Test(.hidden) func h() {} - } + @Test(.hidden) func h() {} + } + func testPoundIfFalseElseTestFunctionRuns() async throws { let testStarted = expectation(description: "Test started") testStarted.expectedFulfillmentCount = 4 var configuration = Configuration() @@ -492,21 +492,21 @@ final class RunnerTests: XCTestCase { testStarted.fulfill() } } - await runTest(for: S.self, configuration: configuration) + await runTest(for: PoundIfFalseElseTest.self, configuration: configuration) await fulfillment(of: [testStarted], timeout: 0.0) } - func testPoundIfFalseElseIfTestFunctionRuns() async throws { - @Suite(.hidden) struct S { + @Suite(.hidden) struct PoundIfFalseElseIfTest { #if false #elseif false #elseif true - @Test(.hidden) func f() {} - @Test(.hidden) func g() {} + @Test(.hidden) func f() {} + @Test(.hidden) func g() {} #endif - @Test(.hidden) func h() {} - } + @Test(.hidden) func h() {} + } + func testPoundIfFalseElseIfTestFunctionRuns() async throws { let testStarted = expectation(description: "Test started") testStarted.expectedFulfillmentCount = 4 var configuration = Configuration() @@ -515,35 +515,35 @@ final class RunnerTests: XCTestCase { testStarted.fulfill() } } - await runTest(for: S.self, configuration: configuration) + await runTest(for: PoundIfFalseElseIfTest.self, configuration: configuration) await fulfillment(of: [testStarted], timeout: 0.0) } - func testNoasyncTestsAreCallable() async throws { - @Suite(.hidden) struct S { - @Test(.hidden) - @available(*, noasync) - func noAsync() {} + @Suite(.hidden) struct NoasyncTestsAreCallableTests { + @Test(.hidden) + @available(*, noasync) + func noAsync() {} - @Test(.hidden) - @available(*, noasync) - func noAsyncThrows() throws {} + @Test(.hidden) + @available(*, noasync) + func noAsyncThrows() throws {} - @Test(.hidden) - @_unavailableFromAsync - func unavailableFromAsync() {} + @Test(.hidden) + @_unavailableFromAsync + func unavailableFromAsync() {} - @Test(.hidden) - @_unavailableFromAsync(message: "") - func unavailableFromAsyncWithMessage() {} + @Test(.hidden) + @_unavailableFromAsync(message: "") + func unavailableFromAsyncWithMessage() {} #if !SWT_NO_GLOBAL_ACTORS - @Test(.hidden) - @available(*, noasync) @MainActor - func noAsyncThrowsMainActor() throws {} + @Test(.hidden) + @available(*, noasync) @MainActor + func noAsyncThrowsMainActor() throws {} #endif - } + } + func testNoasyncTestsAreCallable() async throws { let testStarted = expectation(description: "Test started") #if !SWT_NO_GLOBAL_ACTORS testStarted.expectedFulfillmentCount = 6 @@ -556,7 +556,7 @@ final class RunnerTests: XCTestCase { testStarted.fulfill() } } - await runTest(for: S.self, configuration: configuration) + await runTest(for: NoasyncTestsAreCallableTests.self, configuration: configuration) await fulfillment(of: [testStarted], timeout: 0.0) } From 85ecedf219c655138c15f24c4da9a91d115c0c51 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 1 Apr 2024 16:40:42 -0400 Subject: [PATCH 2/2] Adopt recent swift-syntax-6.0 changes --- .../Support/DiagnosticMessage.swift | 5 ++ .../TestingMacros/TestDeclarationMacro.swift | 67 +++++++++++++------ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Sources/TestingMacros/Support/DiagnosticMessage.swift b/Sources/TestingMacros/Support/DiagnosticMessage.swift index b1747aa51..055063b98 100644 --- a/Sources/TestingMacros/Support/DiagnosticMessage.swift +++ b/Sources/TestingMacros/Support/DiagnosticMessage.swift @@ -138,8 +138,13 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { result = ("subscript", "a") case .enumCaseDecl: result = ("enumeration case", "an") +#if canImport(SwiftSyntax600) + case .typeAliasDecl: + result = ("typealias", "a") +#else case .typealiasDecl: result = ("typealias", "a") +#endif case .macroDecl: result = ("macro", "a") case .protocolDecl: diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index b031dcfb4..bb8147379 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -135,13 +135,22 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // We don't support inout, isolated, or _const parameters on test functions. for parameter in parameterList { - if let specifier = parameter.type.as(AttributedTypeSyntax.self)?.specifier { - switch specifier.tokenKind { - case .keyword(.inout), .keyword(.isolated), .keyword(._const): + let invalidSpecifierKeywords: [TokenKind] = [.keyword(.inout), .keyword(.isolated), .keyword(._const),] + if let parameterType = parameter.type.as(AttributedTypeSyntax.self) { +#if canImport(SwiftSyntax600) + for specifier in parameterType.specifiers { + guard case let .simpleTypeSpecifier(specifier) = specifier else { + continue + } + if invalidSpecifierKeywords.contains(specifier.specifier.tokenKind) { + diagnostics.append(.specifierNotSupported(specifier.specifier, on: parameter, whenUsing: testAttribute)) + } + } +#else + if let specifier = parameterType.specifier, invalidSpecifierKeywords.contains(specifier.tokenKind) { diagnostics.append(.specifierNotSupported(specifier, on: parameter, whenUsing: testAttribute)) - default: - break } +#endif } } @@ -244,21 +253,41 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { private static func _createCaptureListExpr( from parametersWithLabels: some Sequence<(DeclReferenceExprSyntax, FunctionParameterSyntax)> ) -> ClosureCaptureClauseSyntax { - ClosureCaptureClauseSyntax { - for (label, parameter) in parametersWithLabels { - if case let .keyword(specifierKeyword) = parameter.type.as(AttributedTypeSyntax.self)?.specifier?.tokenKind, - specifierKeyword == .borrowing || specifierKeyword == .consuming { - ClosureCaptureSyntax( - name: label.baseName, - equal: .equalToken(), - expression: CopyExprSyntax( - copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space), - expression: label - ) - ) - } else { - ClosureCaptureSyntax(expression: label) + let specifierKeywordsNeedingCopy: [TokenKind] = [.keyword(.borrowing), .keyword(.consuming),] + let closureCaptures = parametersWithLabels.lazy.map { label, parameter in + var needsCopy = false + if let parameterType = parameter.type.as(AttributedTypeSyntax.self) { +#if canImport(SwiftSyntax600) + needsCopy = parameterType.specifiers.contains { specifier in + guard case let .simpleTypeSpecifier(specifier) = specifier else { + return false + } + return specifierKeywordsNeedingCopy.contains(specifier.specifier.tokenKind) } +#else + if let specifier = parameterType.specifier { + needsCopy = specifierKeywordsNeedingCopy.contains(specifier.tokenKind) + } +#endif + } + + if needsCopy { + return ClosureCaptureSyntax( + name: label.baseName, + equal: .equalToken(), + expression: CopyExprSyntax( + copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space), + expression: label + ) + ) + } else { + return ClosureCaptureSyntax(expression: label) + } + } + + return ClosureCaptureClauseSyntax { + for closureCapture in closureCaptures { + closureCapture } } }