Skip to content

Commit a834159

Browse files
committed
[Macros] Implement closure body macros in MacroSystem and add a test
for expanding a body macro on a closure expr.
1 parent b055993 commit a834159

File tree

2 files changed

+120
-8
lines changed

2 files changed

+120
-8
lines changed

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ private func expandPreambleMacro(
444444
private func expandBodyMacro(
445445
definition: BodyMacro.Type,
446446
attributeNode: AttributeSyntax,
447-
attachedTo decl: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
447+
attachedTo node: some SyntaxProtocol,
448448
in context: some MacroExpansionContext,
449449
indentationWidth: Trivia
450450
) -> CodeBlockSyntax? {
@@ -456,7 +456,7 @@ private func expandBodyMacro(
456456
in: context,
457457
foldingWith: .standardOperators
458458
),
459-
declarationNode: DeclSyntax(decl.detach(in: context)),
459+
declarationNode: Syntax(node.detach(in: context)),
460460
parentDeclNode: nil,
461461
extendedType: nil,
462462
conformanceList: nil,
@@ -473,9 +473,16 @@ private func expandBodyMacro(
473473
// Remove any indentation from the first line using `drop(while:)` and then
474474
// prepend a space when it's being introduced on a declaration that has no
475475
// body yet.
476-
let leadingWhitespace = decl.body == nil ? " " : ""
476+
let leadingWhitespace: String
477+
if let decl = node as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax),
478+
decl.body == nil
479+
{
480+
leadingWhitespace = " "
481+
} else {
482+
leadingWhitespace = ""
483+
}
477484
let indentedSource =
478-
leadingWhitespace + expanded.indented(by: decl.indentationOfFirstLine).drop(while: { $0.isWhitespace })
485+
leadingWhitespace + expanded.indented(by: node.indentationOfFirstLine).drop(while: { $0.isWhitespace })
479486
return "\(raw: indentedSource)"
480487
}
481488

@@ -733,6 +740,19 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
733740
return AttributeRemover(removingWhere: { attributesToRemove.contains($0) }).rewrite(visitedNode)
734741
}
735742

743+
if var closureSyntax = node.as(ClosureExprSyntax.self) {
744+
closureSyntax = visitClosureBodyMacros(closureSyntax)
745+
746+
// Visit the node, disabling the `visitAny` handling.
747+
skipVisitAnyHandling.insert(Syntax(closureSyntax))
748+
let visitedNode = self.visit(closureSyntax).cast(ClosureExprSyntax.self)
749+
skipVisitAnyHandling.remove(Syntax(closureSyntax))
750+
751+
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode).map(\.attributeNode)
752+
753+
return AttributeRemover(removingWhere: { attributesToRemove.contains($0) }).rewrite(visitedNode)
754+
}
755+
736756
return nil
737757
}
738758

@@ -802,6 +822,41 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
802822
return node.with(\.body, body.with(\.statements, preamble + body.statements))
803823
}
804824

