Skip to content

Commit 0489d74

Browse files
committed
detect circular macro expansion
- `MacroApplication` now detects any freestanding macro that appears on an expansion path more than once and throws `MacroExpansionError.circularExpansion` - added a test case in `ExpressionMacroTests`
1 parent 24a2501 commit 0489d74

File tree

3 files changed

+127
-12
lines changed

3 files changed

+127
-12
lines changed

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ enum MacroExpansionError: Error, CustomStringConvertible {
6363
case noFreestandingMacroRoles(Macro.Type)
6464
case moreThanOneBodyMacro
6565
case preambleWithoutBody
66+
case circularExpansion(Macro.Type, any FreestandingMacroExpansionSyntax)
6667

6768
var description: String {
6869
switch self {
@@ -92,6 +93,9 @@ enum MacroExpansionError: Error, CustomStringConvertible {
9293

9394
case .preambleWithoutBody:
9495
return "preamble macro cannot be applied to a function with no body"
96+
97+
case .circularExpansion(let type, let syntax):
98+
return "circular expansion detected: '\(syntax)' with macro implementation type '\(type)'"
9599
}
96100
}
97101
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,9 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
667667
/// added to top-level 'CodeBlockItemList'.
668668
var extensions: [CodeBlockItemSyntax] = []
669669

670+
/// Stores the types of the freestanding macros that are currently expanding.
671+
var expandingFreestandingMacros: [any Macro.Type] = []
672+
670673
init(
671674
macroSystem: MacroSystem,
672675
contextGenerator: @escaping (Syntax) -> Context,
@@ -683,7 +686,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
683686
}
684687

685688
override func visitAny(_ node: Syntax) -> Syntax? {
686-
if skipVisitAnyHandling.contains(node) {
689+
guard !skipVisitAnyHandling.contains(node) else {
687690
return nil
688691
}
689692

@@ -692,8 +695,10 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
692695
// position are handled by 'visit(_:CodeBlockItemListSyntax)'.
693696
// Only expression expansions inside other syntax nodes is handled here.
694697
switch expandExpr(node: node) {
695-
case .success(let expanded):
696-
return Syntax(visit(expanded))
698+
case .success(let expansion):
699+
return expansion.consume { expanded in
700+
Syntax(visit(expanded))
701+
}
697702
case .failure:
698703
return Syntax(node)
699704
case .notAMacro:
@@ -794,9 +799,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
794799
func addResult(_ node: CodeBlockItemSyntax) {
795800
// Expand freestanding macro.
796801
switch expandCodeBlockItem(node: node) {
797-
case .success(let expanded):
798-
for item in expanded {
799-
addResult(item)
802+
case .success(let expansion):
803+
expansion.consume { expanded in
804+
for item in expanded {
805+
addResult(item)
806+
}
800807
}
801808
return
802809
case .failure:
@@ -839,9 +846,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
839846
func addResult(_ node: MemberBlockItemSyntax) {
840847
// Expand freestanding macro.
841848
switch expandMemberDecl(node: node) {
842-
case .success(let expanded):
843-
for item in expanded {
844-
addResult(item)
849+
case .success(let expansion):
850+
expansion.consume { expanded in
851+
for item in expanded {
852+
addResult(item)
853+
}
845854
}
846855
return
847856
case .failure:
@@ -1205,9 +1214,39 @@ extension MacroApplication {
12051214
// MARK: Freestanding macro expansion
12061215

12071216
extension MacroApplication {
1217+
class MacroExpansion<ResultType> {
1218+
private let expandedNode: ResultType
1219+
private let macro: any Macro.Type
1220+
private unowned let macroApplication: MacroApplication
1221+
private var isConsumed = false
1222+
1223+
fileprivate init(expandedNode: ResultType, macro: any Macro.Type, macroApplication: MacroApplication) {
1224+
self.expandedNode = expandedNode
1225+
self.macro = macro
1226+
self.macroApplication = macroApplication
1227+
}
1228+
1229+
/// Invokes the given closure with the node resulting from a macro expansion.
1230+
///
1231+
/// - Precondition: This method has never been called for this instance.
1232+
///
1233+
/// This method inserts a pair of push and pop operations immediately around the closure invocation to maintain an
1234+
/// exact stack of expanding freestanding macros to detect circluar macro expansion. Callers should only call this
1235+
/// method once and perform all further expansions on `expanded` only within the scope of `body`.
1236+
func consume<T>(_ body: (_ expanded: ResultType) throws -> T) rethrows -> T {
1237+
precondition(!isConsumed)
1238+
isConsumed = true
1239+
macroApplication.expandingFreestandingMacros.append(macro)
1240+
defer {
1241+
macroApplication.expandingFreestandingMacros.removeLast()
1242+
}
1243+
return try body(expandedNode)
1244+
}
1245+
}
1246+
12081247
enum MacroExpansionResult<ResultType> {
12091248
/// Expansion of the macro succeeded.
1210-
case success(ResultType)
1249+
case success(expansion: MacroExpansion<ResultType>)
12111250

12121251
/// Macro system found the macro to expand but running the expansion threw
12131252
/// an error and thus no expansion result exists.
@@ -1219,16 +1258,21 @@ extension MacroApplication {
12191258

12201259
private func expandFreestandingMacro<ExpandedMacroType: SyntaxProtocol>(
12211260
_ node: (any FreestandingMacroExpansionSyntax)?,
1222-
expandMacro: (_ macro: Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
1261+
expandMacro: (_ macro: any Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
12231262
) -> MacroExpansionResult<ExpandedMacroType> {
12241263
guard let node,
12251264
let macro = macroSystem.lookup(node.macroName.text)?.type
12261265
else {
12271266
return .notAMacro
12281267
}
1268+
12291269
do {
1270+
guard expandingFreestandingMacros.allSatisfy({ $0 != macro }) else {
1271+
throw MacroExpansionError.circularExpansion(macro, node)
1272+
}
1273+
12301274
if let expanded = try expandMacro(macro, node) {
1231-
return .success(expanded)
1275+
return .success(expansion: MacroExpansion(expandedNode: expanded, macro: macro, macroApplication: self))
12321276
} else {
12331277
return .failure
12341278
}

Tests/SwiftSyntaxMacroExpansionTest/ExpressionMacroTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,46 @@ fileprivate struct StringifyMacro: ExpressionMacro {
3737
}
3838
}
3939

40+
private struct InfiniteRecursionMacro: ExpressionMacro {
41+
static func expansion(
42+
of node: some FreestandingMacroExpansionSyntax,
43+
in context: some MacroExpansionContext
44+
) throws -> ExprSyntax {
45+
if let i = node.arguments.first?.expression.as(IntegerLiteralExprSyntax.self)?.representedLiteralValue {
46+
return "\(raw: i) + #infiniteRecursion(i: \(raw: i + 1))"
47+
} else {
48+
return "#nested1"
49+
}
50+
}
51+
}
52+
53+
private struct Nested1RecursionMacro: ExpressionMacro {
54+
static func expansion(
55+
of node: some FreestandingMacroExpansionSyntax,
56+
in context: some MacroExpansionContext
57+
) throws -> ExprSyntax {
58+
"(#nested2, #nested3, #infiniteRecursion(i: 1), #infiniteRecursion)"
59+
}
60+
}
61+
62+
private struct Nested2RecursionMacro: ExpressionMacro {
63+
static func expansion(
64+
of node: some FreestandingMacroExpansionSyntax,
65+
in context: some MacroExpansionContext
66+
) throws -> ExprSyntax {
67+
"(#nested3, #nested3)"
68+
}
69+
}
70+
71+
private struct Nested3RecursionMacro: ExpressionMacro {
72+
static func expansion(
73+
of node: some FreestandingMacroExpansionSyntax,
74+
in context: some MacroExpansionContext
75+
) throws -> ExprSyntax {
76+
"0"
77+
}
78+
}
79+
4080
final class ExpressionMacroTests: XCTestCase {
4181
private let indentationWidth: Trivia = .spaces(2)
4282

@@ -292,4 +332,31 @@ final class ExpressionMacroTests: XCTestCase {
292332
macros: ["test": DiagnoseFirstArgument.self]
293333
)
294334
}
335+
336+
func testDetectCircularExpansion() {
337+
assertMacroExpansion(
338+
"#nested1",
339+
expandedSource: "((0, 0), 0, 1 + #infiniteRecursion(i: 2), #nested1)",
340+
diagnostics: [
341+
DiagnosticSpec(
342+
message:
343+
"circular expansion detected: '#infiniteRecursion(i: 2)' with macro implementation type 'InfiniteRecursionMacro'",
344+
line: 1,
345+
column: 5
346+
),
347+
DiagnosticSpec(
348+
message:
349+
"circular expansion detected: '#nested1' with macro implementation type 'Nested1RecursionMacro'",
350+
line: 1,
351+
column: 1
352+
),
353+
],
354+
macros: [
355+
"nested1": Nested1RecursionMacro.self,
356+
"nested2": Nested2RecursionMacro.self,
357+
"nested3": Nested3RecursionMacro.self,
358+
"infiniteRecursion": InfiniteRecursionMacro.self,
359+
]
360+
)
361+
}
295362
}

0 commit comments

Comments
 (0)