Skip to content

Commit 6edc2e1

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.recursiveExpansion` - added a test case in `ExpressionMacroTests` rdar://113567654 Fixes #2018
1 parent 24a2501 commit 6edc2e1

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
@@ -667,6 +667,12 @@ 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+
///
672+
/// As macros are expanded by DFS, `expandingFreestandingMacros` always represent the expansion path starting from
673+
/// the root macro node to the last macro node currently expanding.
674+
var expandingFreestandingMacros: [any Macro.Type] = []
675+
670676
init(
671677
macroSystem: MacroSystem,
672678
contextGenerator: @escaping (Syntax) -> Context,
@@ -683,7 +689,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
683689
}
684690

685691
override func visitAny(_ node: Syntax) -> Syntax? {
686-
if skipVisitAnyHandling.contains(node) {
692+
guard !skipVisitAnyHandling.contains(node) else {
687693
return nil
688694
}
689695

@@ -692,8 +698,10 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
692698
// position are handled by 'visit(_:CodeBlockItemListSyntax)'.
693699
// Only expression expansions inside other syntax nodes is handled here.
694700
switch expandExpr(node: node) {
695-
case .success(let expanded):
696-
return Syntax(visit(expanded))
701+
case .success(let expansion):
702+
return expansion.withExpandedNode { expandedNode in
703+
Syntax(visit(expandedNode))
704+
}
697705
case .failure:
698706
return Syntax(node)
699707
case .notAMacro:
@@ -794,9 +802,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
794802
func addResult(_ node: CodeBlockItemSyntax) {
795803
// Expand freestanding macro.
796804
switch expandCodeBlockItem(node: node) {
797-
case .success(let expanded):
798-
for item in expanded {
799-
addResult(item)
805+
case .success(let expansion):
806+
expansion.withExpandedNode { expandedNode in
807+
for item in expandedNode {
808+
addResult(item)
809+
}
800810
}
801811
return
802812
case .failure:
@@ -839,9 +849,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
839849
func addResult(_ node: MemberBlockItemSyntax) {
840850
// Expand freestanding macro.
841851
switch expandMemberDecl(node: node) {
842-
case .success(let expanded):
843-
for item in expanded {
844-
addResult(item)
852+
case .success(let expansion):
853+
expansion.withExpandedNode { expandedNode in
854+
for item in expandedNode {
855+
addResult(item)
856+
}
845857
}
846858
return
847859
case .failure:
@@ -1205,9 +1217,36 @@ extension MacroApplication {
12051217
// MARK: Freestanding macro expansion
12061218

12071219
extension MacroApplication {
1220+
/// Encapsulates an expanded node, the type of the macro from which the node was expanded, and the macro application,
1221+
/// such that recursive macro expansion can be consistently detected.
1222+
struct MacroExpansion<ResultType> {
1223+
private let expandedNode: ResultType
1224+
private let macro: any Macro.Type
1225+
private unowned let macroApplication: MacroApplication
1226+
1227+
fileprivate init(expandedNode: ResultType, macro: any Macro.Type, macroApplication: MacroApplication) {
1228+
self.expandedNode = expandedNode
1229+
self.macro = macro
1230+
self.macroApplication = macroApplication
1231+
}
1232+
1233+
/// Invokes the given closure with the node resulting from a macro expansion.
1234+
///
1235+
/// This method inserts a pair of push and pop operations immediately around the invocation of `body` to maintain
1236+
/// an exact stack of expanding freestanding macros to detect recursive macro expansion. Callers should perform any
1237+
/// further macro expansion on `expanded` only within the scope of `body`.
1238+
func withExpandedNode<T>(_ body: (_ expandedNode: ResultType) throws -> T) rethrows -> T {
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.
@@ -1217,18 +1256,37 @@ extension MacroApplication {
12171256
case notAMacro
12181257
}
12191258

1259+
/// Expands the given freestanding macro node into a syntax node by invoking the given closure.
1260+
///
1261+
/// Any error thrown by `expandMacro` and circular expansion error will be added to diagnostics.
1262+
///
1263+
/// - Parameters:
1264+
/// - node: The freestanding macro node to be expanded.
1265+
/// - expandMacro: The closure that expands the given macro type and macro node into a syntax node.
1266+
///
1267+
/// - Returns:
1268+
/// Returns `.notAMacro` if `node` is `nil` or `node.macroName` isn't registered with any macro type.
1269+
/// Returns `.failure` if `expandMacro` throws an error or returns `nil`, or recursive expansion is detected.
1270+
/// Returns `.success` otherwise.
12201271
private func expandFreestandingMacro<ExpandedMacroType: SyntaxProtocol>(
12211272
_ node: (any FreestandingMacroExpansionSyntax)?,
1222-
expandMacro: (_ macro: Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
1273+
expandMacro: (_ macro: any Macro.Type, _ node: any FreestandingMacroExpansionSyntax) throws -> ExpandedMacroType?
12231274
) -> MacroExpansionResult<ExpandedMacroType> {
12241275
guard let node,
12251276
let macro = macroSystem.lookup(node.macroName.text)?.type
12261277
else {
12271278
return .notAMacro
12281279
}
1280+
12291281
do {
1282+
guard !expandingFreestandingMacros.contains(where: { $0 == macro }) else {
1283+
// We may think of any ongoing macro expansion as a tree in which macro types being expanded are nodes.
1284+
// Any macro type being expanded more than once will create a cycle which the compiler as of now doesn't allow.
1285+
throw MacroExpansionError.recursiveExpansion(macro)
1286+
}
1287+
12301288
if let expanded = try expandMacro(macro, node) {
1231-
return .success(expanded)
1289+
return .success(expansion: MacroExpansion(expandedNode: expanded, macro: macro, macroApplication: self))
12321290
} else {
12331291
return .failure
12341292
}

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)