expo-modules-macros 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +18 -10
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +12 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +26 -1
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +14 -1
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +259 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +151 -32
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +48 -65
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +1 -1
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/build/index.d.ts +12 -3
- package/build/index.js +18 -10
- package/build/types.d.ts +86 -19
- package/build/types.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -81,11 +81,11 @@ Node consumers can call the scanner through this package instead of locating the
|
|
|
81
81
|
```ts
|
|
82
82
|
import { scanModules, scanExports } from 'expo-modules-macros';
|
|
83
83
|
|
|
84
|
-
const { modules, warnings } = await scanModules(['ios/'], { defines: ['DEBUG'] });
|
|
84
|
+
const { modules, warnings } = await scanModules(['ios/'], { defines: ['DEBUG'], timeout: 30_000 });
|
|
85
85
|
const { exports } = await scanExports(['ios/']);
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
The binary is a compiled executable, so each call still spawns a process. What the wrapper owns is the part consumers would otherwise duplicate: resolving the shipped binary, building the arguments, parsing the JSON, checking `schemaVersion`, and turning a non-zero exit into a `ScannerError`. The result types are hand-written mirrors of the Swift `Codable` types, which is what the version check guards against drifting.
|
|
88
|
+
The binary is a compiled executable, so each call still spawns a process. What the wrapper owns is the part consumers would otherwise duplicate: resolving the shipped binary, building the arguments, parsing the JSON, checking `schemaVersion`, and turning a non-zero exit or an exceeded `timeout` into a `ScannerError`. The result types are hand-written mirrors of the Swift `Codable` types, which is what the version check guards against drifting.
|
|
89
89
|
|
|
90
90
|
# How the plugin reaches the compiler
|
|
91
91
|
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -344,7 +344,7 @@ internal func containsFunctionType(_ type: TypeSyntax) -> Bool {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
/// The first function type in `type` or anywhere inside it, in source order.
|
|
347
|
-
|
|
347
|
+
internal func firstFunctionType(in type: some SyntaxProtocol) -> FunctionTypeSyntax? {
|
|
348
348
|
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
349
349
|
return functionType
|
|
350
350
|
}
|
|
@@ -206,8 +206,8 @@ internal struct JSFunction {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
|
|
209
|
-
/// `let result = <callee>.f(...)` then the return encode (or
|
|
210
|
-
///
|
|
209
|
+
/// `let result = <callee>.f(...)` then the return encode (or just `<callee>.f(...)` for a function
|
|
210
|
+
/// that returns nothing).
|
|
211
211
|
private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
|
|
212
212
|
var lines: [String] = []
|
|
213
213
|
for index in decodingFrom..<arity {
|
|
@@ -218,7 +218,6 @@ internal struct JSFunction {
|
|
|
218
218
|
lines.append(contentsOf: encodeResultLines())
|
|
219
219
|
} else {
|
|
220
220
|
lines.append(callExpression(receiver: receiver, arity: arity))
|
|
221
|
-
lines.append("return .undefined()")
|
|
222
221
|
}
|
|
223
222
|
return lines
|
|
224
223
|
}
|
|
@@ -229,7 +228,8 @@ internal struct JSFunction {
|
|
|
229
228
|
/// `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
230
229
|
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
231
230
|
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
232
|
-
/// no-return function returns
|
|
231
|
+
/// no-return function has nothing to encode: its binding closure returns `Void` (see
|
|
232
|
+
/// `decorateStatements(object:receiver:)`).
|
|
233
233
|
///
|
|
234
234
|
/// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
|
|
235
235
|
/// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
|
|
@@ -239,7 +239,7 @@ internal struct JSFunction {
|
|
|
239
239
|
/// which always run on the JS thread — keep encoding directly without the wrapper.
|
|
240
240
|
private func encodeResultLines() -> [String] {
|
|
241
241
|
guard let returnType else {
|
|
242
|
-
return [
|
|
242
|
+
return []
|
|
243
243
|
}
|
|
244
244
|
let encode = "try \(encodeCall(returnType, freeForm: returnFreeForm, of: "result"))"
|
|
245
245
|
if isAsync {
|
|
@@ -272,11 +272,16 @@ internal struct JSFunction {
|
|
|
272
272
|
// a shorthand `{ [capture] name, name in }` parameter; for a sync binding the explicit
|
|
273
273
|
// `JavaScriptUnownedValue` also selects the (otherwise `@_disfavoredOverload`) unowned-`this`
|
|
274
274
|
// overload.
|
|
275
|
+
// A sync function that returns nothing binds through the `Void` overload, which leaves JS's
|
|
276
|
+
// result as `undefined` without building and writing a value. The closure declares `-> Void`
|
|
277
|
+
// because a body of several statements without a `return` is otherwise ambiguous between the
|
|
278
|
+
// two unowned-`this` overloads.
|
|
275
279
|
let captures = receiver.captureClause
|
|
276
280
|
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
281
|
+
let returnClause = returnType == nil && !isAsync ? " -> Void" : ""
|
|
277
282
|
|
|
278
283
|
return """
|
|
279
|
-
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
284
|
+
\(object).setProperty("\(jsName)") { \(captures)\(parameters)\(returnClause) in
|
|
280
285
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
281
286
|
}
|
|
282
287
|
"""
|
|
@@ -348,7 +353,7 @@ internal struct JSProperty {
|
|
|
348
353
|
let setDecode = "\(callee).\(swiftName) = try \(decode)"
|
|
349
354
|
lines.append(
|
|
350
355
|
accessorClosure(
|
|
351
|
-
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)
|
|
356
|
+
descriptorName, "set", receiver: receiver, returnsVoid: true, body: "\(unwrap)\(setDecode)"))
|
|
352
357
|
}
|
|
353
358
|
|
|
354
359
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
@@ -361,8 +366,10 @@ internal struct JSProperty {
|
|
|
361
366
|
|
|
362
367
|
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
|
|
363
368
|
/// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
|
|
369
|
+
/// A setter passes `returnsVoid`, so its closure declares `-> Void` and binds through the `Void`
|
|
370
|
+
/// overload, like a function that returns nothing.
|
|
364
371
|
private func accessorClosure(
|
|
365
|
-
_ descriptorName: String, _ key: String, receiver: Receiver, body: String
|
|
372
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, returnsVoid: Bool = false, body: String
|
|
366
373
|
) -> String {
|
|
367
374
|
let captures = receiver.captureClause
|
|
368
375
|
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
@@ -377,8 +384,9 @@ internal struct JSProperty {
|
|
|
377
384
|
// `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
|
|
378
385
|
// a shared object unwraps it in the body.
|
|
379
386
|
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
387
|
+
let returnClause = returnsVoid ? " -> Void" : ""
|
|
380
388
|
return """
|
|
381
|
-
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
389
|
+
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters)\(returnClause) in
|
|
382
390
|
\(indentedBody)
|
|
383
391
|
}
|
|
384
392
|
"""
|
|
@@ -445,7 +453,7 @@ internal func buildDecorateSharedObjectPhase(
|
|
|
445
453
|
}
|
|
446
454
|
|
|
447
455
|
/// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
|
|
448
|
-
/// nothing JS-visible, so the binding returns `.
|
|
456
|
+
/// nothing JS-visible, so the binding closure returns `Void`.
|
|
449
457
|
private func isVoidType(_ type: TypeSyntax?) -> Bool {
|
|
450
458
|
guard let type else {
|
|
451
459
|
return true
|
|
@@ -122,7 +122,7 @@ private struct EventMember {
|
|
|
122
122
|
/// Validates the declaration `@Event` is attached to and reads the event out of it. The checks
|
|
123
123
|
/// mirror what the expansion relies on: a single-binding instance `var` (the macro synthesizes a
|
|
124
124
|
/// computed getter, so `let`, accessors, and initializers are all incompatible) whose type is a
|
|
125
|
-
/// function type returning `Void` with at most one payload parameter.
|
|
125
|
+
/// function type returning `Void` with at most one payload parameter, which holds no closure.
|
|
126
126
|
private func validatedEvent(
|
|
127
127
|
of node: AttributeSyntax,
|
|
128
128
|
on declaration: some DeclSyntaxProtocol
|
|
@@ -169,6 +169,17 @@ private func validatedEvent(
|
|
|
169
169
|
throw MacroExpansionErrorMessage(
|
|
170
170
|
"@Event function type takes at most one payload parameter; combine multiple values into a single record")
|
|
171
171
|
}
|
|
172
|
+
// The payload is encoded to JavaScript, and a native closure can't be encoded.
|
|
173
|
+
if let payloadClosure = functionType.parameters.first.flatMap({ firstFunctionType(in: $0.type) }) {
|
|
174
|
+
throw DiagnosticsError(diagnostics: [
|
|
175
|
+
Diagnostic(
|
|
176
|
+
node: payloadClosure,
|
|
177
|
+
message: EventDiagnosticMessage(
|
|
178
|
+
"@Event payload can't hold a closure: the payload is encoded to JavaScript, and a native closure can't be encoded.",
|
|
179
|
+
id: "event-closure-payload"
|
|
180
|
+
))
|
|
181
|
+
])
|
|
182
|
+
}
|
|
172
183
|
|
|
173
184
|
let swiftName = identifier.identifier.text
|
|
174
185
|
return EventMember(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
1
2
|
import SwiftSyntax
|
|
2
3
|
import SwiftSyntaxBuilder
|
|
3
4
|
import SwiftSyntaxMacros
|
|
@@ -226,8 +227,21 @@ private func recordProperties(
|
|
|
226
227
|
"@Record properties must declare an explicit type — '\(ident.identifier.text)' has none"
|
|
227
228
|
)
|
|
228
229
|
}
|
|
229
|
-
// A literal-inferred type never mentions `Any
|
|
230
|
+
// A literal-inferred type never mentions `Any` or a closure, so only an explicit annotation can.
|
|
230
231
|
let annotatedType = binding.typeAnnotation?.type
|
|
232
|
+
// A closure has no `decode`, `encode` or dynamic type, and the record converts every property both
|
|
233
|
+
// ways. A JavaScript function decodes into a native closure only as a `@JS` argument; a native
|
|
234
|
+
// closure can't be encoded.
|
|
235
|
+
if let functionType = annotatedType.flatMap(firstFunctionType(in:)) {
|
|
236
|
+
throw DiagnosticsError(diagnostics: [
|
|
237
|
+
Diagnostic(
|
|
238
|
+
node: functionType,
|
|
239
|
+
message: RecordDiagnosticMessage(
|
|
240
|
+
"@Record property '\(ident.identifier.text)' can't hold a closure: a record property is also encoded to JavaScript, and a native closure can't be encoded",
|
|
241
|
+
id: "record-closure-property"
|
|
242
|
+
))
|
|
243
|
+
])
|
|
244
|
+
}
|
|
231
245
|
let freeForm = try annotatedType.flatMap { annotatedType -> FreeFormConversion? in
|
|
232
246
|
guard mentionsFreeFormAny(annotatedType) else {
|
|
233
247
|
return nil
|
|
@@ -545,3 +559,14 @@ private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
|
|
|
545
559
|
}
|
|
546
560
|
return true
|
|
547
561
|
}
|
|
562
|
+
|
|
563
|
+
private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
564
|
+
let message: String
|
|
565
|
+
let diagnosticID: MessageID
|
|
566
|
+
let severity: DiagnosticSeverity = .error
|
|
567
|
+
|
|
568
|
+
init(_ message: String, id: String) {
|
|
569
|
+
self.message = message
|
|
570
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -141,7 +141,8 @@ private struct UnionCase {
|
|
|
141
141
|
|
|
142
142
|
/// Reads and validates the union off the attached declaration. Every check is a compile error located
|
|
143
143
|
/// on the offending node: the macro must be on a non-generic `enum` with at least one case, and every
|
|
144
|
-
/// case must carry exactly one associated value, its payload type distinct from
|
|
144
|
+
/// case must carry exactly one associated value that holds no closure, its payload type distinct from
|
|
145
|
+
/// every earlier case's.
|
|
145
146
|
private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> UnionType {
|
|
146
147
|
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
|
|
147
148
|
throw MacroExpansionErrorMessage("@Union can only be applied to an enum")
|
|
@@ -197,6 +198,18 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
197
198
|
continue
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
// A payload is decoded from and encoded to JavaScript, and a native closure can't be encoded.
|
|
202
|
+
if let payloadClosure = firstFunctionType(in: parameter.type) {
|
|
203
|
+
diagnostics.append(
|
|
204
|
+
Diagnostic(
|
|
205
|
+
node: payloadClosure,
|
|
206
|
+
message: UnionDiagnosticMessage(
|
|
207
|
+
"@Union case '\(name)' can't carry a closure: the payload is also encoded to JavaScript, and a native closure can't be encoded.",
|
|
208
|
+
id: "union-closure-payload"
|
|
209
|
+
)))
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
let payloadType = parameter.type.trimmedDescription
|
|
201
214
|
// Alternatives decode in declaration order and the first success wins, so a case whose payload
|
|
202
215
|
// type repeats an earlier case's is unreachable: an exact-spelling duplicate is an error. (An
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import SwiftIfConfig
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
|
|
4
|
+
/// The Apple OSes `scan-exports` attributes declarations to, in the order they are reported. Spelled
|
|
5
|
+
/// as `os(...)` spells them.
|
|
6
|
+
let applePlatforms = ["iOS", "macOS", "tvOS", "watchOS", "visionOS"]
|
|
7
|
+
|
|
8
|
+
/// The platform names `@available` accepts for each reported OS, including the legacy `OSX`. Other
|
|
9
|
+
/// names (`iOSApplicationExtension`, `macCatalyst`, `swift`, …) do not change where a declaration is
|
|
10
|
+
/// built for an app, so they are ignored.
|
|
11
|
+
private let availabilityPlatformNames: [String: String] = [
|
|
12
|
+
"iOS": "iOS",
|
|
13
|
+
"macOS": "macOS",
|
|
14
|
+
"OSX": "macOS",
|
|
15
|
+
"tvOS": "tvOS",
|
|
16
|
+
"watchOS": "watchOS",
|
|
17
|
+
"visionOS": "visionOS",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/// Answers which platforms build a declaration in one file: those whose `#if` evaluation keeps it,
|
|
21
|
+
/// less those an `@available` attribute on it or on an enclosing type makes unavailable. The `#if`
|
|
22
|
+
/// regions are evaluated once per platform for the whole file. Each platform also carries the
|
|
23
|
+
/// deprecation the declaration's own `@available` attributes give it there.
|
|
24
|
+
///
|
|
25
|
+
/// A condition the scan cannot answer (`#if DEBUG`, `targetEnvironment`, a non-SDK `canImport`)
|
|
26
|
+
/// makes its region inactive on every platform, so a declaration under one is built nowhere.
|
|
27
|
+
struct PlatformResolver {
|
|
28
|
+
private let regions: [(platform: String, regions: ConfiguredRegions)]
|
|
29
|
+
|
|
30
|
+
init(tree: SourceFileSyntax) {
|
|
31
|
+
regions = applePlatforms.map { platform in
|
|
32
|
+
(platform, tree.configuredRegions(in: ScanBuildConfiguration(platform: platform, defines: [])))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// The platforms that build `node`, in `applePlatforms` order. Empty when none does.
|
|
37
|
+
func platforms(of node: some SyntaxProtocol) -> [ExportedPlatform] {
|
|
38
|
+
let availability = Availability(of: node)
|
|
39
|
+
return regions.compactMap { platform, regions in
|
|
40
|
+
guard regions.isActive(node) == .active, !availability.isUnavailable(on: platform) else {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
return ExportedPlatform(
|
|
44
|
+
name: platform,
|
|
45
|
+
introduced: availability.introduced[platform],
|
|
46
|
+
deprecated: availability.deprecation(on: platform)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// What the `@available` attributes on a declaration and its enclosing declarations say. A member
|
|
53
|
+
/// cannot be more available than the type it is declared in, so the attributes combine: a platform
|
|
54
|
+
/// any of them makes unavailable is unavailable, and the latest introduced version wins.
|
|
55
|
+
/// Deprecations are read from the declaration's own attributes only.
|
|
56
|
+
private struct Availability {
|
|
57
|
+
private var unavailable: Set<String> = []
|
|
58
|
+
private var isUnavailableEverywhere = false
|
|
59
|
+
private(set) var introduced: [String: String] = [:]
|
|
60
|
+
/// The deprecation from `@available(*, deprecated, ...)`, which applies on every platform.
|
|
61
|
+
private var deprecatedEverywhere: ExportedDeprecation?
|
|
62
|
+
/// The deprecations from attributes that name a platform.
|
|
63
|
+
private var deprecated: [String: ExportedDeprecation] = [:]
|
|
64
|
+
|
|
65
|
+
init(of node: some SyntaxProtocol) {
|
|
66
|
+
var current: Syntax? = Syntax(node)
|
|
67
|
+
var isOwn = true
|
|
68
|
+
while let decl = current {
|
|
69
|
+
if let attributes = decl.asProtocol(WithAttributesSyntax.self)?.attributes {
|
|
70
|
+
for element in attributes {
|
|
71
|
+
if let attribute = element.as(AttributeSyntax.self),
|
|
72
|
+
attribute.attributeName.trimmedDescription == "available",
|
|
73
|
+
case .availability(let arguments) = attribute.arguments {
|
|
74
|
+
read(arguments, readsDeprecation: isOwn)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
isOwn = false
|
|
79
|
+
current = decl.parent
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func isUnavailable(on platform: String) -> Bool {
|
|
84
|
+
return isUnavailableEverywhere || unavailable.contains(platform)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The deprecation on `platform`: the platform's own, with any field it leaves out taken from the
|
|
88
|
+
/// `*` one.
|
|
89
|
+
func deprecation(on platform: String) -> ExportedDeprecation? {
|
|
90
|
+
switch (deprecated[platform], deprecatedEverywhere) {
|
|
91
|
+
case (nil, let everywhere):
|
|
92
|
+
return everywhere
|
|
93
|
+
case (let own?, nil):
|
|
94
|
+
return own
|
|
95
|
+
case (let own?, let everywhere?):
|
|
96
|
+
return own.filling(from: everywhere)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Reads one attribute. The long form names one platform (or `*`) first and then what applies to
|
|
101
|
+
/// it: `@available(iOS, introduced: 16)`, `@available(tvOS, unavailable)`,
|
|
102
|
+
/// `@available(*, deprecated, renamed: "other")`. The shorthand lists platforms with versions:
|
|
103
|
+
/// `@available(iOS 17, macOS 14, *)`.
|
|
104
|
+
///
|
|
105
|
+
/// A long-form attribute deprecates its platform when it writes `deprecated` (bare or with a
|
|
106
|
+
/// version) or `obsoleted:`. `message:` and `renamed:` describe that deprecation, and are ignored
|
|
107
|
+
/// when the attribute deprecates nothing.
|
|
108
|
+
private mutating func read(_ arguments: AvailabilityArgumentListSyntax, readsDeprecation: Bool) {
|
|
109
|
+
guard case .token(let first) = arguments.first?.argument else {
|
|
110
|
+
for argument in arguments {
|
|
111
|
+
if case .availabilityVersionRestriction(let restriction) = argument.argument,
|
|
112
|
+
let platform = availabilityPlatformNames[restriction.platform.text],
|
|
113
|
+
let version = restriction.version {
|
|
114
|
+
raiseIntroduced(on: platform, to: version.trimmedDescription)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
let isWildcard = first.tokenKind == .binaryOperator("*")
|
|
120
|
+
let platform = availabilityPlatformNames[first.text]
|
|
121
|
+
guard isWildcard || platform != nil else {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
var deprecation = ExportedDeprecation()
|
|
125
|
+
var isDeprecated = false
|
|
126
|
+
for argument in arguments.dropFirst() {
|
|
127
|
+
switch argument.argument {
|
|
128
|
+
case .token(let token) where token.text == "unavailable":
|
|
129
|
+
if let platform {
|
|
130
|
+
unavailable.insert(platform)
|
|
131
|
+
} else {
|
|
132
|
+
isUnavailableEverywhere = true
|
|
133
|
+
}
|
|
134
|
+
case .token(let token) where token.text == "deprecated":
|
|
135
|
+
isDeprecated = true
|
|
136
|
+
case .availabilityLabeledArgument(let labeled):
|
|
137
|
+
switch (labeled.label.text, labeled.value) {
|
|
138
|
+
case ("introduced", .version(let version)):
|
|
139
|
+
if let platform {
|
|
140
|
+
raiseIntroduced(on: platform, to: version.trimmedDescription)
|
|
141
|
+
}
|
|
142
|
+
case ("deprecated", .version(let version)):
|
|
143
|
+
isDeprecated = true
|
|
144
|
+
deprecation.since = version.trimmedDescription
|
|
145
|
+
case ("obsoleted", .version(let version)):
|
|
146
|
+
isDeprecated = true
|
|
147
|
+
deprecation.obsoleted = version.trimmedDescription
|
|
148
|
+
case ("message", .string(let literal)):
|
|
149
|
+
deprecation.message = decodedText(of: literal)
|
|
150
|
+
case ("renamed", .string(let literal)):
|
|
151
|
+
deprecation.renamed = decodedText(of: literal)
|
|
152
|
+
default:
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
guard readsDeprecation, isDeprecated else {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if let platform {
|
|
163
|
+
deprecated[platform] = deprecated[platform].map { deprecation.filling(from: $0) } ?? deprecation
|
|
164
|
+
} else {
|
|
165
|
+
deprecatedEverywhere = deprecatedEverywhere.map { deprecation.filling(from: $0) } ?? deprecation
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private mutating func raiseIntroduced(on platform: String, to version: String) {
|
|
170
|
+
if let current = introduced[platform], !isVersion(version, laterThan: current) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
introduced[platform] = version
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
extension ExportedDeprecation {
|
|
178
|
+
/// This deprecation with each field it leaves out taken from `other`.
|
|
179
|
+
fileprivate func filling(from other: ExportedDeprecation) -> ExportedDeprecation {
|
|
180
|
+
return ExportedDeprecation(
|
|
181
|
+
since: since ?? other.since,
|
|
182
|
+
obsoleted: obsoleted ?? other.obsoleted,
|
|
183
|
+
message: message ?? other.message,
|
|
184
|
+
renamed: renamed ?? other.renamed
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// The value of a string literal in an attribute, with its escape sequences decoded: `\"` reads as
|
|
190
|
+
/// `"`, `\n` as a line break, and `\u{2192}` as `→`. An escaped line break in a multi-line literal
|
|
191
|
+
/// joins the lines, as in Swift. An escape that isn't valid is kept as written.
|
|
192
|
+
private func decodedText(of literal: SimpleStringLiteralExprSyntax) -> String {
|
|
193
|
+
let text = literal.segments.map(\.content.text).joined()
|
|
194
|
+
var result = ""
|
|
195
|
+
var index = text.startIndex
|
|
196
|
+
while index < text.endIndex {
|
|
197
|
+
let character = text[index]
|
|
198
|
+
index = text.index(after: index)
|
|
199
|
+
guard character == "\\", index < text.endIndex else {
|
|
200
|
+
result.append(character)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
let escaped = text[index]
|
|
204
|
+
index = text.index(after: index)
|
|
205
|
+
switch escaped {
|
|
206
|
+
case "0":
|
|
207
|
+
result.append("\0")
|
|
208
|
+
case "n":
|
|
209
|
+
result.append("\n")
|
|
210
|
+
case "r":
|
|
211
|
+
result.append("\r")
|
|
212
|
+
case "t":
|
|
213
|
+
result.append("\t")
|
|
214
|
+
case "\"", "'", "\\":
|
|
215
|
+
result.append(escaped)
|
|
216
|
+
case "\n", "\r\n":
|
|
217
|
+
continue
|
|
218
|
+
case "u":
|
|
219
|
+
if text[index...].hasPrefix("{"), let close = text[index...].firstIndex(of: "}"),
|
|
220
|
+
let scalarValue = UInt32(text[text.index(after: index)..<close], radix: 16),
|
|
221
|
+
let scalar = Unicode.Scalar(scalarValue) {
|
|
222
|
+
result.unicodeScalars.append(scalar)
|
|
223
|
+
index = text.index(after: close)
|
|
224
|
+
} else {
|
|
225
|
+
result.append("\\u")
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
result.append("\\")
|
|
229
|
+
result.append(escaped)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return result
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Compares two dotted version spellings component by component, treating a missing component as
|
|
236
|
+
/// `0`, so `17` and `17.0` are equal.
|
|
237
|
+
private func isVersion(_ lhs: String, laterThan rhs: String) -> Bool {
|
|
238
|
+
let left = lhs.split(separator: ".").map { Int($0) ?? 0 }
|
|
239
|
+
let right = rhs.split(separator: ".").map { Int($0) ?? 0 }
|
|
240
|
+
for index in 0..<max(left.count, right.count) {
|
|
241
|
+
let l = index < left.count ? left[index] : 0
|
|
242
|
+
let r = index < right.count ? right[index] : 0
|
|
243
|
+
if l != r {
|
|
244
|
+
return l > r
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// The spelled access modifier of a declaration (`open`, `public`, `package`, `internal`,
|
|
251
|
+
/// `fileprivate`, `private`), or `nil` when none is written. A modifier with a detail, like the
|
|
252
|
+
/// `private` in `private(set)`, limits only the setter, so it is not the declaration's.
|
|
253
|
+
func spelledAccessLevel(of modifiers: DeclModifierListSyntax) -> String? {
|
|
254
|
+
return modifiers.first { modifier in
|
|
255
|
+
modifier.detail == nil && accessModifierNames.contains(modifier.name.text)
|
|
256
|
+
}?.name.text
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private let accessModifierNames: Set<String> = ["open", "public", "package", "internal", "fileprivate", "private"]
|