Skip to content

Commit 5fba646

Browse files
committed
Rework ActiveSyntaxRewriter to operate on top of ConfiguredRegions
The operation that removes all inactive (and unparsed) regions from a syntax tree is a two-pass algorithm. For clients that have already created a ConfiguredRegions instance, let that serve as the first pass. ConfiguredRegions now tracks the active clauses of any evaluated `#if`s, and the ActiveSyntaxRewriter uses that information to guide its traversal and rewrite rather than re-evaluating all of the clauses. This required reworking the way in which we handle recursion in ActiveSyntaxRewriter. Before, we were rewriting nodes "outside-in", but doing so broke node identity and, therefore, lookup of the active #if clauses. Instead, make sure we rewrite from the leaves up to the root. Clients still have the option of removing inactive/unparsed code from a syntax node given just a build configuration, in which case we'll first create a ConfiguredRegions.
1 parent 2115d77 commit 5fba646

File tree

3 files changed

+120
-60
lines changed

3 files changed

+120
-60
lines changed

Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift

Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,64 @@ extension SyntaxProtocol {
5252
in configuration: some BuildConfiguration,
5353
retainFeatureCheckIfConfigs: Bool
5454
) -> (result: Syntax, diagnostics: [Diagnostic]) {
55-
// First pass: Find all of the active clauses for the #ifs we need to
56-
// visit, along with any diagnostics produced along the way. This process
57-
// does not change the tree in any way.
58-
let visitor = ActiveSyntaxVisitor(viewMode: .sourceAccurate, configuration: configuration)
59-
visitor.walk(self)
55+
let configuredRegions = configuredRegions(in: configuration)
56+
return (
57+
result: configuredRegions.removingInactive(
58+
from: self,
59+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
60+
),
61+
diagnostics: configuredRegions.diagnostics
62+
)
63+
}
64+
}
6065