825+
func visitClosureBodyMacros(
826+
_ node: ClosureExprSyntax
827+
) -> ClosureExprSyntax {
828+
// Expand body macro.
829+
let expandedBodies = expandMacros(
830+
attachedTo: node,
831+
ofType: BodyMacro.Type.self
832+
) { attributeNode, definition, _ in
833+
expandBodyMacro(
834+
definition: definition,
835+
attributeNode: attributeNode,
836+
attachedTo: node,
837+
in: contextGenerator(Syntax(node)),
838+
indentationWidth: indentationWidth
839+
).map { [$0] }
840+
}
841+
842+
// Dig out the body.
843+
let body: CodeBlockSyntax
844+
switch expandedBodies.count {
845+
case 0:
846+
// Nothing changes
847+
return node
848+
849+
case 1:
850+
body = expandedBodies[0]
851+
852+
default:
853+
contextGenerator(Syntax(node)).addDiagnostics(from: MacroExpansionError.moreThanOneBodyMacro, node: node)
854+
body = expandedBodies[0]
855+
}
856+
857+
return node.with(\.statements, body.statements)
858+
}
859+
805860
override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
806861
var newItems: [CodeBlockItemSyntax] = []
807862
func addResult(_ node: CodeBlockItemSyntax) {
@@ -1006,9 +1061,16 @@ extension MacroApplication {
10061061
///
10071062
/// The macros must be registered in `macroSystem`.
10081063
private func macroAttributes(
1009-
attachedTo decl: DeclSyntax
1064+
attachedTo decl: some SyntaxProtocol
10101065
) -> [(attributeNode: AttributeSyntax, spec: MacroSpec)] {
1011-
guard let attributedNode = decl.asProtocol(WithAttributesSyntax.self) else {
1066+
let attributedNode: (any WithAttributesSyntax)?
1067+
if let closure = decl.as(ClosureExprSyntax.self) {
1068+
attributedNode = closure.signature?.asProtocol(WithAttributesSyntax.self)
1069+
} else {
1070+
attributedNode = decl.asProtocol(WithAttributesSyntax.self)
1071+
}
1072+
1073+
guard let attributedNode else {
10121074
return []
10131075
}
10141076

@@ -1029,7 +1091,7 @@ extension MacroApplication {
10291091
///
10301092
/// The macros must be registered in `macroSystem`.
10311093
private func macroAttributes<MacroType>(
1032-
attachedTo decl: DeclSyntax,
1094+
attachedTo decl: some SyntaxProtocol,
10331095
ofType: MacroType.Type
10341096
) -> [(attributeNode: AttributeSyntax, definition: MacroType, conformanceList: InheritedTypeListSyntax)] {
10351097
return macroAttributes(attachedTo: decl)
@@ -1049,7 +1111,7 @@ extension MacroApplication {
10491111
ExpandedNodeCollection: Sequence<ExpandedNode>,
10501112
MacroType
10511113
>(
1052-
attachedTo decl: DeclSyntax,
1114+
attachedTo decl: some SyntaxProtocol,
10531115
ofType: MacroType.Type,
10541116
expandMacro:
10551117
(

Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ struct RemoteBodyMacro: BodyMacro {
6363
}
6464
}
6565

66+
struct StartTaskMacro: BodyMacro {
67+
static func expansion(
68+
of node: AttributeSyntax,
69+
providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
70+
in context: some MacroExpansionContext
71+
) throws -> [CodeBlockItemSyntax] {
72+
guard let taskBody = declaration.body else {
73+
return []
74+
}
75+
return [
76+
"""
77+
Task \(taskBody.trimmed)
78+
"""
79+
]
80+
}
81+
82+
static func expansion(
83+
of node: AttributeSyntax,
84+
providingBodyFor closure: ClosureExprSyntax,
85+
in context: some MacroExpansionContext
86+
) throws -> [CodeBlockItemSyntax] {
87+
return [
88+
"""
89+
Task {
90+
\(closure.statements.trimmed)
91+
}
92+
"""
93+
]
94+
}
95+
}
96+
6697
final class BodyMacroTests: XCTestCase {
6798
private let indentationWidth: Trivia = .spaces(2)
6899

@@ -199,4 +230,23 @@ final class BodyMacroTests: XCTestCase {
199230
macros: ["SourceLocationMacro": SourceLocationMacro.self]
200231
)
201232
}
233+
234+
func testClosureBodyExpansion() {
235+
assertMacroExpansion(
236+
"""
237+
{ @StartTask in
238+
a + b
239+
}
240+
""",
241+
expandedSource: """
242+
{ in
243+
Task {
244+
a + b
245+
}
246+
}
247+
""",
248+
macros: ["StartTask": StartTaskMacro.self],
249+
indentationWidth: indentationWidth
250+
)
251+
}
202252
}

0 commit comments

Comments
 (0)