Repository: apple/swift-syntax (swift)

Issue:
SyntaxProtocol.expand() does not expand conformance macros
### Description

Conformance macros used in the input source are discarded without expansion. A basic example:

```swift
struct Conformance: ConformanceMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingConformancesOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
        fatalError()
    }
}
assertMacroExpansion("@Foo class Bar {}", expandedSource: "class Bar {\n}",
                     macros: ["Foo": Conformance.self])
```

This test passes despite the implementation being `fatalError()` due to `expansion(of:performanceConformanceOf:in:)` never being called. There appears to be two parallel implementations of macro expansion with ConformanceMacro only being supported in expandAttachedMacro(), which is not called by SyntaxProtocol.expand().

There may be other macro types not implemented as well; I did not try them all.

### Steps to Reproduce

_No response_

Pull request description:
Somehow, we forgot to implement this here, even though the compiler has supported it for a long time.

Fixes #1742 / rdar://110418242.

Patch:
diff --git a/Sources/SwiftSyntaxMacros/MacroSystem.swift b/Sources/SwiftSyntaxMacros/MacroSystem.swift
index 3b72bdd2968..032b527b341 100644
--- a/Sources/SwiftSyntaxMacros/MacroSystem.swift
+++ b/Sources/SwiftSyntaxMacros/MacroSystem.swift
@@ -178,7 +178,8 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
       let newItem = visit(item.item)
       newItems.append(item.with(\.item, newItem))
 
-      // Expand any peer declarations triggered by macros used as attributes.
+      // Expand any peer declarations or conformances triggered by macros used
+      // as attributes.
       if case let .decl(decl) = item.item {
         let peers = expandPeers(of: decl)
         newItems.append(
@@ -186,6 +187,14 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
             newDecl in CodeBlockItemSyntax(item: .decl(newDecl))
           }
         )
+
+        if let declGroup = decl.asProtocol(DeclGroupSyntax.self) {
+          newItems.append(
+            contentsOf: expandConformances(of: declGroup).map {
+              newDecl in CodeBlockItemSyntax(item: .decl(newDecl))
+            }
+          )
+        }
       }
     }
 
@@ -394,6 +403,43 @@ extension MacroApplication {
     return peers
   }
 
+  // If any of the custom attributes associated with the given declaration
+  // refer to conformance macros, expand them and return the resulting
+  // set of extension declarations.
+  private func expandConformances(of decl: DeclGroupSyntax) -> [DeclSyntax] {
+    let extendedType: Syntax
+    if let identified = decl.asProtocol(IdentifiedDeclSyntax.self) {
+      extendedType = Syntax(identified.identifier.trimmed)
+    } else if let ext = decl.as(ExtensionDeclSyntax.self) {
+      extendedType = Syntax(ext.extendedType.trimmed)
+    } else {
+      return []
+    }
+
+    var extensions: [DeclSyntax] = []
+    let macroAttributes = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ConformanceMacro.Type.self)
+    for (attribute, conformanceMacro) in macroAttributes {
+      do {
+        let newConformances = try conformanceMacro.expansion(of: attribute, providingConformancesOf: decl, in: context)
+
+        for (type, whereClause) in newConformances {
+          var ext: DeclSyntax = """
+            extension \(extendedType): \(type) { }
+            """
+          if let whereClause {
+            ext = DeclSyntax((ext.cast(ExtensionDeclSyntax.self)).with(\.genericWhereClause, whereClause))
+          }
+
+          extensions.append(DeclSyntax(ext))
+        }
+      } catch {
+        context.addDiagnostics(from: error, node: attribute)
+      }
+    }
+
+    return extensions
+  }
+
   /// Expands any attached custom attributes that refer to member declaration macros,
   /// and returns result of adding those members to the given declaration.
   private func expandMembers<Decl: DeclGroupSyntax & DeclSyntaxProtocol>(


Test patch:
diff --git a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift
index 31f0758c544..24b6d5bc117 100644
--- a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift
+++ b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift
@@ -672,6 +672,16 @@ public struct DeclsFromStringsMacro: DeclarationMacro {
   }
 }
 
+public struct SendableConformanceMacro: ConformanceMacro {
+  public static func expansion(
+    of node: AttributeSyntax,
+    providingConformancesOf declaration: some DeclGroupSyntax,
+    in context: some MacroExpansionContext
+  ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
+    return [("Sendable", nil)]
+  }
+}
+
 public struct DeclsFromStringsMacroNoAttrs: DeclarationMacro {
   public static var propagateFreestandingMacroAttributes: Bool { false }
   public static var propagateFreestandingMacroModifiers: Bool { false }
@@ -715,6 +725,7 @@ public let testMacros: [String: Macro.Type] = [
   "wrapStoredProperties": WrapStoredProperties.self,
   "customTypeWrapper": CustomTypeWrapperMacro.self,
   "unwrap": UnwrapMacro.self,
+  "AddSendable": SendableConformanceMacro.self,
 ]
 
 final class MacroSystemTests: XCTestCase {
@@ -1156,4 +1167,40 @@ final class MacroSystemTests: XCTestCase {
     )
 
   }
+
+  func testConformanceExpansion() {
+    assertMacroExpansion(
+      """
+      @AddSendable
+      struct MyType {
+      }
+      """,
+      expandedSource: """
+
+        struct MyType {
+        }
+        extension MyType: Sendable {
+        }
+        """,
+      macros: testMacros,
+      indentationWidth: indentationWidth
+    )
+
+    assertMacroExpansion(
+      """
+      @AddSendable
+      extension A.B {
+      }
+      """,
+      expandedSource: """
+
+        extension A.B {
+        }
+        extension A.B: Sendable {
+        }
+        """,
+      macros: testMacros,
+      indentationWidth: indentationWidth
+    )
+  }
 }