File: ConfiguredRegions.swift

package info (click to toggle)
swiftlang 6.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 2,791,532 kB
  • sloc: cpp: 9,901,743; ansic: 2,201,431; asm: 1,091,827; python: 308,252; objc: 82,166; f90: 80,126; lisp: 38,358; pascal: 25,559; sh: 20,429; ml: 5,058; perl: 4,745; makefile: 4,484; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (305 lines) | stat: -rw-r--r-- 11,256 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftDiagnostics
import SwiftSyntax

/// Describes all of the #if/#elseif/#else clauses within the given syntax node,
/// indicating their active state. This operation will recurse into all
/// clauses to indicate regions of active / inactive / unparsed code.
///
/// For example, given code like the following:
/// #if DEBUG
///   #if A
///     func f()
///   #elseif B
///     func g()
///   #elseif compiler(>= 12.0)
///   please print the number after 41
///   #endif
/// #else
/// #endif
///
/// If the configuration options `DEBUG` and `B` are provided, but `A` is not,
/// and the compiler version is less than 12.0, the results will be contain:
///   - Active region for the `#if DEBUG`.
///   - Inactive region for the `#if A`.
///   - Active region for the `#elseif B`.
///   - Unparsed region for the `#elseif compiler(>= 12.0)`.
///   - Inactive region for the final `#else`.
public struct ConfiguredRegions {
  let regions: [(ifClause: IfConfigClauseSyntax, state: IfConfigRegionState)]

  // A mapping from each of the #if declarations that have been evaluated to
  // the active clause. Absence from this map means that there is no active
  // clause, either because every clause failed or because the entire #if
  // itself is inactive.
  var activeClauses: [IfConfigDeclSyntax: IfConfigClauseSyntax]

  /// The set of diagnostics produced when evaluating the configured regions.
  public let diagnostics: [Diagnostic]

  /// Determine whether the given syntax node is active within the configured
  /// regions.
  ///
  /// Any given node within the range of configured regions can either be
  /// "active" (it is part of the program), "inactive" (it is not part of the
  /// program), or "unparsed" (it is not part of the program and shouldn't
  /// produce any syntax errors).
  ///
  /// This operation takes time that is logarthmic in the number of regions
  /// in the syntax tree.
  public func isActive(_ node: some SyntaxProtocol) -> IfConfigRegionState {
    // Find the slice of the regions in which this node lands.
    var currentSlice = regions[...]
    while !currentSlice.isEmpty {
      let middle = currentSlice.startIndex + currentSlice.count / 2

      // If the node is prior to the start of the middle, take the left-hand side.
      if node.position < currentSlice[middle].ifClause.regionStart {
        currentSlice = currentSlice[..<middle]
        continue
      }

      // If the node is after the end of the middle, take the right-hand side.
      if node.position > currentSlice[middle].ifClause.endPosition {
        currentSlice = currentSlice[(middle + 1)...]
        continue
      }

      // We cannot narrow the range any further.
      break
    }

    // Find the last region in which this node lands. If there is no such
    // region, this is active.
    return currentSlice.last { region in
      (region.ifClause.regionStart...region.ifClause.endPosition).contains(node.position)
    }?.state ?? .active
  }

  /// Determine which clause of an `#if` declaration was active within this
  /// set of configured regions.
  ///
  /// A particular `#if` declaration might have no active clause (e.g., this
  /// operation will return a `nil`) if either none of the clauses had
  /// conditions that succeeded, or the `#if` declaration itself is within an
  /// inactive (or unparsed) region and therefore cannot have an active clause.
  public func activeClause(for node: IfConfigDeclSyntax) -> IfConfigClauseSyntax? {
    return activeClauses[node]
  }
}

extension ConfiguredRegions: RandomAccessCollection {
  public typealias Element = (ifClause: IfConfigClauseSyntax, state: IfConfigRegionState)
  public var startIndex: Int { regions.startIndex }
  public var endIndex: Int { regions.endIndex }

  public subscript(index: Int) -> Element {
    regions[index]
  }
}

extension ConfiguredRegions: CustomDebugStringConvertible {
  /// Provides source ranges for each of the configured regions.
  public var debugDescription: String {
    guard let firstRegion = first else {
      return "[]"
    }

    let root = firstRegion.ifClause.root
    let converter = SourceLocationConverter(fileName: "", tree: root)
    let regionDescriptions = regions.map { (ifClause, state) in
      let startPosition = converter.location(for: ifClause.position)
      let endPosition = converter.location(for: ifClause.endPosition)
      return "[\(startPosition.line):\(startPosition.column) - \(endPosition.line):\(endPosition.column)] = \(state)"
    }

    return "[\(regionDescriptions.joined(separator: ", ")))]"
  }
}

extension IfConfigClauseSyntax {
  /// The effective start of the region after which code is subject to its
  /// condition.
  fileprivate var regionStart: AbsolutePosition {
    condition?.endPosition ?? elements?._syntaxNode.position ?? poundKeyword.endPosition
  }
}

