Repository: apple/swift-syntax (swift)

Issue:
Add a `fixedSource` parameter to `assertMacroExpansion`
The `assertParse` function has a `fixedSource` parameter to check that the application of Fix-Its produces valid results and it has been a valuable tool so far. `assertMacroExpansion` could gain the same parameter to check that the macro’s Fix-Its work as expected.

Pull request:
Resolves #2015

Patch:
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


Test patch:
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
+    )
+  }
 }