Skip to content

Commit bef1ba1

Browse files
authored
Merge pull request #2834 from DougGregor/configured-regions-everywhere
[SwiftIfConfig] Make ConfiguredRegions and a BuildConfiguration generally interchangeable
2 parents c30bd36 + 149ef64 commit bef1ba1

File tree

7 files changed

+336
-122
lines changed

7 files changed

+336
-122
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
/// Captures sufficient information to determine the active clause for an `#if`
17+
/// either by querying existing configured regions or by evaluating the
18+
/// clause's conditions against a build configuration.
19+
enum ActiveClauseEvaluator {
20+
case configuredRegions(ConfiguredRegions)
21+
case configuration(any BuildConfiguration)
22+
23+
/// Previously-known diagnostics.
24+
var priorDiagnostics: [Diagnostic] {
25+
switch self {
26+
case .configuredRegions(let configuredRegions):
27+
return configuredRegions.diagnostics
28+
case .configuration:
29+
return []
30+
}
31+
}
32+
33+
/// Determine which clause of an `#if` declaration is active, if any.
34+
///
35+
/// If this evaluation produced any diagnostics, they will be appended to
36+
/// the diagnostics parameter.
37+
func activeClause(
38+
for node: IfConfigDeclSyntax,
39+
diagnostics: inout [Diagnostic]
40+
) -> IfConfigClauseSyntax? {
41+
switch self {
42+
case .configuredRegions(let configuredRegions):
43+
return configuredRegions.activeClause(for: node)
44+
case .configuration(let configuration):
45+
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
46+
diagnostics.append(contentsOf: localDiagnostics)
47+
return activeClause
48+
}
49+
}
50+
}

Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift

+98-54
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,69 @@ 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)
60-
61-
// If there were no active clauses to visit, we're done!
62-
if !visitor.visitedAnyIfClauses {
63-
return (Syntax(self), visitor.diagnostics)
64-
}
65-
66-
// Second pass: Rewrite the syntax tree by removing the inactive clauses
55+
// Rewrite the syntax tree by removing the inactive clauses
6756
// from each #if (along with the #ifs themselves).
6857
let rewriter = ActiveSyntaxRewriter(
6958
configuration: configuration,
7059
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
7160
)
7261
return (
7362
rewriter.rewrite(Syntax(self)),
74-
visitor.diagnostics
63+
rewriter.diagnostics
7564
)
7665
}
7766
}
7867

