13
13
import SwiftDiagnostics
14
14
import SwiftSyntax
15
15
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
+
16
100
extension SyntaxProtocol {
17
101
/// Find all of the #if/#elseif/#else clauses within the given syntax node,
18
102
/// indicating their active state. This operation will recurse into all
@@ -39,10 +123,13 @@ extension SyntaxProtocol {
39
123
/// - Inactive region for the final `#else`.
40
124
public func configuredRegions(
41
125
in configuration: some BuildConfiguration
42
- ) -> [ ( IfConfigClauseSyntax , IfConfigRegionState ) ] {
126
+ ) -> ConfiguredRegions {
43
127
let visitor = ConfiguredRegionVisitor ( configuration: configuration)
44
128
visitor. walk ( self )
45
- return visitor. regions
129
+ return ConfiguredRegions (
130
+ regions: visitor. regions,
131
+ diagnostics: visitor. diagnostics
132
+ )
46
133
}
47
134
}
48
135
@@ -56,58 +143,111 @@ fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: Sy
56
143
/// Whether we are currently within an active region.
57
144
var inActiveRegion = true
58
145
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
+
59
152
init ( configuration: Configuration ) {
60
153
self . configuration = configuration
61
154
super. init ( viewMode: . sourceAccurate)
62
155
}
63
156
64
157
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.
68
166
var foundActive = false
69
167
var syntaxErrorsAllowed = false
168
+ let outerState : IfConfigRegionState = inActiveRegion ? . active : . inactive
70
169
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)
79
186
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
+ }
84
197
85
- regions. append ( ( clause, . active) )
198
+ if hadError {
199
+ isActive = false
200
+ syntaxErrorsAllowed = true
201
+ } else {
202
+ isActive = thisIsActive
86
203
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
89
217
}
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
+ }
90
223
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
93
230
}
94
231
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) )
99
235
}
100
236
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 {
104
239
let priorInActiveRegion = inActiveRegion
105
- inActiveRegion = false
240
+ inActiveRegion = isActive
106
241
defer {
107
242
inActiveRegion = priorInActiveRegion
108
243
}
109
244
walk ( elements)
110
245
}
246
+
247
+ // Note when we found an active clause.
248
+ if isActive {
249
+ foundActive = true
250
+ }
111
251
}
112
252
113
253
return . skipChildren
0 commit comments