File: AvailabilityGuards.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (259 lines) | stat: -rw-r--r-- 8,966 bytes parent folder | download
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
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 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 Swift project authors
//

import SwiftSyntax
import SwiftSyntaxMacros

/// A structure describing a single platform/version pair from an `@available()`
/// attribute.
struct Availability {
  /// The attribute from which this instance was generated.
  var attribute: AttributeSyntax

  /// The platform name, such as `"macOS"`, if any.
  var platformName: TokenSyntax?

  /// The platform version, such as 1.2.3, if any.
  var version: VersionTupleSyntax?

  /// The `message` argument to the attribute, if any.
  var message: SimpleStringLiteralExprSyntax?

  /// An instance of `PlatformVersionSyntax` representing the same availability
  /// as this instance, if this instance can be represented as an instance of
  /// that type.
  var platformVersion: PlatformVersionSyntax? {
    platformName.map { platformName in
      PlatformVersionSyntax(
        platform: platformName.trimmed.with(\.trailingTrivia, .space),
        version: version?.trimmed
      )
    }
  }

  /// Whether or not this instance represents Swift language availability.
  var isSwift: Bool {
    platformName?.textWithoutBackticks == "swift"
  }
}

// MARK: -

/// Create an expression that contains a test trait for availability (i.e.
/// `.enabled(if: ...)`) for a given availability version.
///
/// - Parameters:
///   - availability: The value to convert to a trait. The `platformName`
///     property of this value must not be `nil`, but the `version` property may
///     be `nil`.
///   - whenKeyword: The keyword that controls how `availability` is
///     interpreted. Pass either `.introduced` or `.obsoleted`.
///   - context: The macro context in which the expression is being parsed.
///
/// - Returns: An instance of `ExprSyntax` representing an instance of
///   ``Trait`` that can be used to prevent a test from running if the
///   availability constraint in `availability` is not met.
private func _createAvailabilityTraitExpr(
  from availability: Availability,
  when whenKeyword: Keyword,
  in context: some MacroExpansionContext
) -> ExprSyntax {
  let version: ExprSyntax = availability.version.map(\.componentValues).map { components in
    "(\(literal: components.major), \(literal: components.minor), \(literal: components.patch))"
  } ?? "nil"
  let message = availability.message.map(\.trimmed).map(ExprSyntax.init) ?? "nil"
  let sourceLocationExpr = createSourceLocationExpr(of: availability.attribute, context: context)

  switch (whenKeyword, availability.isSwift) {
  case (.introduced, false):
    return """
    .__available(\(literal: availability.platformName!.textWithoutBackticks), introduced: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) {
      if #available(\(availability.platformVersion!), *) {
        return true
      }
      return false
    }
    """

  case (.obsoleted, false):
    return """
    .__available(\(literal: availability.platformName!.textWithoutBackticks), obsoleted: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) {
      if #unavailable(\(availability.platformVersion!)) {
        return true
      }
      return false
    }
    """

  case (.introduced, true):
    return """
    .__available("Swift", introduced: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) {
      #if swift(>=\(availability.version!))
      return true
      #else
      return false
      #endif
    }
    """

  case (.obsoleted, true):
    return """
    .__available("Swift", obsoleted: \(version), message: \(message), sourceLocation: \(sourceLocationExpr)) {
      #if swift(<\(availability.version!))
      return true
      #else
      return false
      #endif
    }
    """

  case (.unavailable, _):
    return ".__unavailable(message: \(message), sourceLocation: \(sourceLocationExpr))"

  default:
    fatalError("Unsupported keyword \(whenKeyword) passed to \(#function)")
  }
}