68+
extension ConfiguredRegions {
69+
/// Produce a copy of some syntax node in the configured region that removes
70+
/// all syntax regions that are inactive according to the build configuration,
71+
/// leaving only the code that is active within that build configuration.
72+
///
73+
/// If there are errors in the conditions of any configuration
74+
/// clauses, e.g., `#if FOO > 10`, then the condition will be
75+
/// considered to have failed and the clauses's elements will be
76+
/// removed.
77+
/// - Parameters:
78+
/// - node: the stnrax node from which inactive regions will be removed.
79+
/// - Returns: the syntax node with all inactive regions removed.
80+
public func removingInactive(from node: some SyntaxProtocol) -> Syntax {
81+
return removingInactive(from: node, retainFeatureCheckIfConfigs: false)
82+
}
83+
84+
/// Produce a copy of some syntax node in the configured region that removes
85+
/// all syntax regions that are inactive according to the build configuration,
86+
/// leaving only the code that is active within that build configuration.
87+
///
88+
/// If there are errors in the conditions of any configuration
89+
/// clauses, e.g., `#if FOO > 10`, then the condition will be
90+
/// considered to have failed and the clauses's elements will be
91+
/// removed.
92+
/// - Parameters:
93+
/// - node: the stnrax node from which inactive regions will be removed.
94+
/// - retainFeatureCheckIfConfigs: whether to retain `#if` blocks involving
95+
/// compiler version checks (e.g., `compiler(>=6.0)`) and `$`-based
96+
/// feature checks.
97+
/// - Returns: the syntax node with all inactive regions removed.
98+
@_spi(Compiler)
99+
public func removingInactive(
100+
from node: some SyntaxProtocol,
101+
retainFeatureCheckIfConfigs: Bool
102+
) -> Syntax {
103+
// If there are no inactive regions, there's nothing to do.
104+
if regions.isEmpty {
105+
return Syntax(node)
106+
}
107+
108+
// Rewrite the syntax tree by removing the inactive clauses
109+
// from each #if (along with the #ifs themselves).
110+
let rewriter = ActiveSyntaxRewriter(
111+
configuredRegions: self,
112+
retainFeatureCheckIfConfigs: retainFeatureCheckIfConfigs
113+
)
114+
return rewriter.rewrite(Syntax(node))
115+
}
116+
}
117+
79118
/// Syntax rewriter that only visits syntax nodes that are active according
80119
/// to a particular build configuration.
81120
///
@@ -106,15 +145,22 @@ extension SyntaxProtocol {
106145
///
107146
/// For any other target platforms, the resulting tree will be empty (other
108147
/// than trivia).
109-
class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
110-
let configuration: Configuration
111-
var diagnostics: [Diagnostic] = []
148+
class ActiveSyntaxRewriter: SyntaxRewriter {
149+
let activeClauses: ActiveClauseEvaluator
150+
var diagnostics: [Diagnostic]
112151

113152
/// Whether to retain `#if` blocks containing compiler and feature checks.
114153
var retainFeatureCheckIfConfigs: Bool
115154

116-
init(configuration: Configuration, retainFeatureCheckIfConfigs: Bool) {
117-
self.configuration = configuration
155+
init(configuredRegions: ConfiguredRegions, retainFeatureCheckIfConfigs: Bool) {
156+
self.activeClauses = .configuredRegions(configuredRegions)
157+
self.diagnostics = activeClauses.priorDiagnostics
158+
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
159+
}
160+
161+
init(configuration: some BuildConfiguration, retainFeatureCheckIfConfigs: Bool) {
162+
self.activeClauses = .configuration(configuration)
163+
self.diagnostics = activeClauses.priorDiagnostics
118164
self.retainFeatureCheckIfConfigs = retainFeatureCheckIfConfigs
119165
}
120166

@@ -124,6 +170,19 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
124170
) -> List {
125171
var newElements: [List.Element] = []
126172
var anyChanged = false
173+
174+
// Note that an element changed at the given index.
175+
func noteElementChanged(at elementIndex: List.Index) {
176+
if anyChanged {
177+
return
178+
}
179+
180+
// This is the first element that changed, so note that we have
181+
// changes and add all prior elements to the list of new elements.
182+
anyChanged = true
183+
newElements.append(contentsOf: node[..<elementIndex])
184+
}
185+
127186
for elementIndex in node.indices {
128187
let element = node[elementIndex]
129188

@@ -132,17 +191,9 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
132191
(!retainFeatureCheckIfConfigs || !ifConfigDecl.containsFeatureCheck)
133192
{
134193
// Retrieve the active `#if` clause
135-
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)
194+
let activeClause = activeClauses.activeClause(for: ifConfigDecl, diagnostics: &diagnostics)
136195

137-
// Add these diagnostics.
138-
diagnostics.append(contentsOf: localDiagnostics)
139-
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-
}
196+
noteElementChanged(at: elementIndex)
146197

147198
// Extract the elements from the active clause, if there are any.
148199
guard let elements = activeClause?.elements else {
@@ -161,6 +212,15 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
161212
continue
162213
}
163214

215+
// Transform the element directly. If it changed, note the changes.
216+
if let transformedElement = rewrite(Syntax(element)).as(List.Element.self),
217+
transformedElement.id != element.id
218+
{
219+
noteElementChanged(at: elementIndex)
220+
newElements.append(transformedElement)
221+
continue
222+
}
223+
164224
if anyChanged {
165225
newElements.append(element)
166226
}
@@ -174,47 +234,39 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
174234
}
175235

176236
override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
177-
let rewrittenNode = dropInactive(node) { element in
237+
return dropInactive(node) { element in
178238
guard case .decl(let declElement) = element.item else {
179239
return nil
180240
}
181241

182242
return declElement.as(IfConfigDeclSyntax.self)
183243
}
184-
185-
return super.visit(rewrittenNode)
186244
}
187245

188246
override func visit(_ node: MemberBlockItemListSyntax) -> MemberBlockItemListSyntax {
189-
let rewrittenNode = dropInactive(node) { element in
247+
return dropInactive(node) { element in
190248
return element.decl.as(IfConfigDeclSyntax.self)
191249
}
192-
193-
return super.visit(rewrittenNode)
194250
}
195251

196252
override func visit(_ node: SwitchCaseListSyntax) -> SwitchCaseListSyntax {
197-
let rewrittenNode = dropInactive(node) { element in
253+
return dropInactive(node) { element in
198254
if case .ifConfigDecl(let ifConfigDecl) = element {
199255
return ifConfigDecl
200256
}
201257

202258
return nil
203259
}
204-
205-
return super.visit(rewrittenNode)
206260
}
207261

208262
override func visit(_ node: AttributeListSyntax) -> AttributeListSyntax {
209-
let rewrittenNode = dropInactive(node) { element in
263+
return dropInactive(node) { element in
210264
if case .ifConfigDecl(let ifConfigDecl) = element {
211265
return ifConfigDecl
212266
}
213267

214268
return nil
215269
}
216-
217-
return super.visit(rewrittenNode)
218270
}
219271

220272
/// Apply the given base to the postfix expression.
@@ -234,7 +286,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
234286
return nil
235287
}
236288

237-
let newExpr = applyBaseToPostfixExpression(base: base, postfix: node[keyPath: keyPath])
289+
let newExpr = applyBaseToPostfixExpression(base: base, postfix: visit(node[keyPath: keyPath]))
238290
return ExprSyntax(node.with(keyPath, newExpr))
239291
}
240292

@@ -243,7 +295,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
243295
guard let memberBase = memberAccess.base else {
244296
// If this member access has no base, this is the base we are
245297
// replacing, terminating the recursion. Do so now.
246-
return ExprSyntax(memberAccess.with(\.base, base))
298+
return ExprSyntax(memberAccess.with(\.base, visit(base)))
247299
}
248300

249301
let newBase = applyBaseToPostfixExpression(base: base, postfix: memberBase)
@@ -302,10 +354,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
302354
}
303355

304356
// Retrieve the active `if` clause.
305-
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)
306-
307-
// Record these diagnostics.
308-
diagnostics.append(contentsOf: localDiagnostics)
357+
let activeClause = activeClauses.activeClause(for: postfixIfConfig.config, diagnostics: &diagnostics)
309358

310359
guard case .postfixExpression(let postfixExpr) = activeClause?.elements
311360
else {
@@ -315,7 +364,7 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
315364
// only have both in an ill-formed syntax tree that was manually
316365
// created.
317366
if let base = postfixIfConfig.base ?? outerBase {
318-
return base
367+
return visit(base)
319368
}
320369

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

331380
// If there is no base, return the postfix expression.
332381
guard let base = postfixIfConfig.base ?? outerBase else {
333-
return postfixExpr
382+
return visit(postfixExpr)
334383
}
335384

336385
// Apply the base to the postfix expression.
337386
return applyBaseToPostfixExpression(base: base, postfix: postfixExpr)
338387
}
339388

340389
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)
390+
return dropInactive(outerBase: nil, postfixIfConfig: node)
347391
}
348392
}
349393

0 commit comments

Comments
 (0)