Skip to content

Commit 99be8fa

Browse files
committed
Add a new API to compute the "configured regions" of a syntax tree
This API produces information similar to the "active regions" API used within the compiler and by SourceKit, a sorted array that indicates the #if clauses that are active or inactive (including distinguishing inactive vs. unparsed). When this array has already been computed for a syntax tree, one can then use the new `SyntaxProtocol.isActive(inConfiguredRegions:)` function to determine whether a given node is active. This can be more efficient than the existing `SyntaxProtocol.isActive(in:)` when querying for many nodes. Test the new functionality by cross-checking the two `isActive` implementations against each other on existing tests.
1 parent 41b82f7 commit 99be8fa

File tree

6 files changed

+172
-18
lines changed

6 files changed

+172
-18
lines changed

Sources/SwiftIfConfig/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_swift_syntax_library(SwiftIfConfig
1010
BuildConfiguration.swift
11+
ConfiguredRegions.swift
1112
IfConfigError.swift
1213
IfConfigEvaluation.swift
1314
IfConfigFunctions.swift
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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+
import SwiftDiagnostics
13+
import SwiftSyntax
14+
15+
extension SyntaxProtocol {
16+
/// Find all of the #if/#elseif/#else clauses within the given syntax node,
17+
/// indicating their active state. This operation will recurse into active
18+
/// clauses to represent the flattened nested structure, while nonactive
19+
/// clauses need no recursion (because there is no relevant structure in
20+
/// them).
21+
///
22+
/// For example, given code like the following:
23+
/// #if DEBUG
24+
/// #if A
25+
/// func f()
26+
/// #elseif B
27+
/// func g()
28+
/// #endif
29+
/// #else
30+
/// #endif
31+
///
32+
/// If the configuration options `DEBUG` and `B` are provided, but `A` is not,
33+
/// the results will be contain:
34+
/// - Active region for the `#if DEBUG`
35+
/// - Inactive region for the `#if A`
36+
/// - Active region for the `#elseif B`
37+
/// - Inactive region for the final `#else`.
38+
public func configuredRegions(
39+
in configuration: some BuildConfiguration
40+
) -> [(IfConfigClauseSyntax, IfConfigState)] {
41+
let visitor = ConfiguredRegionVisitor(configuration: configuration)
42+
visitor.walk(self)
43+
return visitor.regions
44+
}
45+
}
46+
47+
fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: SyntaxVisitor {
48+
let configuration: Configuration
49+
50+
/// The regions we've found so far.
51+
var regions: [(IfConfigClauseSyntax, IfConfigState)] = []
52+
53+
/// Whether we are currently within an active region.
54+
var inActiveRegion = true
55+
56+
init(configuration: Configuration) {
57+
self.configuration = configuration
58+
super.init(viewMode: .sourceAccurate)
59+
}
60+
61+
override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
62+
// If're not within an
63+
let activeClause = inActiveRegion ? (try? node.activeClause(in: configuration)) : nil
64+
for clause in node.clauses {
65+
// If this is the active clause, record it and then recurse into the
66+
// elements.
67+
if clause == activeClause {
68+
assert(inActiveRegion)
69+
70+
regions.append((clause, .active))
71+
72+
if let elements = clause.elements {
73+
walk(elements)
74+
}
75+
76+
continue
77+
}
78+
79+
// For inactive clauses, distinguish between inactive and unparsed.
80+
let isVersioned =
81+
(try? clause.isVersioned(
82+
configuration: configuration,
83+
diagnosticHandler: nil
84+
)) ?? false
85+
86+
// If this is within an active region, or this is an unparsed region,
87+
// record it.
88+
if inActiveRegion || isVersioned {
89+
regions.append((clause, isVersioned ? .unparsed : .inactive))
90+
}
91+
92+
// Recurse into inactive (but not unparsed) regions to find any
93+
// unparsed regions below.
94+
if !isVersioned, let elements = clause.elements {
95+
let priorInActiveRegion = inActiveRegion
96+
inActiveRegion = false
97+
defer {
98+
inActiveRegion = priorInActiveRegion
99+
}
100+
walk(elements)
101+
}
102+
}
103+
104+
return .skipChildren
105+
}
106+
}

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -514,23 +514,13 @@ extension SyntaxProtocol {
514514

515515
if activeClause != ifConfigClause {
516516
// This was not the active clause, so we know that we're in an
517-
// inactive block. However, depending on the condition of this
518-
// clause and any enclosing clauses, it might be an unparsed block.
519-
520-
// Check this condition.
521-
if let condition = ifConfigClause.condition {
522-
// Evaluate this condition against the build configuration.
523-
let (_, versioned) = try evaluateIfConfig(
524-
condition: condition,
525-
configuration: configuration,
526-
diagnosticHandler: diagnosticHandler
527-
)
528-
529-
// If the condition is versioned, this is an unparsed region.
530-
// We already know that it is inactive, or we wouldn't be here.
531-
if versioned {
532-
return .unparsed
533-
}
517+
// inactive block. However, if the condition is versioned, this is an
518+
// unparsed region.
519+
if try ifConfigClause.isVersioned(
520+
configuration: configuration,
521+
diagnosticHandler: diagnosticHandler
522+
) {
523+
return .unparsed
534524
}
535525

536526
currentState = .inactive
@@ -542,4 +532,45 @@ extension SyntaxProtocol {
542532

543533
return currentState
544534
}
535+
536+
/// Determine whether the given syntax node is active given a set of
537+
/// configured regions as produced by `configuredRegions(in:)`.
538+
///
539+
/// This is
540+
/// an approximation
541+
public func isActive(
542+
inConfiguredRegions regions: [(IfConfigClauseSyntax, IfConfigState)]
543+
) -> IfConfigState {
544+
var currentState: IfConfigState = .active
545+
for (ifClause, state) in regions {
546+
if self.position < ifClause.position {
547+
return currentState
548+
}
549+
550+
if self.position <= ifClause.endPosition {
551+
currentState = state
552+
}
553+
}
554+
555+
return currentState
556+
}
557+
}
558+
559+
extension IfConfigClauseSyntax {
560+
/// Determine whether this condition is "versioned".
561+
func isVersioned(
562+
configuration: some BuildConfiguration,
563+
diagnosticHandler: ((Diagnostic) -> Void)?
564+
) throws -> Bool {
565+
guard let condition else { return false }
566+
567+
// Evaluate this condition against the build configuration.
568+
let (_, versioned) = try evaluateIfConfig(
569+
condition: condition,
570+
configuration: configuration,
571+
diagnosticHandler: diagnosticHandler
572+
)
573+
574+
return versioned
575+
}
545576
}

Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ The `SwiftIfConfig` library provides utilities to determine which syntax nodes a
3333
(the node is included in the program), "inactive" (the node is not included
3434
in the program), or "unparsed" (the node is not included in the program and
3535
is also allowed to have syntax errors).
36+
* ``SyntaxProtocol.configuredRegions(in:)`` produces an array describing the various regions in which a configuration has an effect, indicating active, inactive, and unparsed regions in the source tree. The array can be used as an input to `SyntaxProtocol.isActive(inConfiguredRegions:)`` to determine whether a given syntax node is active.

Tests/SwiftIfConfigTest/ActiveRegionTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class ActiveRegionTests: XCTestCase {
2626
func testActiveRegions() throws {
2727
try assertActiveCode(
2828
"""
29+
4️⃣
2930
#if DEBUG
3031
0️⃣func f()
3132
#elseif ASSERTS
@@ -37,13 +38,16 @@ public class ActiveRegionTests: XCTestCase {
3738
3️⃣var i
3839
#endif
3940
#endif
41+
5️⃣token
4042
""",
4143
configuration: linuxBuildConfig,
4244
states: [
4345
"0️⃣": .active,
4446
"1️⃣": .inactive,
4547
"2️⃣": .unparsed,
4648
"3️⃣": .inactive,
49+
"4️⃣": .active,
50+
"5️⃣": .active,
4751
]
4852
)
4953
}

Tests/SwiftIfConfigTest/Assertions.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func assertActiveCode(
7878
var parser = Parser(source)
7979
let tree = SourceFileSyntax.parse(from: &parser)
8080

81+
let configuredRegions = tree.configuredRegions(in: configuration)
82+
8183
for (marker, location) in markerLocations {
8284
guard let expectedState = states[marker] else {
8385
XCTFail("Missing marker \(marker) in expected states", file: file, line: line)
@@ -90,7 +92,16 @@ func assertActiveCode(
9092
}
9193

9294
let actualState = try token.isActive(in: configuration)
93-
XCTAssertEqual(actualState, expectedState, "at marker \(marker)", file: file, line: line)
95+
XCTAssertEqual(actualState, expectedState, "isActive(in:) at marker \(marker)", file: file, line: line)
96+
97+
let actualViaRegions = token.isActive(inConfiguredRegions: configuredRegions)
98+
XCTAssertEqual(
99+
actualViaRegions,
100+
expectedState,
101+
"isActive(inConfiguredRegions:) at marker \(marker)",
102+
file: file,
103+
line: line
104+
)
94105
}
95106
}
96107

0 commit comments

Comments
 (0)