Skip to content

Commit e649799

Browse files
committed
Rework isActive(in: configuration) to distinguish inactive vs. unparsed
Update the interface of this function to return an `IfConfigState` rather than just a `Bool`. Then, check the enclosing versioned conditions to distinguish between inactive vs. unparsed. Finally, add a marker-based assertion function that makes it easy to test the active state of any location in the source code. Use the new test to flush out an obvious bug in my original implementation of `isActive(in: configuration)`.
1 parent 96569a7 commit e649799

File tree

5 files changed

+122
-12
lines changed

5 files changed

+122
-12
lines changed

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -715,25 +715,55 @@ extension SyntaxProtocol {
715715
/// #endif
716716
/// #endif
717717
///
718-
/// a call to `isActive` on the syntax node for the function `g` would return `true` when the
718+
/// a call to `isActive` on the syntax node for the function `g` would return `active` when the
719719
/// configuration options `DEBUG` and `B` are provided, but `A` is not.
720-
public func isActive(in configuration: some BuildConfiguration) throws -> Bool {
720+
public func isActive(
721+
in configuration: some BuildConfiguration,
722+
diagnosticHandler: ((Diagnostic) -> Void)? = nil
723+
) throws -> IfConfigState {
721724
var currentNode: Syntax = Syntax(self)
725+
var currentState: IfConfigState = .active
726+
722727
while let parent = currentNode.parent {
723728
// If the parent is an `#if` configuration, check whether our current
724-
// clause is active. If not, we're in an inactive region.
729+
// clause is active. If not, we're in an inactive region. We also
730+
// need to determine whether
725731
if let ifConfigClause = currentNode.as(IfConfigClauseSyntax.self),
726-
let ifConfigDecl = ifConfigClause.parent?.as(IfConfigDeclSyntax.self)
732+
let ifConfigDecl = ifConfigClause.parent?.parent?.as(IfConfigDeclSyntax.self)
727733
{
728-
if try ifConfigDecl.activeClause(in: configuration) != ifConfigClause {
729-
return false
734+
let activeClause = try ifConfigDecl.activeClause(
735+
in: configuration,
736+
diagnosticHandler: diagnosticHandler
737+
)
738+
739+
if activeClause != ifConfigClause {
740+
// This was not the active clause, so we know that we're in an
741+
// inactive block. However, depending on the condition of this
742+
// clause and any enclosing clauses, it might be an unparsed block.
743+
744+
// Check this condition.
745+
if let condition = ifConfigClause.condition {
746+
// Evaluate this condition against the build configuration.
747+
let (_, versioned) = try evaluateIfConfig(
748+
condition: condition,
749+
configuration: configuration,
750+
diagnosticHandler: diagnosticHandler
751+
)
752+
753+
// If the condition is versioned, this is an unparsed region.
754+
// We already know that it is inactive, or we wouldn't be here.
755+
if versioned {
756+
return .unparsed
757+
}
758+
}
759+
760+
currentState = .inactive
730761
}
731762
}
732763

733764
currentNode = parent
734765
}
735766

736-
// No more enclosing nodes; this code is active.
737-
return true
767+
return currentState
738768
}
739769
}

Sources/SwiftIfConfig/IfConfigVisitor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
7575
walk(Syntax(elements))
7676
}
7777

78-
// Skip everything else in the
78+
// Skip everything else in the #if.
7979
return .skipChildren
8080
} catch {
8181
return reportEvaluationError(at: node, error: error)
Lines changed: 50 additions & 0 deletions
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 - 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 SwiftIfConfig
14+
import SwiftParser
15+
import SwiftSyntax
16+
import SwiftSyntaxMacrosGenericTestSupport
17+
import XCTest
18+
19+
public class ActiveRegionTests: XCTestCase {
20+
let linuxBuildConfig = TestingBuildConfiguration(
21+
customConditions: ["DEBUG", "ASSERTS"],
22+
features: ["ParameterPacks"],
23+
attributes: ["available"]
24+
)
25+
26+
func testActiveRegions() throws {
27+
try assertActiveCode(
28+
"""
29+
#if DEBUG
30+
0️⃣func f()
31+
#elseif ASSERTS
32+
1️⃣func g()
33+
34+
#if compiler(>=8.0)
35+
2️⃣func h()
36+
#else
37+
3️⃣var i
38+
#endif
39+
#endif
40+
""",
41+
configuration: linuxBuildConfig,
42+
states: [
43+
"0️⃣": .active,
44+
"1️⃣": .inactive,
45+
"2️⃣": .unparsed,
46+
"3️⃣": .inactive,
47+
]
48+
)
49+
}
50+
}

Tests/SwiftIfConfigTest/Assertions.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,37 @@ func assertIfConfig(
6161
)
6262
}
6363
}
64+
}
6465

66+
/// Assert that the various marked positions in the source code have the
67+
/// expected active states.
68+
func assertActiveCode(
69+
_ markedSource: String,
70+
configuration: some BuildConfiguration = TestingBuildConfiguration(),
71+
states: [String: IfConfigState],
72+
file: StaticString = #filePath,
73+
line: UInt = #line
74+
) throws {
75+
// Pull out the markers that we'll use to dig out nodes to query.
76+
let (markerLocations, source) = extractMarkers(markedSource)
77+
78+
var parser = Parser(source)
79+
let tree = SourceFileSyntax.parse(from: &parser)
80+
81+
for (marker, location) in markerLocations {
82+
guard let expectedState = states[marker] else {
83+
XCTFail("Missing marker \(marker) in expected states", file: file, line: line)
84+
continue
85+
}
86+
87+
guard let token = tree.token(at: AbsolutePosition(utf8Offset: location)) else {
88+
XCTFail("Unable to find token at location \(location)", file: file, line: line)
89+
continue
90+
}
91+
92+
let actualState = try token.isActive(in: configuration)
93+
XCTAssertEqual(actualState, expectedState, "at marker \(marker)", file: file, line: line)
94+
}
6595
}
6696

6797
/// Assert that applying the given build configuration to the source code

Tests/SwiftIfConfigTest/VisitorTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -25,9 +25,9 @@ class AllActiveVisitor: ActiveSyntaxAnyVisitor<TestingBuildConfiguration> {
2525
super.init(viewMode: .sourceAccurate, configuration: configuration)
2626
}
2727
open override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
28-
var active: Bool = false
28+
var active: IfConfigState = .inactive
2929
XCTAssertNoThrow(try active = node.isActive(in: configuration))
30-
XCTAssertTrue(active)
30+
XCTAssertEqual(active, .active)
3131
return .visitChildren
3232
}
3333
}

0 commit comments

Comments
 (0)