1313import SwiftDiagnostics
1414import 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+
16100extension 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
0 commit comments