extension SyntaxProtocol {
  /// Find all of the #if/#elseif/#else clauses within the given syntax node,
  /// indicating their active state. This operation will recurse into all
  /// clauses to indicate regions of active / inactive / unparsed code.
  ///
  /// For example, given code like the following:
  /// #if DEBUG
  ///   #if A
  ///     func f()
  ///   #elseif B
  ///     func g()
  ///   #elseif compiler(>= 12.0)
  ///   please print the number after 41
  ///   #endif
  /// #else
  /// #endif
  ///
  /// If the configuration options `DEBUG` and `B` are provided, but `A` is not,
  /// and the compiler version is less than 12.0, the results will be contain:
  ///   - Active region for the `#if DEBUG`.
  ///   - Inactive region for the `#if A`.
  ///   - Active region for the `#elseif B`.
  ///   - Unparsed region for the `#elseif compiler(>= 12.0)`.
  ///   - Inactive region for the final `#else`.
  public func configuredRegions(
    in configuration: some BuildConfiguration
  ) -> ConfiguredRegions {
    let visitor = ConfiguredRegionVisitor(configuration: configuration)
    visitor.walk(self)
    return ConfiguredRegions(
      regions: visitor.regions,
      activeClauses: visitor.activeClauses,
      diagnostics: visitor.diagnostics
    )
  }
}

/// Helper class that walks a syntax tree looking for configured regions.
fileprivate class ConfiguredRegionVisitor<Configuration: BuildConfiguration>: SyntaxVisitor {
  let configuration: Configuration

  /// The regions we've found so far.
  var regions: [(IfConfigClauseSyntax, IfConfigRegionState)] = []

  /// Whether we are currently within an active region.
  var inActiveRegion = true

  /// Whether we are currently within an #if at all.
  var inAnyIfConfig = false

  // All diagnostics encountered along the way.
  var diagnostics: [Diagnostic] = []

  // A mapping from each of the #if declarations that have been evaluated to
  // the active clause. Absence from this map means that there is no active
  // clause, either because every clause failed or because the entire #if
  // itself is inactive.
  var activeClauses: [IfConfigDeclSyntax: IfConfigClauseSyntax] = [:]

  init(configuration: Configuration) {
    self.configuration = configuration
    super.init(viewMode: .sourceAccurate)
  }

  override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
    // We are in an #if.
    let priorInAnyIfConfig = inAnyIfConfig
    inAnyIfConfig = true
    defer {
      inAnyIfConfig = priorInAnyIfConfig
    }

    // Walk through the clauses to find the active one.
    var foundActive = false
    var syntaxErrorsAllowed = false
    let outerState: IfConfigRegionState = inActiveRegion ? .active : .inactive
    for clause in node.clauses {
      let isActive: Bool
      if let condition = clause.condition {
        if !foundActive {
          // Fold operators so we can evaluate this #if condition.
          let (foldedCondition, foldDiagnostics) = IfConfigClauseSyntax.foldOperators(condition)
          diagnostics.append(contentsOf: foldDiagnostics)

          // In an active region, evaluate the condition to determine whether
          // this clause is active. Otherwise, this clause is inactive.
          if inActiveRegion {
            let (thisIsActive, _, evalDiagnostics) = evaluateIfConfig(
              condition: foldedCondition,
              configuration: configuration
            )
            diagnostics.append(contentsOf: evalDiagnostics)

            // Determine if there was an error that prevented us from
            // evaluating the condition. If so, we'll allow syntax errors
            // from here on out.
            let hadError =
              foldDiagnostics.contains { diag in
                diag.diagMessage.severity == .error
              }
              || evalDiagnostics.contains { diag in
                diag.diagMessage.severity == .error
              }

            if hadError {
              isActive = false
              syntaxErrorsAllowed = true
            } else {
              isActive = thisIsActive

              // Determine whether syntax errors are allowed.
              syntaxErrorsAllowed = foldedCondition.allowsSyntaxErrorsFolded
            }
          } else {
            isActive = false

            // Determine whether syntax errors are allowed, even though we
            // skipped evaluation of the actual condition.
            syntaxErrorsAllowed = foldedCondition.allowsSyntaxErrorsFolded
          }
        } else {
          // We already found an active condition, so this is inactive.
          isActive = false
        }
      } else {
        // This is an #else. It's active if we haven't found an active clause
        // yet and are in an active region.
        isActive = !foundActive && inActiveRegion
      }

      // If this is the active clause, record it as such.
      if isActive {
        activeClauses[node] = clause
      }

      // Determine and record the current state.
      let currentState: IfConfigRegionState
      switch (isActive, syntaxErrorsAllowed) {
      case (true, _): currentState = .active
      case (false, false): currentState = .inactive
      case (false, true): currentState = .unparsed
      }

      // If there is a state change, record it.
      if !priorInAnyIfConfig || currentState != .inactive || currentState != outerState {
        regions.append((clause, currentState))
      }

      // If this is a parsed region, recurse into it.
      if currentState != .unparsed, let elements = clause.elements {
        let priorInActiveRegion = inActiveRegion
        inActiveRegion = isActive
        defer {
          inActiveRegion = priorInActiveRegion
        }
        walk(elements)
      }

      // Note when we found an active clause.
      if isActive {
        foundActive = true
      }
    }

    return .skipChildren
  }
}