Skip to content

Commit 149ef64

Browse files
committed
Teach ActiveSyntax(Any)Visitor to use configured regions or a build configuration
Rework the implementations of ActiveSyntax(Any)Visitor to allow configured regions, so that clients can re-use the work of evaluating the build configuration for later traversals. As a side effect of this, ActiveSyntax(Any)Visitor are no longer generic over the type of build configuration, which should make them easier to use.
1 parent d50749f commit 149ef64

File tree

3 files changed

+109
-34
lines changed

3 files changed

+109
-34
lines changed

Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift

+28-14
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,30 @@ import SwiftSyntax
3939
/// All notes visited by this visitor will have the "active" state, i.e.,
4040
/// `node.isActive(in: configuration)` will have evaluated to `.active`.
4141
/// When errors occur, they will be recorded in the array of diagnostics.
42-
open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor {
43-
/// The build configuration, which will be queried for each relevant `#if`.
44-
public let configuration: Configuration
42+
open class ActiveSyntaxVisitor: SyntaxVisitor {
43+
/// The abstracted build configuration, which will be queried for each
44+
/// relevant `#if`.
45+
let activeClauses: ActiveClauseEvaluator
4546

4647
/// The diagnostics accumulated during this walk of active syntax.
4748
public private(set) var diagnostics: [Diagnostic] = []
4849

49-
public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) {
50-
self.configuration = configuration
50+
public init(viewMode: SyntaxTreeViewMode, configuration: some BuildConfiguration) {
51+
self.activeClauses = .configuration(configuration)
52+
self.diagnostics = activeClauses.priorDiagnostics
53+
super.init(viewMode: viewMode)
54+
}
55+
56+
public init(viewMode: SyntaxTreeViewMode, configuredRegions: ConfiguredRegions) {
57+
self.activeClauses = .configuredRegions(configuredRegions)
58+
self.diagnostics = activeClauses.priorDiagnostics
5159
super.init(viewMode: viewMode)
5260
}
5361

5462
open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
5563
// Note: there is a clone of this code in ActiveSyntaxAnyVisitor. If you
5664
// change one, please also change the other.
57-
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
58-
diagnostics.append(contentsOf: localDiagnostics)
65+
let activeClause = activeClauses.activeClause(for: node, diagnostics: &diagnostics)
5966

6067
// If there is an active clause, visit it's children.
6168
if let activeClause, let elements = activeClause.elements {
@@ -93,15 +100,23 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
93100
/// All notes visited by this visitor will have the "active" state, i.e.,
94101
/// `node.isActive(in: configuration)` will have evaluated to `.active`.
95102
/// When errors occur, they will be recorded in the array of diagnostics.
96-
open class ActiveSyntaxAnyVisitor<Configuration: BuildConfiguration>: SyntaxAnyVisitor {
97-
/// The build configuration, which will be queried for each relevant `#if`.
98-
public let configuration: Configuration
103+
open class ActiveSyntaxAnyVisitor: SyntaxAnyVisitor {
104+
/// The abstracted build configuration, which will be queried for each
105+
/// relevant `#if`.
106+
let activeClauses: ActiveClauseEvaluator
99107

100108
/// The diagnostics accumulated during this walk of active syntax.
101109
public private(set) var diagnostics: [Diagnostic] = []
102110

103-
public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) {
104-
self.configuration = configuration
111+
public init(viewMode: SyntaxTreeViewMode, configuration: some BuildConfiguration) {
112+
self.activeClauses = .configuration(configuration)
113+
self.diagnostics = activeClauses.priorDiagnostics
114+
super.init(viewMode: viewMode)
115+
}
116+
117+
public init(viewMode: SyntaxTreeViewMode, configuredRegions: ConfiguredRegions) {
118+
self.activeClauses = .configuredRegions(configuredRegions)
119+
self.diagnostics = activeClauses.priorDiagnostics
105120
super.init(viewMode: viewMode)
106121
}
107122

@@ -110,8 +125,7 @@ open class ActiveSyntaxAnyVisitor<Configuration: BuildConfiguration>: SyntaxAnyV
110125
// change one, please also change the other.
111126

112127
// If there is an active clause, visit it's children.
113-
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
114-
diagnostics.append(contentsOf: localDiagnostics)
128+
let activeClause = activeClauses.activeClause(for: node, diagnostics: &diagnostics)
115129

116130
if let activeClause, let elements = activeClause.elements {
117131
walk(elements)

Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ The `SwiftIfConfig` library provides utilities to determine which syntax nodes a
2929
* <doc:ActiveSyntaxVisitor> and <doc:ActiveSyntaxAnyVisitor> are visitor types that only visit the syntax nodes that are included ("active") for a given build configuration, implicitly skipping any nodes within inactive `#if` clauses.
3030
* `SyntaxProtocol.removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions.
3131
* `IfConfigDeclSyntax.activeClause(in:)` determines which of the clauses of an `#if` is active for the given build configuration, returning the active clause.
32-
* `SyntaxProtocol.configuredRegions(in:)` produces a `ConfiguredRegions` value that can be used to efficiently test whether a given syntax node is in an active, inactive, or unparsed region (via `isActive`).
32+
* `SyntaxProtocol.configuredRegions(in:)` produces a `ConfiguredRegions` value that can be used to efficiently test whether a given syntax node is in an active, inactive, or unparsed region, remove inactive syntax, or determine the
33+
active clause for a given `#if`. Use `ConfiguredRegions` for repeated queries.

Tests/SwiftIfConfigTest/VisitorTests.swift

+79-19
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,49 @@ import _SwiftSyntaxTestSupport
2323
///
2424
/// This cross-checks the visitor itself with the `SyntaxProtocol.isActive(in:)`
2525
/// API.
26-
class AllActiveVisitor: ActiveSyntaxAnyVisitor<TestingBuildConfiguration> {
27-
init(configuration: TestingBuildConfiguration) {
28-
super.init(viewMode: .sourceAccurate, configuration: configuration)
26+
class AllActiveVisitor: ActiveSyntaxAnyVisitor {
27+
let configuration: TestingBuildConfiguration
28+
29+
init(
30+
configuration: TestingBuildConfiguration,
31+
configuredRegions: ConfiguredRegions? = nil
32+
) {
33+
self.configuration = configuration
34+
35+
if let configuredRegions {
36+
super.init(viewMode: .sourceAccurate, configuredRegions: configuredRegions)
37+
} else {
38+
super.init(viewMode: .sourceAccurate, configuration: configuration)
39+
}
2940
}
41+
3042
open override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
3143
XCTAssertEqual(node.isActive(in: configuration).state, .active)
3244
return .visitChildren
3345
}
3446
}
3547

36-
class NameCheckingVisitor: ActiveSyntaxAnyVisitor<TestingBuildConfiguration> {
48+
class NameCheckingVisitor: ActiveSyntaxAnyVisitor {
49+
let configuration: TestingBuildConfiguration
50+
3751
/// The set of names we are expected to visit. Any syntax nodes with
3852
/// names that aren't here will be rejected, and each of the names listed
3953
/// here must occur exactly once.
4054
var expectedNames: Set<String>
4155

42-
init(configuration: TestingBuildConfiguration, expectedNames: Set<String>) {
56+
init(
57+
configuration: TestingBuildConfiguration,
58+
expectedNames: Set<String>,
59+
configuredRegions: ConfiguredRegions? = nil
60+
) {
61+
self.configuration = configuration
4362
self.expectedNames = expectedNames
4463

45-
super.init(viewMode: .sourceAccurate, configuration: configuration)
64+
if let configuredRegions {
65+
super.init(viewMode: .sourceAccurate, configuredRegions: configuredRegions)
66+
} else {
67+
super.init(viewMode: .sourceAccurate, configuration: configuration)
68+
}
4669
}
4770

4871
deinit {
@@ -145,32 +168,31 @@ public class VisitorTests: XCTestCase {
145168

146169
func testAnyVisitorVisitsOnlyActive() throws {
147170
// Make sure that all visited nodes are active nodes.
148-
AllActiveVisitor(configuration: linuxBuildConfig).walk(inputSource)
149-
AllActiveVisitor(configuration: iosBuildConfig).walk(inputSource)
171+
assertVisitedAllActive(linuxBuildConfig)
172+
assertVisitedAllActive(iosBuildConfig)
150173
}
151174

152175
func testVisitsExpectedNodes() throws {
153176
// Check that the right set of names is visited.
154-
NameCheckingVisitor(
155-
configuration: linuxBuildConfig,
177+
assertVisitedExpectedNames(
178+
linuxBuildConfig,
156179
expectedNames: ["f", "h", "i", "S", "generationCount", "value", "withAvail"]
157-
).walk(inputSource)
180+
)
158181

159-
NameCheckingVisitor(
160-
configuration: iosBuildConfig,
182+
assertVisitedExpectedNames(
183+
iosBuildConfig,
161184
expectedNames: ["g", "h", "i", "a", "S", "generationCount", "value", "error", "withAvail"]
162-
).walk(inputSource)
185+
)
163186
}
164187

165188
func testVisitorWithErrors() throws {
166189
var configuration = linuxBuildConfig
167190
configuration.badAttributes.insert("available")
168-
let visitor = NameCheckingVisitor(
169-
configuration: configuration,
170-
expectedNames: ["f", "h", "i", "S", "generationCount", "value", "notAvail"]
191+
assertVisitedExpectedNames(
192+
configuration,
193+
expectedNames: ["f", "h", "i", "S", "generationCount", "value", "notAvail"],
194+
diagnosticCount: 3
171195
)
172-
visitor.walk(inputSource)
173-
XCTAssertEqual(visitor.diagnostics.count, 3)
174196
}
175197

176198
func testRemoveInactive() {
@@ -337,6 +359,44 @@ public class VisitorTests: XCTestCase {
337359
}
338360
}
339361

362+
extension VisitorTests {
363+
/// Ensure that all visited nodes are active nodes according to the given
364+
/// build configuration.
365+
fileprivate func assertVisitedAllActive(_ configuration: TestingBuildConfiguration) {
366+
AllActiveVisitor(configuration: configuration).walk(inputSource)
367+
368+
let configuredRegions = inputSource.configuredRegions(in: configuration)
369+
AllActiveVisitor(
370+
configuration: configuration,
371+
configuredRegions: configuredRegions
372+
).walk(inputSource)
373+
}
374+
375+
/// Ensure that we visit nodes with the set of names we were expecting to
376+
/// visit.
377+
fileprivate func assertVisitedExpectedNames(
378+
_ configuration: TestingBuildConfiguration,
379+
expectedNames: Set<String>,
380+
diagnosticCount: Int = 0
381+
) {
382+
let firstVisitor = NameCheckingVisitor(
383+
configuration: configuration,
384+
expectedNames: expectedNames
385+
)
386+
firstVisitor.walk(inputSource)
387+
XCTAssertEqual(firstVisitor.diagnostics.count, diagnosticCount)
388+
389+
let configuredRegions = inputSource.configuredRegions(in: configuration)
390+
let secondVisitor = NameCheckingVisitor(
391+
configuration: configuration,
392+
expectedNames: expectedNames,
393+
configuredRegions: configuredRegions
394+
)
395+
secondVisitor.walk(inputSource)
396+
XCTAssertEqual(secondVisitor.diagnostics.count, diagnosticCount)
397+
}
398+
}
399+
340400
/// Assert that removing any inactive code according to the given build
341401
/// configuration returns the expected source and diagnostics.
342402
fileprivate func assertRemoveInactive(

0 commit comments

Comments
 (0)