Skip to content

Commit 8a9445f

Browse files
authored
Merge pull request #2767 from AppAppWorks/detect-circular-expansion
Detect circular macro expansion
2 parents 2256eaa + 6edc2e1 commit 8a9445f

File tree

3 files changed

+141
-12
lines changed

3 files changed

+141
-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 recursiveExpansion(any Macro.Type)
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 .recursiveExpansion(let type):
98+
return "recursive expansion of macro '\(type)'"
9599
}
96100
}
97101
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

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

671+
/// Stores the types of the freestanding macros that are currently expanding.
672+
///
673+
/// As macros are expanded by DFS, `expandingFreestandingMacros` always represent the expansion path starting from
674+
/// the root macro node to the last macro node currently expanding.
675+
var expandingFreestandingMacros: [any Macro.Type] = []
676+
671677
init(
672678
macroSystem: MacroSystem,
673679
contextGenerator: @escaping (Syntax) -> Context,
@@ -684,7 +690,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
684690
}
685691

686692
override func visitAny(_ node: Syntax) -> Syntax? {
687-
if skipVisitAnyHandling.contains(node) {
693+
guard !skipVisitAnyHandling.contains(node) else {
688694
return nil
689695
}
690696

@@ -693,8 +699,10 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
693699
// position are handled by 'visit(_:CodeBlockItemListSyntax)'.
694700
// Only expression expansions inside other syntax nodes is handled here.
695701
switch expandExpr(node: node) {
696-
case .success(let expanded):
697-
return Syntax(visit(expanded))
702+
case .success(let expansion):
703+
return expansion.withExpandedNode { expandedNode in
704+
Syntax(visit(expandedNode))
705+
}
698706
case .failure:
699707
return Syntax(node)
700708
case .notAMacro:
@@ -795,9 +803,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
795803
func addResult(_ node: CodeBlockItemSyntax) {
796804
// Expand freestanding macro.
797805
switch expandCodeBlockItem(node: node) {
798-
case .success(let expanded):
799-
for item in expanded {
800-
addResult(item)
806+
case .success(let expansion):
807+
expansion.withExpandedNode { expandedNode in
808+
for item in expandedNode {
809+
addResult(item)
810+
}
801811
}
802812
return
803813
case .failure:
@@ -840,9 +850,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
840850
func addResult(_ node: MemberBlockItemSyntax) {
841851
// Expand freestanding macro.
842852
switch expandMemberDecl(node: node) {
843-
case .success(let expanded):
844-
for item in expanded {
845-
addResult(item)
853+
case .success(let expansion):
854+
expansion.withExpandedNode { expandedNode in
855+
for item in expandedNode {
856+
addResult(item)
857+
}
846858
}
847859
return
848860
case .failure:
@@ -1218,9 +1230,36 @@ extension MacroApplication {
12181230
// MARK: Freestanding macro expansion
12191231

12201232
extension MacroApplication {
1233+
/// Encapsulates an expanded node, the type of the macro from which the node was expanded, and the macro application,
1234+
/// such that recursive macro expansion can be consistently detected.
1235+
struct MacroExpansion<ResultType> {
1236+
private let expandedNode: ResultType
1237+
private let macro: any Macro.Type
1238+
private unowned let macroApplication: MacroApplication
1239+
1240+
fileprivate init(expandedNode: ResultType, macro: any Macro.Type, macroApplication: MacroApplication) {
1241+
self.expandedNode = expandedNode
1242+
self.macro = macro
1243+
self.macroApplication = macroApplication
1244+
}
1245+
1246+
/// Invokes the given closure with the node resulting from a macro expansion.
1247+
///
1248+
/// This method inserts a pair of push and pop operations immediately around the invocation of `body` to maintain
1249+
/// an exact stack of expanding freestanding macros to detect recursive macro expansion. Callers should perform any
1250+
/// further macro expansion on `expanded` only within the scope of `body`.
1251+
func withExpandedNode<T>(_ body: (_ expandedNode: ResultType) throws -> T) rethrows -> T {
1252+
macroApplication.expandingFreestandingMacros.append(macro)
1253+
defer {
1254+
macroApplication.expandingFreestandingMacros.removeLast()
1255+
}
1256+
return try body(expandedNode)
1257+
}
1258+
}
1259+
12211260
enum MacroExpansionResult<ResultType> {
12221261
/// Expansion of the macro succeeded.
1223-
case success(ResultType)
1262+
case success(expansion: MacroExpansion<ResultType>)
12241263

12251264
/// Macro system found the macro to expand but running the expansion threw
12261265
/// an error and thus no expansion result exists.
@@ -1230,18 +1269,37 @@ extension MacroApplication {
12301269
case notAMacro
12311270
}
12321271

1272+
/// Expands the given freestanding macro node into a syntax node by invoking the given closure.
1273+
///
1274+
/// Any error thrown by `expandMacro` and circular expansion error will be added to diagnostics.
1275+
///
1276+
/// - Parameters:
1277+
/// - node: The freestanding macro node to be expanded.
1278+
/// - expandMacro: The closure that expands the given macro type and macro node into a syntax node.
1279+
///
1280+
/// - Returns:
1281+
/// Returns `.notAMacro` if `node` is `nil` or `node.macroName` isn't registered with any macro type.
1282+
/// Returns `.failure` if `expandMacro` throws an error or returns `nil`, or recursive expansion is detected.
1283+
/// Returns `.success` otherwise.
12331284
private func expandFreestandingMacro<ExpandedMacroType: SyntaxProtocol>(
12341285
_ node: (any FreestandingMacroExpansionSyntax)?,
1235-
expandMacro: (_ macro: Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
1286+
expandMacro: (_ macro: any Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
12361287
) -> MacroExpansionResult<ExpandedMacroType> {
12371288
guard let node,
12381289
let macro = macroSystem.lookup(node.macroName.text)?.type
12391290
else {
12401291
return .notAMacro
12411292
}
1293+
12421294
do {
1295+
guard !expandingFreestandingMacros.contains(where: { $0 == macro }) else {
1296+
// We may think of any ongoing macro expansion as a tree in which macro types being expanded are nodes.
1297+
// Any macro type being expanded more than once will create a cycle which the compiler as of now doesn't allow.
1298+
throw MacroExpansionError.recursiveExpansion(macro)
1299+
}
1300+
12431301
if let expanded = try expandMacro(macro, node) {
1244-
return .success(expanded)
1302+
return .success(expansion: MacroExpansion(expandedNode: expanded, macro: macro, macroApplication: self))
12451303
} else {
12461304
return .failure
12471305
}

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+
"recursive expansion of macro 'InfiniteRecursionMacro'",
344+
line: 1,
345+
column: 5
346+
),
347+
DiagnosticSpec(
348+
message:
349+
"recursive expansion of macro '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)