/// Create an expression that contains test traits for availability (i.e.
/// `.enabled(if: ...)`).
///
/// - Parameters:
///   - decl: The expression annotated with availability attributes.
///   - context: The macro context in which the expression is being parsed.
///
/// - Returns: An array of expressions producing ``Trait`` instances that can be
///   used to prevent a test from running if any availability constraints are
///   not met. If `decl` has no `@available` attributes, an empty array is
///   returned.
func createAvailabilityTraitExprs(
  for decl: some WithAttributesSyntax,
  in context: some MacroExpansionContext
) -> [ExprSyntax] {
  var result = [ExprSyntax]()

  result += decl.availability(when: .unavailable).lazy.map { unavailability in
    _createAvailabilityTraitExpr(from: unavailability, when: .unavailable, in: context)
  }

  result += decl.availability(when: .introduced).lazy.map { availability in
    _createAvailabilityTraitExpr(from: availability, when: .introduced, in: context)
  }

  result += decl.availability(when: .obsoleted).lazy.map { availability in
    _createAvailabilityTraitExpr(from: availability, when: .obsoleted, in: context)
  }

  return result
}

/// Create a syntax node that checks for availability based on a declaration
/// and, either invokes some other syntax node or exits early.
///
/// - Parameters:
///   - decl: The declaration annotated with availability attributes.
///   - node: The node to evaluate if `decl` is available at runtime. This node
///     may be any arbitrary syntax.
///   - exitStatement: The scope-exiting statement to evaluate if `decl` is
///     unavailable at runtime. The default expression is `return`.
///   - context: The macro context in which the expression is being parsed.
///
/// - Returns: A syntax node containing one or more `guard`, `if`, and/or `#if`
///   statements that test availability based on the attributes on `decl` and
///   exit early with `exitStatement` if any availability constraints are not
///   met. If `decl` has no `@available` attributes, a copy of `node` is
///   returned.
func createSyntaxNode(
  guardingForAvailabilityOf decl: some DeclSyntaxProtocol & WithAttributesSyntax,
  beforePerforming node: some SyntaxProtocol,
  orExitingWith exitStatement: StmtSyntax = StmtSyntax(ReturnStmtSyntax()),
  in context: some MacroExpansionContext
) -> CodeBlockItemListSyntax {
  var result: CodeBlockItemListSyntax = "\(node)"

  // Create an expression that acts as an availability guard (i.e.
  // `guard #available(...) else { ... }`). The expression is evaluated before
  // `node` to allow for early exit.
  do {
    let availableExprs: [ExprSyntax] = decl.availability(when: .introduced).lazy
      .filter { !$0.isSwift }
      .compactMap(\.platformVersion)
      .map { "#available(\($0), *)" }
    if !availableExprs.isEmpty {
      let conditionList = ConditionElementListSyntax {
        for availableExpr in availableExprs {
          availableExpr
        }
      }
      result = """
      guard \(conditionList) else {
        \(exitStatement)
      }
      \(result)
      """
    }
  }

  // As above, but for unavailability (`#unavailable(...)`.)
  do {
    let unavailableExprs: [ExprSyntax] = decl.availability(when: .obsoleted).lazy
      .filter { !$0.isSwift }
      .compactMap(\.platformVersion)
      .map { "#unavailable(\($0))" }
    if !unavailableExprs.isEmpty {
      let conditionList = ConditionElementListSyntax {
        for unavailableExpr in unavailableExprs {
          unavailableExpr
        }
      }
      result = """
      guard \(conditionList) else {
        \(exitStatement)
      }
      \(result)
      """
    }
  }

  // If this function has a minimum or maximum Swift version requirement, we
  // need to scope its body with #if/#endif.
  do {
    let introducedVersion = decl.availability(when: .introduced).lazy
      .filter(\.isSwift)
      .compactMap(\.version?.componentValues)
      .max()
    let obsoletedVersion = decl.availability(when: .obsoleted).lazy
      .filter(\.isSwift)
      .compactMap(\.version?.componentValues)
      .min()

    let swiftVersionGuardExpr: ExprSyntax? = switch (introducedVersion, obsoletedVersion) {
    case let (.some(introducedVersion), .some(obsoletedVersion)):
      "swift(>=\(raw: introducedVersion)) && swift(<\(raw: obsoletedVersion))"
    case let (.some(introducedVersion), _):
      "swift(>=\(raw: introducedVersion))"
    case let (_, .some(obsoletedVersion)):
      "swift(<\(raw: obsoletedVersion))"
    default:
      nil
    }
    if let swiftVersionGuardExpr {
      result = """
      #if \(swiftVersionGuardExpr)
      \(result)
      #else
      \(exitStatement)
      #endif
      """
    }
  }

  return result
}