Skip to content

Commit cdff432

Browse files
authored
Merge pull request #2819 from DougGregor/ifconfig-compiler-improvements
2 parents 049dc7f + 98aeb44 commit cdff432

6 files changed

+232
-97
lines changed

Diff for: Sources/SwiftIfConfig/BuildConfiguration.swift

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1010
//
1111
//===----------------------------------------------------------------------===//
12+
import SwiftSyntax
1213

1314
/// Describes the ordering of a sequence of bytes that make up a word of
1415
/// storage for a particular architecture.
@@ -114,15 +115,15 @@ public protocol BuildConfiguration {
114115
/// information, which will translate into the `version` argument.
115116
///
116117
/// - Parameters:
117-
/// - importPath: A nonempty sequence of identifiers describing the
118-
/// imported module, which was written in source as a dotted sequence,
119-
/// e.g., `UIKit.UIViewController` will be passed in as the import path
120-
/// array `["UIKit", "UIViewController"]`.
118+
/// - importPath: A nonempty sequence of (token, identifier) pairs
119+
/// describing the imported module, which was written in source as a
120+
/// dotted sequence, e.g., `UIKit.UIViewController` will be passed in as
121+
/// the import path array `[(token, "UIKit"), (token, "UIViewController")]`.
121122
/// - version: The version restriction on the imported module. For the
122123
/// normal `canImport(<import-path>)` syntax, this will always be
123124
/// `CanImportVersion.unversioned`.
124125
/// - Returns: Whether the module can be imported.
125-
func canImport(importPath: [String], version: CanImportVersion) throws -> Bool
126+
func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool
126127

127128
/// Determine whether the given name is the active target OS (e.g., Linux, iOS).
128129
///

Diff for: Sources/SwiftIfConfig/ConfiguredRegions.swift

+170-30
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,90 @@
1313
import SwiftDiagnostics
1414
import SwiftSyntax
1515

16+
/// Describes all of the #if/#elseif/#else clauses within the given syntax node,
17+
/// indicating their active state. This operation will recurse into all
18+
/// clauses to indicate regions of active / inactive / unparsed code.
19+
///
20+
/// For example, given code like the following:
21+
/// #if DEBUG
22+
/// #if A
23+
/// func f()
24+
/// #elseif B
25+
/// func g()
26+
/// #elseif compiler(>= 12.0)
27+
/// please print the number after 41
28+
/// #endif
29+
/// #else
30+
/// #endif
31+
///
32+
/// If the configuration options `DEBUG` and `B` are provided, but `A` is not,
33+
/// and the compiler version is less than 12.0, 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+
/// - Unparsed region for the `#elseif compiler(>= 12.0)`.
38+
/// - Inactive region for the final `#else`.
39+
public struct ConfiguredRegions {
40+
let regions: [Element]
41+
42+
/// The set of diagnostics produced when evaluating the configured regions.
43+
public let diagnostics: [Diagnostic]
44+
45+
/// Determine whether the given syntax node is active within the configured
46+
/// regions.
47+
public func isActive(_ node: some SyntaxProtocol) -> IfConfigRegionState {
48+
var currentState: IfConfigRegionState = .active
49+
for (ifClause, state) in regions {
50+
if node.position < ifClause.position {
51+
return currentState
52+
}
53+
54+
if node.position >= ifClause.regionStart && node.position <= ifClause.endPosition {
55+
currentState = state
56+
}
57+
}
58+
59+
return currentState
60+
}
61+
}
62+
63+
extension ConfiguredRegions: RandomAccessCollection {
64+
public typealias Element = (IfConfigClauseSyntax, IfConfigRegionState)
65+
public var startIndex: Int { regions.startIndex }
66+
public var endIndex: Int { regions.endIndex }
67+
68+
public subscript(index: Int) -> Element {
69+
regions[index]
70+
}
71+
}
72+
73+
extension ConfiguredRegions: CustomDebugStringConvertible {
74+
/// Provides source ranges for each of the configured regions.
75+
public var debugDescription: String {
76+
guard let firstRegion = first else {
77+
return "[]"
78+
}
79+
80+
let root = firstRegion.0.root
81+
let converter = SourceLocationConverter(fileName: "", tree: root)
82+
let regionDescriptions = regions.map { (ifClause, state) in
83+
let startPosition = converter.location(for: ifClause.position)
84+
let endPosition = converter.location(for: ifClause.endPosition)
85+
return "[\(startPosition.line):\(startPosition.column) - \(endPosition.line):\(endPosition.column)] = \(state)"
86+
}
87+
88+
return "[\(regionDescriptions.joined(separator: ", ")))]"
89+
}
90+
}
91+
92+
extension IfConfigClauseSyntax {
93+
/// The effective start of the region after which code is subject to its
94+
/// condition.
95+
fileprivate var regionStart: AbsolutePosition {
96+
condition?.endPosition ?? elements?._syntaxNode.position ?? poundKeyword.endPosition
97+
}
98+
}
99+
16100
extension SyntaxProtocol {
17101
/// Find all of the #if/#elseif/#else clauses within the given syntax node,
18102
/// indicating their active state. This operation will recurse into all
@@ -39,10 +123,13 @@ extension SyntaxProtocol {
39123
/// - Inactive region for the final `#else`.
40124
public func configuredRegions(
41125
in configuration: some BuildConfiguration
42-
) -> [(IfConfigClauseSyntax, IfConfigRegionState)] {
126+
) -> ConfiguredRegions {
43127
let visitor = ConfiguredRegionVisitor(configuration: configuration)
44128
visitor.walk(self)
45-
return visitor.regions
129+
return ConfiguredRegions(
130+
regions: visitor.regions,
131+
diagnostics: visitor.diagnostics
132+
)
46133
}
47134
}
48135

@@ -56,58 +143,111 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
56143
/// Whether we are currently within an active region.
57144
var inActiveRegion = true
58145

146+
/// Whether we are currently within an #if at all.
147+
var inAnyIfConfig = false
148+
149+
// All diagnostics encountered along the way.
150+
var diagnostics: [Diagnostic] = []
151+
59152
init(configuration: Configuration) {
60153
self.configuration = configuration
61154
super.init(viewMode: .sourceAccurate)
62155
}
63156

64157
override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
65-
// If we're in an active region, find the active clause. Otherwise,
66-
// there isn't one.
67-
let activeClause = inActiveRegion ? node.activeClause(in: configuration).clause : nil
158+
// We are in an #if.
159+
let priorInAnyIfConfig = inAnyIfConfig
160+
inAnyIfConfig = true
161+
defer {
162+
inAnyIfConfig = priorInAnyIfConfig
163+
}
164+
165+
// Walk through the clauses to find the active one.
68166
var foundActive = false
69167
var syntaxErrorsAllowed = false
168+
let outerState: IfConfigRegionState = inActiveRegion ? .active : .inactive
70169
for clause in node.clauses {
71-
// If we haven't found the active clause yet, syntax errors are allowed
72-
// depending on this clause.
73-
if !foundActive {
74-
syntaxErrorsAllowed =
75-
clause.condition.map {
76-
IfConfigClauseSyntax.syntaxErrorsAllowed($0).syntaxErrorsAllowed
77-
} ?? false
78-
}
170+
let isActive: Bool
171+
if let condition = clause.condition {
172+
if !foundActive {
173+
// Fold operators so we can evaluate this #if condition.
174+
let (foldedCondition, foldDiagnostics) = IfConfigClauseSyntax.foldOperators(condition)
175+
diagnostics.append(contentsOf: foldDiagnostics)
176+
177+
// In an active region, evaluate the condition to determine whether
178+
// this clause is active. Otherwise, this clause is inactive.
179+
// inactive.
180+
if inActiveRegion {
181+
let (thisIsActive, _, evalDiagnostics) = evaluateIfConfig(
182+
condition: foldedCondition,
183+
configuration: configuration
184+
)
185+
diagnostics.append(contentsOf: evalDiagnostics)
79186

80-
// If this is the active clause, record it and then recurse into the
81-
// elements.
82-
if clause == activeClause {
83-
assert(inActiveRegion)
187+
// Determine if there was an error that prevented us from
188+
// evaluating the condition. If so, we'll allow syntax errors
189+
// from here on out.
190+
let hadError =
191+
foldDiagnostics.contains { diag in
192+
diag.diagMessage.severity == .error
193+
}
194+
|| evalDiagnostics.contains { diag in
195+
diag.diagMessage.severity == .error
196+
}
84197

85-
regions.append((clause, .active))
198+
if hadError {
199+
isActive = false
200+
syntaxErrorsAllowed = true
201+
} else {
202+
isActive = thisIsActive
86203

87-
if let elements = clause.elements {
88-
walk(elements)
204+
// Determine whether syntax errors are allowed.
205+
syntaxErrorsAllowed = foldedCondition.allowsSyntaxErrorsFolded
206+
}
207+
} else {
208+
isActive = false
209+
210+
// Determine whether syntax errors are allowed, even though we
211+
// skipped evaluation of the actual condition.
212+
syntaxErrorsAllowed = foldedCondition.allowsSyntaxErrorsFolded
213+
}
214+
} else {
215+
// We already found an active condition, so this is inactive.
216+
isActive = false
89217
}
218+
} else {
219+
// This is an #else. It's active if we haven't found an active clause
220+
// yet and are in an active region.
221+
isActive = !foundActive && inActiveRegion
222+
}
90223

91-
foundActive = true
92-
continue
224+
// Determine and record the current state.
225+
let currentState: IfConfigRegionState
226+
switch (isActive, syntaxErrorsAllowed) {
227+
case (true, _): currentState = .active
228+
case (false, false): currentState = .inactive
229+
case (false, true): currentState = .unparsed
93230
}
94231

95-
// If this is within an active region, or this is an unparsed region,
96-
// record it.
97-
if inActiveRegion || syntaxErrorsAllowed {
98-
regions.append((clause, syntaxErrorsAllowed ? .unparsed : .inactive))
232+
// If there is a state change, record it.
233+
if !priorInAnyIfConfig || currentState != .inactive || currentState != outerState {
234+
regions.append((clause, currentState))
99235
}
100236

101-
// Recurse into inactive (but not unparsed) regions to find any
102-
// unparsed regions below.
103-
if !syntaxErrorsAllowed, let elements = clause.elements {
237+
// If this is a parsed region, recurse into it.
238+
if currentState != .unparsed, let elements = clause.elements {
104239
let priorInActiveRegion = inActiveRegion
105-
inActiveRegion = false
240+
inActiveRegion = isActive
106241
defer {
107242
inActiveRegion = priorInActiveRegion
108243
}
109244
walk(elements)
110245
}
246+
247+
// Note when we found an active clause.
248+
if isActive {
249+
foundActive = true
250+
}
111251
}
112252

113253
return .skipChildren

Diff for: Sources/SwiftIfConfig/IfConfigEvaluation.swift

+8-8
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ func evaluateIfConfig(
443443
}
444444

445445
// Extract the import path.
446-
let importPath: [String]
446+
let importPath: [(TokenSyntax, String)]
447447
do {
448448
importPath = try extractImportPath(firstArg.expression)
449449
} catch {
@@ -502,7 +502,7 @@ func evaluateIfConfig(
502502
return checkConfiguration(at: call) {
503503
(
504504
active: try configuration.canImport(
505-
importPath: importPath.map { String($0) },
505+
importPath: importPath,
506506
version: version
507507
),
508508
syntaxErrorsAllowed: fn.syntaxErrorsAllowed
@@ -540,22 +540,22 @@ extension SyntaxProtocol {
540540
}
541541

542542
/// Given an expression with the expected form A.B.C, extract the import path
543-
/// ["A", "B", "C"] from it. Throws an error if the expression doesn't match
544-
/// this form.
545-
private func extractImportPath(_ expression: some ExprSyntaxProtocol) throws -> [String] {
543+
/// ["A", "B", "C"] from it with the token syntax nodes for each name.
544+
/// Throws an error if the expression doesn't match this form.
545+
private func extractImportPath(_ expression: some ExprSyntaxProtocol) throws -> [(TokenSyntax, String)] {
546546
// Member access.
547547
if let memberAccess = expression.as(MemberAccessExprSyntax.self),
548548
let base = memberAccess.base,
549549
let memberName = memberAccess.declName.simpleIdentifier?.name
550550
{
551-
return try extractImportPath(base) + [memberName]
551+
return try extractImportPath(base) + [(memberAccess.declName.baseName, memberName)]
552552
}
553553

554554
// Declaration reference.
555555
if let declRef = expression.as(DeclReferenceExprSyntax.self),
556556
let name = declRef.simpleIdentifier?.name
557557
{
558-
return [name]
558+
return [(declRef.baseName, name)]
559559
}
560560

561561
throw IfConfigDiagnostic.expectedModuleName(syntax: ExprSyntax(expression))
@@ -794,7 +794,7 @@ private struct CanImportSuppressingBuildConfiguration<Other: BuildConfiguration>
794794
return try other.hasAttribute(name: name)
795795
}
796796

797-
func canImport(importPath: [String], version: CanImportVersion) throws -> Bool {
797+
func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool {
798798
return false
799799
}
800800

0 commit comments

Comments
 (0)