expo-modules-macros 0.18.0 → 0.19.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/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +4 -3
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +15 -7
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +101 -52
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +42 -86
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +19 -7
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -50
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +5 -4
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +19 -11
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +101 -67
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +99 -27
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +3 -1
- package/build/types.d.ts +18 -3
- package/build/types.js +1 -1
- package/package.json +3 -3
|
@@ -158,7 +158,8 @@ struct ExportedEvent: Encodable, Equatable {
|
|
|
158
158
|
|
|
159
159
|
/// A `@ExpoModule` type and its `@JS` surface.
|
|
160
160
|
struct ExportedModule: Encodable, Equatable {
|
|
161
|
-
/// The Swift class name.
|
|
161
|
+
/// The Swift class name. A module is always declared at file scope, since autolinking registers no
|
|
162
|
+
/// other.
|
|
162
163
|
let name: String
|
|
163
164
|
|
|
164
165
|
/// The JS module name: `@ExpoModule("Foo")` override, else the class name.
|
|
@@ -178,7 +179,9 @@ struct ExportedModule: Encodable, Equatable {
|
|
|
178
179
|
|
|
179
180
|
/// One entry of `@ExpoModule(classes: [...])`.
|
|
180
181
|
struct ExportedClass: Encodable, Equatable {
|
|
181
|
-
/// The
|
|
182
|
+
/// The `name` of the matching `@SharedObject`, which is qualified when the class is nested
|
|
183
|
+
/// (`Outer.Statement`). The Swift name as spelled in the argument when the scan found no
|
|
184
|
+
/// `@SharedObject` with this name.
|
|
182
185
|
let name: String
|
|
183
186
|
|
|
184
187
|
/// The name of the module member JS reads the class from: the matching `@SharedObject`'s `jsName`.
|
|
@@ -189,10 +192,11 @@ struct ExportedClass: Encodable, Equatable {
|
|
|
189
192
|
|
|
190
193
|
/// A `@SharedObject` type: a JS class with an optional `@JS init` constructor plus its `@JS` members.
|
|
191
194
|
struct ExportedSharedObject: Encodable, Equatable {
|
|
192
|
-
/// The Swift class name.
|
|
195
|
+
/// The Swift class name, qualified with the types it is nested in (`Outer.Player`).
|
|
193
196
|
let name: String
|
|
194
197
|
|
|
195
|
-
/// The JS class name: `@SharedObject("Foo")` override, else the class name
|
|
198
|
+
/// The JS class name: `@SharedObject("Foo")` override, else the class name without the enclosing
|
|
199
|
+
/// types.
|
|
196
200
|
let jsName: String
|
|
197
201
|
|
|
198
202
|
/// The `@JS init` parameters, or `nil` when there's none. A shared object has at most one.
|
|
@@ -207,7 +211,8 @@ struct ExportedSharedObject: Encodable, Equatable {
|
|
|
207
211
|
|
|
208
212
|
/// A `@Record` type and its properties (data only: no functions, accessors, or constructor).
|
|
209
213
|
struct ExportedRecord: Encodable, Equatable {
|
|
210
|
-
/// The Swift type name (struct or class)
|
|
214
|
+
/// The Swift type name (struct or class), qualified with the types it is nested in
|
|
215
|
+
/// (`Outer.Options`).
|
|
211
216
|
let name: String
|
|
212
217
|
|
|
213
218
|
let properties: [ExportedRecordProperty]
|
|
@@ -247,8 +252,9 @@ struct ExportedEnumCase: Encodable, Equatable {
|
|
|
247
252
|
/// Detected by conformance, not by a macro attribute, because core converts it through its
|
|
248
253
|
/// `RawRepresentable`/`Enumerable` conformance (`Coding/…+Enumerable`) with no macro involved.
|
|
249
254
|
struct ExportedEnum: Encodable, Equatable {
|
|
250
|
-
/// The Swift enum name
|
|
251
|
-
/// reaches JS as its raw values, not under
|
|
255
|
+
/// The Swift enum name, qualified with the types it is nested in (`Options.Encoding`). No `jsName`:
|
|
256
|
+
/// an enum carries no macro to spell an override on, and it reaches JS as its raw values, not under
|
|
257
|
+
/// a bound name.
|
|
252
258
|
let name: String
|
|
253
259
|
|
|
254
260
|
/// The raw value type as written (`String`, `Int`, …), or `nil` for a bare `Enumerable` conformance
|
|
@@ -296,8 +302,9 @@ struct ExportedUnionMember: Encodable, Equatable {
|
|
|
296
302
|
/// `Double`, two compatible records) the earlier case wins. Reordering them describes a different
|
|
297
303
|
/// union than the one the module runs.
|
|
298
304
|
struct ExportedUnion: Encodable, Equatable {
|
|
299
|
-
/// The Swift enum name
|
|
300
|
-
/// payload types rather than under a bound
|
|
305
|
+
/// The Swift enum name, qualified with the types it is nested in (`Options.Value`). No `jsName`:
|
|
306
|
+
/// `@Union` takes no arguments, and a union reaches JS as its payload types rather than under a bound
|
|
307
|
+
/// name.
|
|
301
308
|
let name: String
|
|
302
309
|
|
|
303
310
|
let members: [ExportedUnionMember]
|
|
@@ -320,8 +327,9 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
320
327
|
/// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
|
|
321
328
|
/// and change for different reasons. Version 2 added `events` to modules and shared objects; version 3
|
|
322
329
|
/// added `enums`; version 4 added `unions`; version 5 resolves refs, adding `refKind` and correcting
|
|
323
|
-
/// an enum ref's `typeof`; version 6 added `classes` to modules
|
|
324
|
-
|
|
330
|
+
/// an enum ref's `typeof`; version 6 added `classes` to modules; version 7 reports nested types under
|
|
331
|
+
/// qualified names and gives a resolved ref the name of its declaration.
|
|
332
|
+
let scanExportsSchemaVersion = 7
|
|
325
333
|
|
|
326
334
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
327
335
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -5,65 +5,88 @@
|
|
|
5
5
|
/// enum, or nothing at all. Resolution is therefore a second pass over the collected surface, run once
|
|
6
6
|
/// every file has been walked.
|
|
7
7
|
///
|
|
8
|
-
///
|
|
8
|
+
/// A name is looked up the way Swift looks it up: from the type the use site is written in, outward to
|
|
9
|
+
/// file scope. In `Options`, `Encoding` names `Options.Encoding` when the scan declares one, else a
|
|
10
|
+
/// file-scope `Encoding`.
|
|
11
|
+
///
|
|
12
|
+
/// Three things change on a resolved ref:
|
|
13
|
+
/// - `name` becomes the declaration's qualified name, so a consumer can match it to the declaration
|
|
14
|
+
/// without repeating the scoped lookup.
|
|
9
15
|
/// - `refKind` names which kind declares it, so a consumer doesn't repeat this lookup.
|
|
10
16
|
/// - the `typeof` category is corrected for an enum, which crosses as its raw value rather than as an
|
|
11
17
|
/// object. That correction is the reason this can't be left to the consumer: the scanner is the only
|
|
12
18
|
/// side that knows the enum's `rawType`.
|
|
13
19
|
///
|
|
14
|
-
/// A name the scan never declared keeps `refKind: nil
|
|
15
|
-
/// a platform convertible (`CGPoint`, `URL`) or a type from another module, and the
|
|
16
|
-
/// catalog decides.
|
|
20
|
+
/// A name the scan never declared keeps its spelling, `refKind: nil`, and its `object` category. Not
|
|
21
|
+
/// an error: it is a platform convertible (`CGPoint`, `URL`) or a type from another module, and the
|
|
22
|
+
/// consumer's own catalog decides.
|
|
17
23
|
|
|
18
24
|
/// The declared names of a scanned surface, each mapped to how it should be reported at a use site.
|
|
19
25
|
/// Built once per scan and used for every ref lookup.
|
|
20
26
|
struct RefIndex {
|
|
21
|
-
///
|
|
22
|
-
|
|
27
|
+
/// The declaration a ref resolves to.
|
|
28
|
+
struct Entry: Equatable {
|
|
29
|
+
/// The declaration's qualified name.
|
|
30
|
+
let name: String
|
|
31
|
+
let kind: RefKind
|
|
32
|
+
/// The JS category a ref to the declaration crosses as.
|
|
33
|
+
let jsType: JSType
|
|
34
|
+
/// A `@SharedObject`'s JS name, `nil` for every other kind.
|
|
35
|
+
var jsName: String? = nil
|
|
36
|
+
}
|
|
23
37
|
|
|
24
|
-
/// A
|
|
25
|
-
private var
|
|
38
|
+
/// A declaration's qualified name -> its entry.
|
|
39
|
+
private var entries: [String: Entry] = [:]
|
|
26
40
|
|
|
27
41
|
init(surface: ExportedSurface) {
|
|
28
42
|
for record in surface.records {
|
|
29
|
-
entries[record.name] = (.record, .object)
|
|
43
|
+
entries[record.name] = Entry(name: record.name, kind: .record, jsType: .object)
|
|
30
44
|
}
|
|
31
45
|
for sharedObject in surface.sharedObjects {
|
|
32
|
-
entries[sharedObject.name] = (
|
|
33
|
-
|
|
46
|
+
entries[sharedObject.name] = Entry(
|
|
47
|
+
name: sharedObject.name, kind: .sharedObject, jsType: .object, jsName: sharedObject.jsName)
|
|
34
48
|
}
|
|
35
49
|
for union in surface.unions {
|
|
36
50
|
// A union crosses as whichever alternative matched, so its category is only meaningful per
|
|
37
51
|
// member. `object` is the honest coarse answer; the consumer reads `members` for the real shape.
|
|
38
|
-
entries[union.name] = (.union, .object)
|
|
52
|
+
entries[union.name] = Entry(name: union.name, kind: .union, jsType: .object)
|
|
39
53
|
}
|
|
40
54
|
for enumeration in surface.enums {
|
|
41
|
-
entries[enumeration.name] = (
|
|
55
|
+
entries[enumeration.name] = Entry(
|
|
56
|
+
name: enumeration.name, kind: .enum, jsType: jsType(ofRawType: enumeration.rawType))
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
///
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
///
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
/// The declaration a ref to `name` written inside the type `scope` resolves to, or `nil` when the
|
|
61
|
+
/// scan declares no such type. `scope` is the qualified name of the type the use site is written
|
|
62
|
+
/// in.
|
|
63
|
+
///
|
|
64
|
+
/// The name is first looked up in `scope` and then in each enclosing scope out to file scope, so the
|
|
65
|
+
/// innermost declaration wins, as in Swift. A leading `Self.` names `scope` itself, so it is dropped
|
|
66
|
+
/// before the lookup. When that finds nothing, the leading components of a qualified spelling are
|
|
67
|
+
/// dropped one at a time (`Media.Status` -> `Status`): a use site may qualify a name with a module
|
|
68
|
+
/// the scan doesn't see, which is how the conformance and raw-type checks already match names.
|
|
69
|
+
func lookup(_ name: String, in scope: String) -> Entry? {
|
|
70
|
+
let name = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
|
|
71
|
+
var components = scope.split(separator: ".").map(String.init)
|
|
72
|
+
while true {
|
|
73
|
+
if let entry = entries[(components + [name]).joined(separator: ".")] {
|
|
74
|
+
return entry
|
|
75
|
+
}
|
|
76
|
+
if components.isEmpty {
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
components.removeLast()
|
|
80
|
+
}
|
|
81
|
+
var suffix = name.split(separator: ".").map(String.init).dropFirst()
|
|
82
|
+
while !suffix.isEmpty {
|
|
83
|
+
if let entry = entries[suffix.joined(separator: ".")] {
|
|
84
|
+
return entry
|
|
85
|
+
}
|
|
86
|
+
suffix = suffix.dropFirst()
|
|
87
|
+
}
|
|
64
88
|
return nil
|
|
65
89
|
}
|
|
66
|
-
return table[trailing]
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
/// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
|
|
@@ -76,32 +99,35 @@ private func jsType(ofRawType rawType: TypeNode?) -> JSType {
|
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
extension TypeNode {
|
|
79
|
-
/// This node with every `.ref` in it resolved against `index`, recursively.
|
|
80
|
-
/// node when nothing in it resolves.
|
|
81
|
-
func resolvingRefs(using index: RefIndex) -> TypeNode {
|
|
102
|
+
/// This node with every `.ref` in it resolved against `index` from the type `scope`, recursively.
|
|
103
|
+
/// Returns an unchanged node when nothing in it resolves.
|
|
104
|
+
func resolvingRefs(using index: RefIndex, in scope: String) -> TypeNode {
|
|
82
105
|
switch self {
|
|
83
106
|
case .primitive, .unknown:
|
|
84
107
|
return self
|
|
85
108
|
case .ref(let name, _, _):
|
|
86
|
-
guard let entry = index.lookup(name) else {
|
|
109
|
+
guard let entry = index.lookup(name, in: scope) else {
|
|
87
110
|
return self
|
|
88
111
|
}
|
|
89
112
|
// Only an enum crosses as something other than an object, so only it carries an override.
|
|
90
113
|
return .ref(
|
|
91
|
-
name:
|
|
114
|
+
name: entry.name,
|
|
115
|
+
refKind: entry.kind,
|
|
116
|
+
jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
|
|
92
117
|
case .optional(let wrapped):
|
|
93
|
-
return .optional(wrapped: wrapped.resolvingRefs(using: index))
|
|
118
|
+
return .optional(wrapped: wrapped.resolvingRefs(using: index, in: scope))
|
|
94
119
|
case .array(let element):
|
|
95
|
-
return .array(element: element.resolvingRefs(using: index))
|
|
120
|
+
return .array(element: element.resolvingRefs(using: index, in: scope))
|
|
96
121
|
case .dictionary(let key, let value):
|
|
97
122
|
return .dictionary(
|
|
98
|
-
key: key.resolvingRefs(using: index
|
|
123
|
+
key: key.resolvingRefs(using: index, in: scope),
|
|
124
|
+
value: value.resolvingRefs(using: index, in: scope))
|
|
99
125
|
case .promise(let value):
|
|
100
|
-
return .promise(value: value.resolvingRefs(using: index))
|
|
126
|
+
return .promise(value: value.resolvingRefs(using: index, in: scope))
|
|
101
127
|
case .function(let parameters, let returns, let isAsync, let isThrowing):
|
|
102
128
|
return .function(
|
|
103
|
-
parameters: parameters.map { $0.resolvingRefs(using: index) },
|
|
104
|
-
returns: returns?.resolvingRefs(using: index),
|
|
129
|
+
parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
|
|
130
|
+
returns: returns?.resolvingRefs(using: index, in: scope),
|
|
105
131
|
isAsync: isAsync,
|
|
106
132
|
isThrowing: isThrowing
|
|
107
133
|
)
|
|
@@ -113,9 +139,10 @@ extension ExportedSurface {
|
|
|
113
139
|
/// This surface with every `.ref` in every reported type resolved against its own declarations. The
|
|
114
140
|
/// second pass `scanExports` runs once the walk is done.
|
|
115
141
|
///
|
|
116
|
-
///
|
|
117
|
-
///
|
|
118
|
-
///
|
|
142
|
+
/// Each type's refs are looked up from that type, so a ref resolves to a type nested in it before
|
|
143
|
+
/// one at file scope. Only the boundary types are rewritten; names, flags, and ordering are
|
|
144
|
+
/// untouched. The declared types (a record's own name, a union's members) are not themselves refs,
|
|
145
|
+
/// so they carry no `refKind`: it is the *use sites* that gain one.
|
|
119
146
|
func resolvingRefs() -> ExportedSurface {
|
|
120
147
|
let index = RefIndex(surface: self)
|
|
121
148
|
return ExportedSurface(
|
|
@@ -123,11 +150,15 @@ extension ExportedSurface {
|
|
|
123
150
|
ExportedModule(
|
|
124
151
|
name: module.name,
|
|
125
152
|
jsName: module.jsName,
|
|
126
|
-
functions: module.functions.map { $0.resolvingRefs(using: index) },
|
|
127
|
-
properties: module.properties.map { $0.resolvingRefs(using: index) },
|
|
128
|
-
events: module.events.map { $0.resolvingRefs(using: index) },
|
|
129
|
-
classes: module.classes.map {
|
|
130
|
-
|
|
153
|
+
functions: module.functions.map { $0.resolvingRefs(using: index, in: module.name) },
|
|
154
|
+
properties: module.properties.map { $0.resolvingRefs(using: index, in: module.name) },
|
|
155
|
+
events: module.events.map { $0.resolvingRefs(using: index, in: module.name) },
|
|
156
|
+
classes: module.classes.map { exportedClass in
|
|
157
|
+
guard let entry = index.lookup(exportedClass.name, in: module.name),
|
|
158
|
+
entry.kind == .sharedObject else {
|
|
159
|
+
return exportedClass
|
|
160
|
+
}
|
|
161
|
+
return ExportedClass(name: entry.name, jsName: entry.jsName)
|
|
131
162
|
},
|
|
132
163
|
file: module.file
|
|
133
164
|
)
|
|
@@ -136,10 +167,12 @@ extension ExportedSurface {
|
|
|
136
167
|
ExportedSharedObject(
|
|
137
168
|
name: sharedObject.name,
|
|
138
169
|
jsName: sharedObject.jsName,
|
|
139
|
-
constructorParameters: sharedObject.constructorParameters?.map {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
constructorParameters: sharedObject.constructorParameters?.map {
|
|
171
|
+
$0.resolvingRefs(using: index, in: sharedObject.name)
|
|
172
|
+
},
|
|
173
|
+
functions: sharedObject.functions.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
|
|
174
|
+
properties: sharedObject.properties.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
|
|
175
|
+
events: sharedObject.events.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
|
|
143
176
|
file: sharedObject.file
|
|
144
177
|
)
|
|
145
178
|
},
|
|
@@ -149,7 +182,7 @@ extension ExportedSurface {
|
|
|
149
182
|
properties: record.properties.map { property in
|
|
150
183
|
ExportedRecordProperty(
|
|
151
184
|
name: property.name,
|
|
152
|
-
type: property.type.resolvingRefs(using: index),
|
|
185
|
+
type: property.type.resolvingRefs(using: index, in: record.name),
|
|
153
186
|
isOptional: property.isOptional,
|
|
154
187
|
hasDefault: property.hasDefault
|
|
155
188
|
)
|
|
@@ -164,7 +197,8 @@ extension ExportedSurface {
|
|
|
164
197
|
ExportedUnion(
|
|
165
198
|
name: union.name,
|
|
166
199
|
members: union.members.map { member in
|
|
167
|
-
ExportedUnionMember(
|
|
200
|
+
ExportedUnionMember(
|
|
201
|
+
name: member.name, type: member.type.resolvingRefs(using: index, in: union.name))
|
|
168
202
|
},
|
|
169
203
|
file: union.file
|
|
170
204
|
)
|
|
@@ -174,19 +208,19 @@ extension ExportedSurface {
|
|
|
174
208
|
}
|
|
175
209
|
|
|
176
210
|
extension ExportedParameter {
|
|
177
|
-
fileprivate func resolvingRefs(using index: RefIndex) -> ExportedParameter {
|
|
211
|
+
fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedParameter {
|
|
178
212
|
return ExportedParameter(
|
|
179
|
-
label: label, name: name, type: type.resolvingRefs(using: index), isOptional: isOptional)
|
|
213
|
+
label: label, name: name, type: type.resolvingRefs(using: index, in: scope), isOptional: isOptional)
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
|
|
183
217
|
extension ExportedFunction {
|
|
184
|
-
fileprivate func resolvingRefs(using index: RefIndex) -> ExportedFunction {
|
|
218
|
+
fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedFunction {
|
|
185
219
|
return ExportedFunction(
|
|
186
220
|
name: name,
|
|
187
221
|
jsName: jsName,
|
|
188
|
-
parameters: parameters.map { $0.resolvingRefs(using: index) },
|
|
189
|
-
returns: returns?.resolvingRefs(using: index),
|
|
222
|
+
parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
|
|
223
|
+
returns: returns?.resolvingRefs(using: index, in: scope),
|
|
190
224
|
isAsync: isAsync,
|
|
191
225
|
isThrowing: isThrowing,
|
|
192
226
|
isStatic: isStatic
|
|
@@ -195,11 +229,11 @@ extension ExportedFunction {
|
|
|
195
229
|
}
|
|
196
230
|
|
|
197
231
|
extension ExportedProperty {
|
|
198
|
-
fileprivate func resolvingRefs(using index: RefIndex) -> ExportedProperty {
|
|
232
|
+
fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedProperty {
|
|
199
233
|
return ExportedProperty(
|
|
200
234
|
name: name,
|
|
201
235
|
jsName: jsName,
|
|
202
|
-
type: type?.resolvingRefs(using: index),
|
|
236
|
+
type: type?.resolvingRefs(using: index, in: scope),
|
|
203
237
|
isSettable: isSettable,
|
|
204
238
|
isStatic: isStatic
|
|
205
239
|
)
|
|
@@ -207,8 +241,8 @@ extension ExportedProperty {
|
|
|
207
241
|
}
|
|
208
242
|
|
|
209
243
|
extension ExportedEvent {
|
|
210
|
-
fileprivate func resolvingRefs(using index: RefIndex) -> ExportedEvent {
|
|
244
|
+
fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedEvent {
|
|
211
245
|
return ExportedEvent(
|
|
212
|
-
name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index), isSync: isSync)
|
|
246
|
+
name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index, in: scope), isSync: isSync)
|
|
213
247
|
}
|
|
214
248
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
/// Extracts the full JS-exported surface of every
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
///
|
|
3
|
+
/// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, and `@Record` type:
|
|
4
|
+
/// their `@JS` members and record properties. The deep counterpart to `DetectionVisitor`.
|
|
5
|
+
/// Recognition is purely syntactic and re-reads what the macros read (the macro target can't be
|
|
6
|
+
/// imported), so it stays in step with `JSFunction` / `JSProperty` / `JSConstructor` /
|
|
7
7
|
/// `RecordProperty`.
|
|
8
|
+
///
|
|
9
|
+
/// Types nested in another type's body, or in a file-scope `extension`, are reported under their
|
|
10
|
+
/// qualified Swift name (`Options.Encoding`), so a ref to one can be resolved from the scope it is
|
|
11
|
+
/// written in. A type declared in a function body or a closure is local and is not reported.
|
|
8
12
|
final class SurfaceVisitor: SyntaxVisitor {
|
|
9
13
|
private let file: String
|
|
10
14
|
private(set) var modules: [ExportedModule] = []
|
|
@@ -19,19 +23,48 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
guard let scope = enclosingTypes(of: node) else {
|
|
27
|
+
return .skipChildren
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
classify(
|
|
30
|
+
name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
|
|
31
|
+
// The member walk reads the body itself; the children are visited only to find nested types.
|
|
32
|
+
return .visitChildren
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
guard let scope = enclosingTypes(of: node) else {
|
|
37
|
+
return .skipChildren
|
|
33
38
|
}
|
|
34
|
-
|
|
39
|
+
classify(
|
|
40
|
+
name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
|
|
41
|
+
return .visitChildren
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
45
|
+
// No macro the surface reports applies to an actor, but its body can declare types that do.
|
|
46
|
+
enclosingTypes(of: node) == nil ? .skipChildren : .visitChildren
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
50
|
+
// Only for the types declared in its body. Members an extension adds to a module, shared object,
|
|
51
|
+
// or record are not part of the surface, since the macros only read the type's own body.
|
|
52
|
+
enclosingTypes(of: node) == nil ? .skipChildren : .visitChildren
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A type declared in a function or accessor body or in a closure is local and never reported, so
|
|
56
|
+
// these are not walked.
|
|
57
|
+
|
|
58
|
+
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
|
|
59
|
+
.skipChildren
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
|
|
63
|
+
.skipChildren
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
|
67
|
+
.skipChildren
|
|
35
68
|
}
|
|
36
69
|
|
|
37
70
|
/// The two kinds of enum the surface reports: a `@Union` by its attribute, an `Enumerable` enum by
|
|
@@ -41,14 +74,15 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
41
74
|
/// would be nothing to report as an enum, and listing it in both arrays would describe two
|
|
42
75
|
/// contradictory JS types for one declaration.
|
|
43
76
|
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
44
|
-
guard
|
|
77
|
+
guard let scope = enclosingTypes(of: node) else {
|
|
45
78
|
return .skipChildren
|
|
46
79
|
}
|
|
80
|
+
let name = qualifiedName(node.name.identifierName, in: scope)
|
|
47
81
|
|
|
48
82
|
if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
|
|
49
83
|
unions.append(
|
|
50
84
|
ExportedUnion(
|
|
51
|
-
name:
|
|
85
|
+
name: name,
|
|
52
86
|
members: collectUnionMembers(node.memberBlock.members),
|
|
53
87
|
file: file
|
|
54
88
|
))
|
|
@@ -56,24 +90,38 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
56
90
|
let rawType = rawValueType(of: node.inheritanceClause)
|
|
57
91
|
enums.append(
|
|
58
92
|
ExportedEnum(
|
|
59
|
-
name:
|
|
93
|
+
name: name,
|
|
60
94
|
rawType: rawType,
|
|
61
95
|
cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
|
|
62
96
|
file: file
|
|
63
97
|
))
|
|
64
98
|
}
|
|
65
|
-
|
|
99
|
+
// An enum is often a namespace for other types, so its body is searched too.
|
|
100
|
+
return .visitChildren
|
|
66
101
|
}
|
|
67
102
|
|
|
68
|
-
/// Routes a
|
|
103
|
+
/// Routes a class or struct to the right collector based on which Expo macro it carries. A type
|
|
69
104
|
/// carrying none of them is ignored. `@Record` and `@ExpoModule`/`@SharedObject` are mutually
|
|
70
105
|
/// exclusive in practice, so the first match wins.
|
|
71
|
-
|
|
106
|
+
///
|
|
107
|
+
/// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
|
|
108
|
+
/// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
|
|
109
|
+
/// spell it.
|
|
110
|
+
private func classify(
|
|
111
|
+
name: String, scope: [String], attributes: AttributeListSyntax, members: MemberBlockItemListSyntax
|
|
112
|
+
) {
|
|
113
|
+
let qualified = qualifiedName(name, in: scope)
|
|
114
|
+
|
|
72
115
|
if let attribute = attributes.firstAttribute(named: DetectedMacro.expoModule.rawValue) {
|
|
116
|
+
// Only a file-scope module is reported. Autolinking registers no other (`DetectionVisitor`
|
|
117
|
+
// reads file scope only), so a nested one never loads.
|
|
118
|
+
guard scope.isEmpty else {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
73
121
|
let (functions, properties, events, _) = collectJSMembers(members)
|
|
74
122
|
modules.append(
|
|
75
123
|
ExportedModule(
|
|
76
|
-
name:
|
|
124
|
+
name: qualified,
|
|
77
125
|
jsName: stringArgument(of: attribute) ?? name,
|
|
78
126
|
functions: functions,
|
|
79
127
|
properties: properties,
|
|
@@ -92,7 +140,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
92
140
|
let (functions, properties, events, constructor) = collectJSMembers(members)
|
|
93
141
|
sharedObjects.append(
|
|
94
142
|
ExportedSharedObject(
|
|
95
|
-
name:
|
|
143
|
+
name: qualified,
|
|
96
144
|
jsName: stringArgument(of: attribute) ?? name,
|
|
97
145
|
constructorParameters: constructor,
|
|
98
146
|
functions: functions,
|
|
@@ -104,7 +152,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
104
152
|
}
|
|
105
153
|
|
|
106
154
|
if attributes.firstAttribute(named: DetectedMacro.record.rawValue) != nil {
|
|
107
|
-
records.append(ExportedRecord(name:
|
|
155
|
+
records.append(ExportedRecord(name: qualified, properties: collectRecordProperties(members), file: file))
|
|
108
156
|
}
|
|
109
157
|
}
|
|
110
158
|
|
|
@@ -364,16 +412,40 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
364
412
|
}
|
|
365
413
|
}
|
|
366
414
|
|
|
367
|
-
///
|
|
368
|
-
///
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
415
|
+
/// The names of the types enclosing a declaration, outermost first: empty at file scope, and `nil`
|
|
416
|
+
/// when the declaration is local to a function body or a closure, or sits in a `#if` block.
|
|
417
|
+
///
|
|
418
|
+
/// File scope uses the same rule as `DetectionVisitor.isTopLevel`, except that `#if` is not looked
|
|
419
|
+
/// through: the parent is a `CodeBlockItemSyntax` directly under the source file's top-level item
|
|
420
|
+
/// list. A member is reached from its type through `MemberBlockItemSyntax`; an `extension`
|
|
421
|
+
/// contributes its extended type as written (without backticks), so its spelling may already be
|
|
422
|
+
/// qualified.
|
|
423
|
+
private func enclosingTypes(of node: some SyntaxProtocol) -> [String]? {
|
|
424
|
+
if let item = node.parent?.as(CodeBlockItemSyntax.self) {
|
|
425
|
+
return item.parent?.parent?.is(SourceFileSyntax.self) == true ? [] : nil
|
|
372
426
|
}
|
|
373
|
-
|
|
427
|
+
guard let item = node.parent?.as(MemberBlockItemSyntax.self),
|
|
428
|
+
let parent = item.parent?.parent?.parent,
|
|
429
|
+
let scope = enclosingTypes(of: parent) else {
|
|
430
|
+
return nil
|
|
431
|
+
}
|
|
432
|
+
if let decl = parent.as(ExtensionDeclSyntax.self) {
|
|
433
|
+
// Without backticks, like every other reported name.
|
|
434
|
+
return scope + [String(decl.extendedType.trimmedDescription.filter { $0 != "`" })]
|
|
435
|
+
}
|
|
436
|
+
// A class, struct, enum, or actor. A protocol body cannot declare types.
|
|
437
|
+
guard !parent.is(ProtocolDeclSyntax.self), let decl = parent.asProtocol(NamedDeclSyntax.self) else {
|
|
438
|
+
return nil
|
|
439
|
+
}
|
|
440
|
+
return scope + [decl.name.identifierName]
|
|
374
441
|
}
|
|
375
442
|
}
|
|
376
443
|
|
|
444
|
+
/// A declared name qualified with the names of its enclosing types: `Options.Encoding`.
|
|
445
|
+
private func qualifiedName(_ name: String, in scope: [String]) -> String {
|
|
446
|
+
(scope + [name]).joined(separator: ".")
|
|
447
|
+
}
|
|
448
|
+
|
|
377
449
|
// MARK: - Syntactic helpers (shared spelling with the macros)
|
|
378
450
|
|
|
379
451
|
extension TokenSyntax {
|
|
@@ -58,7 +58,9 @@ indirect enum TypeNode: Equatable {
|
|
|
58
58
|
case function(parameters: [TypeNode], returns: TypeNode?, isAsync: Bool, isThrowing: Bool)
|
|
59
59
|
|
|
60
60
|
/// Any other named type (record, shared object, enum, union, …). `name` is the possibly-qualified
|
|
61
|
-
/// spelling.
|
|
61
|
+
/// spelling until resolution. A resolved ref takes the qualified name of its declaration instead,
|
|
62
|
+
/// so `Encoding` written in `Options` becomes `Options.Encoding`, and a consumer finds the
|
|
63
|
+
/// declaration by an exact match.
|
|
62
64
|
///
|
|
63
65
|
/// `refKind` says which scanned kind declares that name, filled in after the scan by
|
|
64
66
|
/// `ExportedSurface.resolvingRefs()`; it is `nil` until then, and stays `nil` for a name the scan
|