diff --git a/Release Notes/511.md b/Release Notes/511.md index 07741b8ba60..a4fd5fa665f 100644 --- a/Release Notes/511.md +++ b/Release Notes/511.md @@ -2,6 +2,11 @@ ## New APIs +- `assertMacroExpansion` now have new parameters named `applyFixIts` and `fixedSource` + - Description: `applyFixIts` and `fixedSource` are used to assert so ensure that the source code after applying Fix-Its matches this string. + - Issue: https://github.com/apple/swift-syntax/issues/2015 + - Pull Request: https://github.com/apple/swift-syntax/pull/2021 + ## API Behavior Changes ## Deprecations @@ -21,9 +26,11 @@ - Effect specifiers: - Description: The `unexpectedAfterThrowsSpecifier` node of the various effect specifiers has been removed. - Pull request: https://github.com/apple/swift-syntax/pull/2219 + - `SyntaxKind` removed conformance to `CaseIterable` - Description: `SyntaxKind` no longer conforms to `CaseIterable` since there is no good use case to iterate over all syntax kinds. - Pull request: https://github.com/apple/swift-syntax/pull/2292 + - `IntegerLiteralExprSyntax.Radix` removed conformance to `CaseIterable` - Description: `IntegerLiteralExprSyntax.Radix` no longer conforms to `CaseIterable` since there is no good use case to iterate over all radix kinds. - Pull request: https://github.com/apple/swift-syntax/pull/2292 diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 402450dcce7..dbde15df38f 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -254,13 +254,18 @@ func assertDiagnostic( /// - macros: The macros that should be expanded, provided as a dictionary /// mapping macro names (e.g., `"stringify"`) to implementation types /// (e.g., `StringifyMacro.self`). +/// - applyFixIts: If specified, filters the Fix-Its that are applied to generate `fixedSource` to only those whose message occurs in this array. If `nil`, all Fix-Its from the diagnostics are applied. +/// - fixedSource: If specified, asserts that the source code after applying Fix-Its matches this string. /// - testModuleName: The name of the test module to use. /// - testFileName: The name of the test file name to use. +/// - indentationWidth: The indentation width used in the expansion. public func assertMacroExpansion( _ originalSource: String, expandedSource expectedExpandedSource: String, diagnostics: [DiagnosticSpec] = [], macros: [String: Macro.Type], + applyFixIts: [String]? = nil, + fixedSource expectedFixedSource: String? = nil, testModuleName: String = "TestModule", testFileName: String = "test.swift", indentationWidth: Trivia = .spaces(4), @@ -317,4 +322,16 @@ public func assertMacroExpansion( assertDiagnostic(actualDiag, in: context, expected: expectedDiag) } } + + // Applying Fix-Its + if let expectedFixedSource = expectedFixedSource { + let fixedTree = FixItApplier.applyFixes(from: context.diagnostics, filterByMessages: applyFixIts, to: origSourceFile) + let fixedTreeDescription = fixedTree.description + assertStringsEqualWithDiff( + fixedTreeDescription.trimmingTrailingWhitespace(), + expectedFixedSource.trimmingTrailingWhitespace(), + file: file, + line: line + ) + } } diff --git a/Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift index b7f2e9647f5..c9d10683cc0 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift @@ -28,89 +28,117 @@ import XCTest final class PeerMacroTests: XCTestCase { private let indentationWidth: Trivia = .spaces(2) - func testAddCompletionHandler() { - struct AddCompletionHandler: PeerMacro { - static func expansion( - of node: AttributeSyntax, - providingPeersOf declaration: some DeclSyntaxProtocol, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - // Only on functions at the moment. We could handle initializers as well - // with a bit of work. - guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { - throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions") - } + fileprivate struct AddCompletionHandler: PeerMacro { + static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + // Only on functions at the moment. We could handle initializers as well + // with a bit of work. + guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { + throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions") + } - // This only makes sense for async functions. - if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil { - throw MacroExpansionErrorMessage( - "@addCompletionHandler requires an async function" - ) + // This only makes sense for async functions. + if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil { + let newEffects: FunctionEffectSpecifiersSyntax + if let existingEffects = funcDecl.signature.effectSpecifiers { + newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async)) + } else { + newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async)) } - // Form the completion handler parameter. - let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed - - let completionHandlerParam = - FunctionParameterSyntax( - firstName: .identifier("completionHandler"), - colon: .colonToken(trailingTrivia: .space), - type: TypeSyntax("(\(resultType ?? "")) -> Void") - ) - - // Add the completion handler parameter to the parameter list. - let parameterList = funcDecl.signature.parameterClause.parameters - var newParameterList = parameterList - if !parameterList.isEmpty { - // We need to add a trailing comma to the preceding list. - newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space) - } - newParameterList.append(completionHandlerParam) + let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects) + + let diag = Diagnostic( + node: Syntax(funcDecl.funcKeyword), + message: MacroExpansionErrorMessage( + "can only add a completion-handler variant to an 'async' function" + ), + fixIts: [ + FixIt( + message: MacroExpansionFixItMessage( + "add 'async'" + ), + changes: [ + FixIt.Change.replace( + oldNode: Syntax(funcDecl.signature), + newNode: Syntax(newSignature) + ) + ] + ) + ] + ) + + context.diagnose(diag) + return [] + } - let callArguments: [String] = parameterList.map { param in - let argName = param.secondName ?? param.firstName + // Form the completion handler parameter. + let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed + + let completionHandlerParam = + FunctionParameterSyntax( + firstName: .identifier("completionHandler"), + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax("(\(resultType ?? "")) -> Void") + ) + + // Add the completion handler parameter to the parameter list. + let parameterList = funcDecl.signature.parameterClause.parameters + var newParameterList = parameterList + if !parameterList.isEmpty { + // We need to add a trailing comma to the preceding list. + newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space) + } + newParameterList.append(completionHandlerParam) - if param.firstName.text != "_" { - return "\(param.firstName.text): \(argName.text)" - } + let callArguments: [String] = parameterList.map { param in + let argName = param.secondName ?? param.firstName - return "\(argName.text)" + if param.firstName.text != "_" { + return "\(param.firstName.text): \(argName.text)" } - let call: ExprSyntax = - "\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))" - - // FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation, - // so that the full body could go here. - let newBody: ExprSyntax = - """ + return "\(argName.text)" + } - Task { - completionHandler(await \(call)) - } + let call: ExprSyntax = + "\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))" - """ + // FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation, + // so that the full body could go here. + let newBody: ExprSyntax = + """ - // Drop the @addCompletionHandler attribute from the new declaration. - let newAttributeList = funcDecl.attributes.filter { - guard case let .attribute(attribute) = $0 else { - return true + Task { + completionHandler(await \(call)) } - return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler" - } - var newFunc = funcDecl - newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async - newFunc.signature.returnClause = nil // drop result type - newFunc.signature.parameterClause.parameters = newParameterList - newFunc.signature.parameterClause.trailingTrivia = [] - newFunc.body = CodeBlockSyntax { newBody } - newFunc.attributes = newAttributeList + """ - return [DeclSyntax(newFunc)] + // Drop the @addCompletionHandler attribute from the new declaration. + let newAttributeList = funcDecl.attributes.filter { + guard case let .attribute(attribute) = $0 else { + return true + } + return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler" } + + var newFunc = funcDecl + newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async + newFunc.signature.returnClause = nil // drop result type + newFunc.signature.parameterClause.parameters = newParameterList + newFunc.signature.parameterClause.trailingTrivia = [] + newFunc.body = CodeBlockSyntax { newBody } + newFunc.attributes = newAttributeList + + return [DeclSyntax(newFunc)] } + } + func testAddCompletionHandler() { assertMacroExpansion( """ @addCompletionHandler @@ -193,4 +221,30 @@ final class PeerMacroTests: XCTestCase { ] ) } + + func testAddCompletionHandlerWhereThereIsNotAsync() { + assertMacroExpansion( + """ + @addCompletionHandler + func f(a: Int, for b: String, _ value: Double) -> String { } + """, + expandedSource: """ + func f(a: Int, for b: String, _ value: Double) -> String { } + """, + diagnostics: [ + DiagnosticSpec( + message: "can only add a completion-handler variant to an 'async' function", + line: 2, + column: 1, + fixIts: [FixItSpec(message: "add 'async'")] + ) + ], + macros: ["addCompletionHandler": AddCompletionHandler.self], + fixedSource: """ + @addCompletionHandler + func f(a: Int, for b: String, _ value: Double) async-> String { } + """, + indentationWidth: indentationWidth + ) + } }