61-
// If there were no active clauses to visit, we're done!
62-
if !visitor.visitedAnyIfClauses {
63-
return (Syntax(self), visitor.diagnostics)
66+
extension ConfiguredRegions {
67+
/// Produce a copy of some syntax node in the configured region that removes
68+
/// all syntax regions that are inactive according to the build configuration,
69+
/// leaving only the code that is active within that build configuration.
70+
///
71+
/// If there are errors in the conditions of any configuration
72+
/// clauses, e.g., `#if FOO > 10`, then the condition will be
73+
/// considered to have failed and the clauses's elements will be
74+
/// removed.
75+
/// - Parameters:
76+
/// - node: the stnrax node from which inactive regions will be removed.
77+
/// - Returns: the syntax node with all inactive regions removed.
78+
public func removingInactive(from node: some SyntaxProtocol) -> Syntax {
79+
return removingInactive(from: node, retainFeatureCheckIfConfigs: false)
80+
}
81+
82+
/// Produce a copy of some syntax node in the configured region that removes
83+
/// all syntax regions that are inactive according to the build configuration,
84+
/// leaving only the code that is active within that build configuration.
85+
///
86+
/// If there are errors in the conditions of any configuration
87+
/// clauses, e.g., `#if FOO > 10`, then the condition will be
88+
/// considered to have failed and the clauses's elements will be
89+
/// removed.
90+
/// - Parameters:
91+
/// - node: the stnrax node from which inactive regions will be removed.
92+
/// - retainFeatureCheckIfConfigs: whether to retain `#if` blocks involving
93+
/// compiler version checks (e.g., `compiler(>=6.0)`) and `$`-based
94+
/// feature checks.
95+
/// - Returns: the syntax node with all inactive regions removed.
96+
@_spi(Compiler)
97+
public func removingInactive(
98+
from node: some SyntaxProtocol,
99+
retainFeatureCheckIfConfigs: Bool
100+
) -> Syntax {
101+
// If there are no inactive regions, there's nothing to do.
102+
if regions.isEmpty {
103+
return Syntax(node)
64104
}
65105

66-
// Second pass: Rewrite the syntax tree by removing the inactive clauses
106+
// Rewrite the syntax tree by removing the inactive clauses
67107
// from each #if (along with the #ifs themselves).
68108
let rewriter = ActiveSyntaxRewriter(
69-
configuration: configuration,
109+
configuredRegions: self,
70110
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
71111
)
72-
return (
73-
rewriter.rewrite(Syntax(self)),
74-
visitor.diagnostics
75-
)
112+
return rewriter.rewrite(Syntax(node))
76113
}
77114
}
78115

@@ -106,15 +143,16 @@ extension SyntaxProtocol {
106143
///
107144
/// For any other target platforms, the resulting tree will be empty (other
108145
/// than trivia).
109-
class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
110-
let configuration: Configuration
111-
var diagnostics: [Diagnostic] = []
146+
class ActiveSyntaxRewriter: SyntaxRewriter {
147+
let configuredRegions: ConfiguredRegions
148+
var diagnostics: [Diagnostic]
112149

113150
/// Whether to retain `#if` blocks containing compiler and feature checks.
114151
var retainFeatureCheckIfConfigs: Bool
115152

116-
init(configuration: Configuration, retainFeatureCheckIfConfigs: Bool) {
117-
self.configuration = configuration
153+
init(configuredRegions: ConfiguredRegions, retainFeatureCheckIfConfigs: Bool) {
154+
self.configuredRegions = configuredRegions
155+
self.diagnostics = configuredRegions.diagnostics
118156
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
119157
}
120158

@@ -124,6 +162,19 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
124162
) -> List {
125163
var newElements: [List.Element] = []
126164
var anyChanged = false
165+
166+
// Note that an element changed at the given index.
167+
func noteElementChanged(at elementIndex: List.Index) {
168+
if anyChanged {
169+
return
170+
}
171+
172+
// This is the first element that changed, so note that we have
173+
// changes and add all prior elements to the list of new elements.
174+
anyChanged = true
175+
newElements.append(contentsOf: node[..<elementIndex])
176+
}
177+
127178
for elementIndex in node.indices {
128179
let element = node[elementIndex]
129180

@@ -132,17 +183,9 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
132183
(!retainFeatureCheckIfConfigs || !ifConfigDecl.containsFeatureCheck)
133184
{
134185
// Retrieve the active `#if` clause
135-
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)
136-
137-
// Add these diagnostics.
138-
diagnostics.append(contentsOf: localDiagnostics)
186+
let activeClause = configuredRegions.activeClause(for: ifConfigDecl)
139187

140-
// If this is the first element that changed, note that we have
141-
// changes and add all prior elements to the list of new elements.
142-
if !anyChanged {
143-
anyChanged = true
144-
newElements.append(contentsOf: node[..<elementIndex])
145-
}
188+
noteElementChanged(at: elementIndex)
146189

147190
// Extract the elements from the active clause, if there are any.
148191
guard let elements = activeClause?.elements else {
@@ -161,6 +204,15 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
161204
continue
162205
}
163206

207+
// Transform the element directly. If it changed, note the changes.
208+
if let transformedElement = rewrite(Syntax(element)).as(List.Element.self),
209+
transformedElement.id != element.id
210+
{
211+
noteElementChanged(at: elementIndex)
212+
newElements.append(transformedElement)
213+
continue
214+
}
215+
164216
if anyChanged {
165217
newElements.append(element)
166218
}
@@ -174,47 +226,39 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
174226
}
175227

176228
override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
177-
let rewrittenNode = dropInactive(node) { element in
229+
return dropInactive(node) { element in
178230
guard case .decl(let declElement) = element.item else {
179231
return nil
180232
}
181233

182234
return declElement.as(IfConfigDeclSyntax.self)
183235
}
184-
185-
return super.visit(rewrittenNode)
186236
}
187237

188238
override func visit(_ node: MemberBlockItemListSyntax) -> MemberBlockItemListSyntax {
189-
let rewrittenNode = dropInactive(node) { element in
239+
return dropInactive(node) { element in
190240
return element.decl.as(IfConfigDeclSyntax.self)
191241
}
192-
193-
return super.visit(rewrittenNode)
194242
}
195243

196244
override func visit(_ node: SwitchCaseListSyntax) -> SwitchCaseListSyntax {
197-
let rewrittenNode = dropInactive(node) { element in
245+
return dropInactive(node) { element in
198246
if case .ifConfigDecl(let ifConfigDecl) = element {
199247
return ifConfigDecl
200248
}
201249

202250
return nil
203251
}
204-
205-
return super.visit(rewrittenNode)
206252
}
207253

208254
override func visit(_ node: AttributeListSyntax) -> AttributeListSyntax {
209-
let rewrittenNode = dropInactive(node) { element in
255+
return dropInactive(node) { element in
210256
if case .ifConfigDecl(let ifConfigDecl) = element {
211257
return ifConfigDecl
212258
}
213259

214260
return nil
215261
}
216-
217-
return super.visit(rewrittenNode)
218262
}
219263

220264
/// Apply the given base to the postfix expression.
@@ -234,7 +278,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
234278
return nil
235279
}
236280

237-
let newExpr = applyBaseToPostfixExpression(base: base, postfix: node[keyPath: keyPath])
281+
let newExpr = applyBaseToPostfixExpression(base: base, postfix: visit(node[keyPath: keyPath]))
238282
return ExprSyntax(node.with(keyPath, newExpr))
239283
}
240284

@@ -243,7 +287,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
243287
guard let memberBase = memberAccess.base else {
244288
// If this member access has no base, this is the base we are
245289
// replacing, terminating the recursion. Do so now.
246-
return ExprSyntax(memberAccess.with(\.base, base))
290+
return ExprSyntax(memberAccess.with(\.base, visit(base)))
247291
}
248292

249293
let newBase = applyBaseToPostfixExpression(base: base, postfix: memberBase)
@@ -302,10 +346,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
302346
}
303347

304348
// Retrieve the active `if` clause.
305-
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)
306-
307-
// Record these diagnostics.
308-
diagnostics.append(contentsOf: localDiagnostics)
349+
let activeClause = configuredRegions.activeClause(for: postfixIfConfig.config)
309350

310351
guard case .postfixExpression(let postfixExpr) = activeClause?.elements
311352
else {
@@ -315,7 +356,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
315356
// only have both in an ill-formed syntax tree that was manually
316357
// created.
317358
if let base = postfixIfConfig.base ?? outerBase {
318-
return base
359+
return visit(base)
319360
}
320361

321362
// If there was no base, we're in an erroneous syntax tree that would
@@ -330,20 +371,15 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
330371

331372
// If there is no base, return the postfix expression.
332373
guard let base = postfixIfConfig.base ?? outerBase else {
333-
return postfixExpr
374+
return visit(postfixExpr)
334375
}
335376

336377
// Apply the base to the postfix expression.
337378
return applyBaseToPostfixExpression(base: base, postfix: postfixExpr)
338379
}
339380

340381
override func visit(_ node: PostfixIfConfigExprSyntax) -> ExprSyntax {
341-
let rewrittenNode = dropInactive(outerBase: nil, postfixIfConfig: node)
342-
if rewrittenNode == ExprSyntax(node) {
343-
return rewrittenNode
344-
}
345-
346-
return visit(rewrittenNode)
382+
return dropInactive(outerBase: nil, postfixIfConfig: node)
347383
}
348384
}
349385

Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
4646
/// The diagnostics accumulated during this walk of active syntax.
4747
public private(set) var diagnostics: [Diagnostic] = []
4848

49-
/// Whether we visited any "#if" clauses.
50-
var visitedAnyIfClauses: Bool = false
51-
5249
public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) {
5350
self.configuration = configuration
5451
super.init(viewMode: viewMode)
@@ -60,8 +57,6 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
6057
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
6158
diagnostics.append(contentsOf: localDiagnostics)
6259

63-
visitedAnyIfClauses = true
64-
6560
// If there is an active clause, visit it's children.
6661
if let activeClause, let elements = activeClause.elements {
6762
walk(elements)

Sources/SwiftIfConfig/ConfiguredRegions.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ import SwiftSyntax
3939
public struct ConfiguredRegions {
4040
let regions: [(ifClause: IfConfigClauseSyntax, state: IfConfigRegionState)]
4141

42+
// A mapping from each of the #if declarations that have been evaluated to
43+
// the active clause. Absence from this map means that there is no active
44+
// clause, either because every clause failed or because the entire #if
45+
// itself is inactive.
46+
var activeClauses: [IfConfigDeclSyntax: IfConfigClauseSyntax]
47+
4248
/// The set of diagnostics produced when evaluating the configured regions.
4349
public let diagnostics: [Diagnostic]
4450

@@ -80,6 +86,17 @@ public struct ConfiguredRegions {
8086
(region.ifClause.regionStart...region.ifClause.endPosition).contains(node.position)
8187
}?.state ?? .active
8288
}
89+
90+
/// Determine which clause of an `#if` declaration was active within this
91+
/// set of configured regions.
92+
///
93+
/// A particular `#if` declaration might have no active clause (e.g., this
94+
/// operation will return a `nil`) if either none of the clauses had
95+
/// conditions that succeeded, or the `#if` declaration itself is within an
96+
/// inactive (or unparsed) region and therefore cannot have an active clause.
97+
public func activeClause(for node: IfConfigDeclSyntax) -> IfConfigClauseSyntax? {
98+
return activeClauses[node]
99+
}
83100
}
84101

85102
extension ConfiguredRegions: RandomAccessCollection {
@@ -150,6 +167,7 @@ extension SyntaxProtocol {
150167
visitor.walk(self)
151168
return ConfiguredRegions(
152169
regions: visitor.regions,
170+
activeClauses: visitor.activeClauses,
153171
diagnostics: visitor.diagnostics
154172
)
155173
}
@@ -171,6 +189,12 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
171189
// All diagnostics encountered along the way.
172190
var diagnostics: [Diagnostic] = []
173191

192+
// A mapping from each of the #if declarations that have been evaluated to
193+
// the active clause. Absence from this map means that there is no active
194+
// clause, either because every clause failed or because the entire #if
195+
// itself is inactive.
196+
var activeClauses: [IfConfigDeclSyntax: IfConfigClauseSyntax] = [:]
197+
174198
init(configuration: Configuration) {
175199
self.configuration = configuration
176200
super.init(viewMode: .sourceAccurate)
@@ -242,6 +266,11 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
242266
isActive = !foundActive && inActiveRegion
243267
}
244268

269+
// If this is the active clause, record it as such.
270+
if isActive {
271+
activeClauses[node] = clause
272+
}
273+
245274
// Determine and record the current state.
246275
let currentState: IfConfigRegionState
247276
switch (isActive, syntaxErrorsAllowed) {

0 commit comments

Comments
 (0)