expo-modules-macros 0.14.0 → 0.16.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 +1 -1
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +405 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +13 -10
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +7 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +31 -4
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +16 -6
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +30 -6
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +17 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +26 -9
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +24 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +26 -7
- package/build/types.d.ts +17 -1
- package/build/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ This package is not meant to be installed directly. It is a dependency of `expo-
|
|
|
18
18
|
# Macros
|
|
19
19
|
|
|
20
20
|
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none.
|
|
21
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. The macro also checks that every type crossing the boundary is convertible in the direction it travels, so a bad type is reported on the author's own declaration rather than on the enclosing type. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it.
|
|
21
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. The macro also checks that every type crossing the boundary is convertible in the direction it travels, so a bad type is reported on the author's own declaration rather than on the enclosing type. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
22
22
|
- **`@Event(_ name: String? = nil, sync: Bool = false)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `sync: true` dispatches inline instead of asynchronously.
|
|
23
23
|
- **`@SharedObject(_ name: String? = nil)`** on a `SharedObject` subclass. Collects the class's `@JS` members into a class definition that a module exposes through `@ExpoModule(classes:)`.
|
|
24
24
|
- **`@Record()`** on a record type. Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/// A closure parameter of a `@JS` function or initializer, read from its declared type. JS passes a
|
|
6
|
+
/// function for it, and the binding wraps that function in a native closure of the declared type.
|
|
7
|
+
///
|
|
8
|
+
/// A function type can't conform to `JavaScriptDecodable`, so a closure parameter doesn't decode
|
|
9
|
+
/// through `decode` like other arguments. The data also moves the other way: the closure's parameters
|
|
10
|
+
/// are encoded when native code calls it, and its return value is decoded from what the JS function
|
|
11
|
+
/// returns.
|
|
12
|
+
internal struct JSClosureType {
|
|
13
|
+
/// The function type as written, under any attributes, parentheses and optional wrapper.
|
|
14
|
+
let functionType: FunctionTypeSyntax
|
|
15
|
+
/// The attributes written on the function type, such as `@escaping` or `@Sendable`.
|
|
16
|
+
let attributes: [AttributeSyntax]
|
|
17
|
+
/// The closure's parameters. Each one is encoded to JS when the closure is called.
|
|
18
|
+
let parameters: [Parameter]
|
|
19
|
+
/// The return type as written, or `nil` for `Void`. It is decoded from the JS function's result.
|
|
20
|
+
let returnType: String?
|
|
21
|
+
let isAsync: Bool
|
|
22
|
+
let isThrowing: Bool
|
|
23
|
+
/// True for an optional closure (`((Int) -> Void)?`): JS `null` and `undefined` become `nil`.
|
|
24
|
+
let isOptional: Bool
|
|
25
|
+
|
|
26
|
+
/// Returns `nil` when the type isn't a closure, or is a closure nested in another type such as an
|
|
27
|
+
/// array. One optional layer is accepted around the function type.
|
|
28
|
+
init?(_ type: TypeSyntax) {
|
|
29
|
+
var type = unparenthesized(type)
|
|
30
|
+
var isOptional = false
|
|
31
|
+
if let wrapped = optionalWrappedType(type) {
|
|
32
|
+
isOptional = true
|
|
33
|
+
type = unparenthesized(wrapped)
|
|
34
|
+
}
|
|
35
|
+
var attributes: [AttributeSyntax] = []
|
|
36
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
37
|
+
attributes = attributed.attributes.compactMap { $0.as(AttributeSyntax.self) }
|
|
38
|
+
type = unparenthesized(attributed.baseType)
|
|
39
|
+
}
|
|
40
|
+
guard let functionType = type.as(FunctionTypeSyntax.self) else {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
self.functionType = functionType
|
|
44
|
+
self.attributes = attributes
|
|
45
|
+
self.parameters = functionType.parameters.map(Parameter.init)
|
|
46
|
+
let returnType = functionType.returnClause.type
|
|
47
|
+
self.returnType = isVoidReturnType(returnType) ? nil : returnType.trimmedDescription
|
|
48
|
+
self.isAsync = functionType.effectSpecifiers?.asyncSpecifier != nil
|
|
49
|
+
self.isThrowing = functionType.effectSpecifiers?.throwsClause != nil
|
|
50
|
+
self.isOptional = isOptional
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// The type of the wrapper closure as the binding declares it. `@escaping` is left out, since it is
|
|
54
|
+
/// only valid on a parameter.
|
|
55
|
+
var wrapperTypeText: String {
|
|
56
|
+
let kept = attributes.filter { attributeName($0) != "escaping" }.map { $0.trimmedDescription + " " }
|
|
57
|
+
let text = kept.joined() + functionType.trimmedDescription
|
|
58
|
+
return isOptional ? "(\(text))?" : text
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// One parameter of the closure type.
|
|
62
|
+
struct Parameter {
|
|
63
|
+
/// The parameter's type without its specifiers, such as `borrowing` or `inout`.
|
|
64
|
+
let type: TypeSyntax
|
|
65
|
+
/// The specifiers written before the type, such as `borrowing`, `sending` or `inout`.
|
|
66
|
+
let specifiers: [String]
|
|
67
|
+
/// True for a variadic parameter (`Int...`).
|
|
68
|
+
let isVariadic: Bool
|
|
69
|
+
|
|
70
|
+
init(_ element: TupleTypeElementSyntax) {
|
|
71
|
+
if let attributed = element.type.as(AttributedTypeSyntax.self), !attributed.specifiers.isEmpty {
|
|
72
|
+
self.specifiers = attributed.specifiers.map(\.trimmedDescription)
|
|
73
|
+
self.type =
|
|
74
|
+
attributed.attributes.isEmpty
|
|
75
|
+
? attributed.baseType : TypeSyntax(attributed.with(\.specifiers, []))
|
|
76
|
+
} else {
|
|
77
|
+
self.specifiers = []
|
|
78
|
+
self.type = element.type
|
|
79
|
+
}
|
|
80
|
+
self.isVariadic = element.ellipsis != nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// The first specifier the wrapper can't support, such as `inout` or `isolated`.
|
|
84
|
+
var unsupportedSpecifier: String? {
|
|
85
|
+
return specifiers.first { !supportedParameterSpecifiers.contains($0) }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The ownership specifiers a closure parameter may carry. The wrapper passes each parameter on to the
|
|
91
|
+
/// callback, so the ownership convention doesn't change what it does.
|
|
92
|
+
private let supportedParameterSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
93
|
+
|
|
94
|
+
/// The statements that bind `name` to the native value of a `@JS` argument read from
|
|
95
|
+
/// `valueExpression` (a `JavaScriptUnownedValue`). A closure gets a wrapper around the JS function,
|
|
96
|
+
/// and every other type decodes through `decodeCall`.
|
|
97
|
+
internal func argumentDecodeStatements(
|
|
98
|
+
type: TypeSyntax,
|
|
99
|
+
into name: String,
|
|
100
|
+
from valueExpression: String
|
|
101
|
+
) -> [String] {
|
|
102
|
+
guard let closure = JSClosureType(type) else {
|
|
103
|
+
return ["let \(name) = try \(decodeCall(type.trimmedDescription, from: valueExpression))"]
|
|
104
|
+
}
|
|
105
|
+
return closureArgumentStatements(closure, into: name, from: valueExpression)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Binds `name` to a closure that calls the JS function in `valueExpression`. The
|
|
109
|
+
/// `JavaScriptCallback` keeps the JS function alive and does the thread hops, so the closure can be
|
|
110
|
+
/// stored and called from any thread.
|
|
111
|
+
private func closureArgumentStatements(
|
|
112
|
+
_ closure: JSClosureType,
|
|
113
|
+
into name: String,
|
|
114
|
+
from valueExpression: String
|
|
115
|
+
) -> [String] {
|
|
116
|
+
let callback = "\(name)Callback"
|
|
117
|
+
let wrapper = closureLiteralLines(closure, callback: callback)
|
|
118
|
+
|
|
119
|
+
guard closure.isOptional else {
|
|
120
|
+
var lines = ["let \(callback) = try JavaScriptCallback.decode(\(valueExpression), in: runtime)"]
|
|
121
|
+
lines.append("let \(name): \(closure.wrapperTypeText) = " + wrapper[0])
|
|
122
|
+
lines.append(contentsOf: wrapper.dropFirst())
|
|
123
|
+
return lines
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var lines = [
|
|
127
|
+
"let \(name): \(closure.wrapperTypeText)",
|
|
128
|
+
"if let \(callback) = try JavaScriptCallback.decodeIfPresent(\(valueExpression), in: runtime) {",
|
|
129
|
+
" \(name) = " + wrapper[0],
|
|
130
|
+
]
|
|
131
|
+
lines.append(contentsOf: wrapper.dropFirst().map { " " + $0 })
|
|
132
|
+
lines.append("} else {")
|
|
133
|
+
lines.append(" \(name) = nil")
|
|
134
|
+
lines.append("}")
|
|
135
|
+
return lines
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// The closure literal `{ p0, p1 in … }` that passes its parameters to the `JavaScriptCallback`,
|
|
139
|
+
/// which encodes them, calls the function and decodes the result. The closure's effects pick the
|
|
140
|
+
/// method:
|
|
141
|
+
/// - a non-throwing `Void` closure doesn't wait for JS (`invokeDetached`);
|
|
142
|
+
/// - a sync closure that throws blocks until JS returns (`invokeBlocking`);
|
|
143
|
+
/// - an `async` closure suspends instead (`invokeAsync`), and the callback awaits a returned promise.
|
|
144
|
+
private func closureLiteralLines(_ closure: JSClosureType, callback: String) -> [String] {
|
|
145
|
+
// `@Sendable` keeps the literal from taking the binding's `@JavaScriptActor` isolation: native code
|
|
146
|
+
// may call it from any thread, and it captures only the callback.
|
|
147
|
+
let parameters = closure.parameters.indices.map { "p\($0)" }
|
|
148
|
+
let header = parameters.isEmpty ? "{ @Sendable in" : "{ @Sendable \(parameters.joined(separator: ", ")) in"
|
|
149
|
+
|
|
150
|
+
var arguments = parameters
|
|
151
|
+
if let returnType = closure.returnType {
|
|
152
|
+
arguments.append("returning: \(expressionType(returnType)).self")
|
|
153
|
+
}
|
|
154
|
+
let argumentList = arguments.joined(separator: ", ")
|
|
155
|
+
|
|
156
|
+
let call: String
|
|
157
|
+
if closure.isAsync {
|
|
158
|
+
call = "try await \(callback).invokeAsync(\(argumentList))"
|
|
159
|
+
} else if closure.isThrowing || closure.returnType != nil {
|
|
160
|
+
call = "try \(callback).invokeBlocking(\(argumentList))"
|
|
161
|
+
} else {
|
|
162
|
+
call = "\(callback).invokeDetached(\(argumentList))"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
guard closure.isAsync && !closure.isThrowing else {
|
|
166
|
+
return [header, " " + call, "}"]
|
|
167
|
+
}
|
|
168
|
+
// The closure can't throw, so an error from JS goes to the callback's error reporting.
|
|
169
|
+
return [
|
|
170
|
+
header,
|
|
171
|
+
" do {",
|
|
172
|
+
" " + call,
|
|
173
|
+
" } catch {",
|
|
174
|
+
" \(callback).reportError(error)",
|
|
175
|
+
" }",
|
|
176
|
+
"}",
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MARK: - Diagnostics
|
|
181
|
+
|
|
182
|
+
/// Emits the closure diagnostics for a `@JS` declaration. A closure is supported only as a parameter
|
|
183
|
+
/// of a function or initializer, where JS passes a function in. Every other position would need a
|
|
184
|
+
/// native closure encoded to a JS function, which isn't supported.
|
|
185
|
+
internal func diagnoseClosureTypes(
|
|
186
|
+
in declaration: some DeclSyntaxProtocol,
|
|
187
|
+
in context: some MacroExpansionContext
|
|
188
|
+
) {
|
|
189
|
+
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
190
|
+
diagnoseClosureParameters(funcDecl.signature.parameterClause.parameters, in: context)
|
|
191
|
+
if let returnType = funcDecl.signature.returnClause?.type, let nested = firstFunctionType(in: returnType) {
|
|
192
|
+
context.diagnose(
|
|
193
|
+
Diagnostic(
|
|
194
|
+
node: nested,
|
|
195
|
+
message: ClosureDiagnosticMessage(
|
|
196
|
+
"A @JS function can't return a closure: a native closure can't be encoded to a JavaScript function.",
|
|
197
|
+
id: "js-closure-return"
|
|
198
|
+
)))
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
203
|
+
diagnoseClosureParameters(initDecl.signature.parameterClause.parameters, in: context)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
207
|
+
let type = varDecl.bindings.first?.typeAnnotation?.type,
|
|
208
|
+
let nested = firstFunctionType(in: type) {
|
|
209
|
+
context.diagnose(
|
|
210
|
+
Diagnostic(
|
|
211
|
+
node: nested,
|
|
212
|
+
message: ClosureDiagnosticMessage(
|
|
213
|
+
"A @JS property can't have a closure type: a native closure can't be encoded to a JavaScript function.",
|
|
214
|
+
id: "js-closure-property"
|
|
215
|
+
)))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func diagnoseClosureParameters(
|
|
220
|
+
_ parameters: FunctionParameterListSyntax,
|
|
221
|
+
in context: some MacroExpansionContext
|
|
222
|
+
) {
|
|
223
|
+
for parameter in parameters {
|
|
224
|
+
guard let closure = JSClosureType(parameter.type) else {
|
|
225
|
+
if let nested = firstFunctionType(in: parameter.type) {
|
|
226
|
+
context.diagnose(
|
|
227
|
+
Diagnostic(
|
|
228
|
+
node: nested,
|
|
229
|
+
message: ClosureDiagnosticMessage(
|
|
230
|
+
"A closure in a @JS argument must be the argument's own type, optionally wrapped in an optional. Closures inside other types, such as arrays or dictionaries, aren't supported.",
|
|
231
|
+
id: "js-closure-nested-in-type"
|
|
232
|
+
)))
|
|
233
|
+
}
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
diagnoseClosure(closure, in: context)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExpansionContext) {
|
|
241
|
+
let functionType = closure.functionType
|
|
242
|
+
|
|
243
|
+
for attribute in closure.attributes {
|
|
244
|
+
let name = attributeName(attribute)
|
|
245
|
+
guard name != "escaping" && name != "Sendable" else {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
context.diagnose(
|
|
249
|
+
Diagnostic(
|
|
250
|
+
node: attribute,
|
|
251
|
+
message: ClosureDiagnosticMessage(
|
|
252
|
+
"'@\(name)' isn't supported on a @JS closure argument. The closure calls JavaScript through its own thread hop, so only '@escaping' and '@Sendable' are allowed.",
|
|
253
|
+
id: "js-closure-attribute"
|
|
254
|
+
)))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if let throwsClause = functionType.effectSpecifiers?.throwsClause, throwsClause.type != nil {
|
|
258
|
+
context.diagnose(
|
|
259
|
+
Diagnostic(
|
|
260
|
+
node: throwsClause,
|
|
261
|
+
message: ClosureDiagnosticMessage(
|
|
262
|
+
"A @JS closure argument can't use typed throws: the error from JavaScript can be any error. Write 'throws' without a type.",
|
|
263
|
+
id: "js-closure-typed-throws"
|
|
264
|
+
)))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for parameter in closure.parameters {
|
|
268
|
+
if let specifier = parameter.unsupportedSpecifier {
|
|
269
|
+
context.diagnose(
|
|
270
|
+
Diagnostic(
|
|
271
|
+
node: parameter.type,
|
|
272
|
+
message: ClosureDiagnosticMessage(
|
|
273
|
+
"A @JS closure argument can't take an '\(specifier)' parameter: its value is encoded and sent to JavaScript, which can't write it back or share its isolation.",
|
|
274
|
+
id: "js-closure-parameter-specifier"
|
|
275
|
+
)))
|
|
276
|
+
}
|
|
277
|
+
if parameter.isVariadic {
|
|
278
|
+
context.diagnose(
|
|
279
|
+
Diagnostic(
|
|
280
|
+
node: parameter.type,
|
|
281
|
+
message: ClosureDiagnosticMessage(
|
|
282
|
+
"A @JS closure argument can't take a variadic parameter. Use an array instead.",
|
|
283
|
+
id: "js-closure-variadic-parameter"
|
|
284
|
+
)))
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The closure's parameters are encoded and its result is decoded, so neither can be another
|
|
289
|
+
// closure or a free-form type.
|
|
290
|
+
let returnType = functionType.returnClause.type
|
|
291
|
+
for type in closure.parameters.map(\.type) + [returnType] {
|
|
292
|
+
if let nested = firstFunctionType(in: type) {
|
|
293
|
+
context.diagnose(
|
|
294
|
+
Diagnostic(
|
|
295
|
+
node: nested,
|
|
296
|
+
message: ClosureDiagnosticMessage(
|
|
297
|
+
"A @JS closure argument can't take or return another closure.",
|
|
298
|
+
id: "js-closure-nested"
|
|
299
|
+
)))
|
|
300
|
+
} else if isFreeFormBoundaryType(type.trimmedDescription) {
|
|
301
|
+
context.diagnose(
|
|
302
|
+
Diagnostic(
|
|
303
|
+
node: type,
|
|
304
|
+
message: ClosureDiagnosticMessage(
|
|
305
|
+
"A @JS closure argument can't use the free-form '\(type.trimmedDescription)'. Use a concrete type, or 'JavaScriptValue' to pass a JS value through unchanged.",
|
|
306
|
+
id: "js-closure-free-form"
|
|
307
|
+
)))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if closure.returnType != nil && !closure.isThrowing {
|
|
312
|
+
context.diagnose(
|
|
313
|
+
Diagnostic(
|
|
314
|
+
node: functionType,
|
|
315
|
+
message: ClosureDiagnosticMessage(
|
|
316
|
+
"A @JS closure argument that returns a value must be 'throws': when the JavaScript function throws, the closure has no value to return.",
|
|
317
|
+
id: "js-closure-requires-throws"
|
|
318
|
+
),
|
|
319
|
+
fixIts: [insertThrowsFixIt(for: functionType)]))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// The fix-it for a non-throwing closure that returns a value: insert `throws` after the parameter
|
|
324
|
+
/// list, or after `async` when the type has it. The inserted keyword takes over the trivia that
|
|
325
|
+
/// followed the token before it, so `(Int) -> Int` becomes `(Int) throws -> Int`.
|
|
326
|
+
private func insertThrowsFixIt(for functionType: FunctionTypeSyntax) -> FixIt {
|
|
327
|
+
var newType = functionType
|
|
328
|
+
if var effectSpecifiers = functionType.effectSpecifiers, let asyncSpecifier = effectSpecifiers.asyncSpecifier {
|
|
329
|
+
effectSpecifiers.throwsClause = ThrowsClauseSyntax(
|
|
330
|
+
throwsSpecifier: .keyword(.throws, trailingTrivia: asyncSpecifier.trailingTrivia))
|
|
331
|
+
effectSpecifiers.asyncSpecifier = asyncSpecifier.with(\.trailingTrivia, .space)
|
|
332
|
+
newType.effectSpecifiers = effectSpecifiers
|
|
333
|
+
} else {
|
|
334
|
+
newType.effectSpecifiers = TypeEffectSpecifiersSyntax(
|
|
335
|
+
throwsClause: ThrowsClauseSyntax(
|
|
336
|
+
throwsSpecifier: .keyword(.throws, trailingTrivia: functionType.rightParen.trailingTrivia)))
|
|
337
|
+
newType.rightParen = functionType.rightParen.with(\.trailingTrivia, .space)
|
|
338
|
+
}
|
|
339
|
+
return FixIt(
|
|
340
|
+
message: ClosureFixItMessage("Mark the closure 'throws'", id: "js-closure-insert-throws"),
|
|
341
|
+
changes: [.replace(oldNode: Syntax(functionType), newNode: Syntax(newType))]
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// MARK: - Helpers
|
|
346
|
+
|
|
347
|
+
/// True when `type` is a function type or has one anywhere inside it.
|
|
348
|
+
internal func containsFunctionType(_ type: TypeSyntax) -> Bool {
|
|
349
|
+
return firstFunctionType(in: type) != nil
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// The first function type in `type` or anywhere inside it, in source order.
|
|
353
|
+
private func firstFunctionType(in type: some SyntaxProtocol) -> FunctionTypeSyntax? {
|
|
354
|
+
if let functionType = type.as(FunctionTypeSyntax.self) {
|
|
355
|
+
return functionType
|
|
356
|
+
}
|
|
357
|
+
for child in type.children(viewMode: .sourceAccurate) {
|
|
358
|
+
if let found = firstFunctionType(in: child) {
|
|
359
|
+
return found
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return nil
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// The type inside any number of single-element, unlabeled parentheses: `((Int) -> Void)` is the
|
|
366
|
+
/// same type as `(Int) -> Void`.
|
|
367
|
+
private func unparenthesized(_ type: TypeSyntax) -> TypeSyntax {
|
|
368
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
369
|
+
let element = tuple.elements.first, element.firstName == nil {
|
|
370
|
+
return unparenthesized(element.type)
|
|
371
|
+
}
|
|
372
|
+
return type
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// True when a closure's return type is `Void`, `()` or `Swift.Void`, also inside parentheses.
|
|
376
|
+
private func isVoidReturnType(_ type: TypeSyntax) -> Bool {
|
|
377
|
+
let type = unparenthesized(type)
|
|
378
|
+
let text = type.trimmedDescription
|
|
379
|
+
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private func attributeName(_ attribute: AttributeSyntax) -> String {
|
|
383
|
+
return attribute.attributeName.trimmedDescription
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private struct ClosureDiagnosticMessage: DiagnosticMessage {
|
|
387
|
+
let message: String
|
|
388
|
+
let diagnosticID: MessageID
|
|
389
|
+
let severity: DiagnosticSeverity = .error
|
|
390
|
+
|
|
391
|
+
init(_ message: String, id: String) {
|
|
392
|
+
self.message = message
|
|
393
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private struct ClosureFixItMessage: FixItMessage {
|
|
398
|
+
let message: String
|
|
399
|
+
let fixItID: MessageID
|
|
400
|
+
|
|
401
|
+
init(_ message: String, id: String) {
|
|
402
|
+
self.message = message
|
|
403
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -86,7 +86,7 @@ internal struct JSFunction {
|
|
|
86
86
|
// Decode the required prefix once — these slots are present in every accepted arity, so the
|
|
87
87
|
// decode is shared rather than repeated per branch.
|
|
88
88
|
for index in 0..<required {
|
|
89
|
-
lines.append(
|
|
89
|
+
lines.append(contentsOf: decodeLines(at: index))
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if required == maximum {
|
|
@@ -95,7 +95,7 @@ internal struct JSFunction {
|
|
|
95
95
|
// body; a sync one calls and encodes directly.
|
|
96
96
|
if isAsync {
|
|
97
97
|
for index in required..<maximum {
|
|
98
|
-
lines.append(
|
|
98
|
+
lines.append(contentsOf: decodeLines(at: index))
|
|
99
99
|
}
|
|
100
100
|
lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: maximum))
|
|
101
101
|
} else {
|
|
@@ -110,7 +110,7 @@ internal struct JSFunction {
|
|
|
110
110
|
let label = arity == maximum ? "default:" : "case \(arity):"
|
|
111
111
|
lines.append(label)
|
|
112
112
|
for index in required..<arity {
|
|
113
|
-
lines.append(" " +
|
|
113
|
+
lines.append(contentsOf: decodeLines(at: index).map { " " + $0 })
|
|
114
114
|
}
|
|
115
115
|
lines.append(contentsOf: asyncBodyLines(receiver: receiver, arity: arity).map { " " + $0 })
|
|
116
116
|
}
|
|
@@ -127,7 +127,7 @@ internal struct JSFunction {
|
|
|
127
127
|
let label = arity == maximum ? "default:" : "case \(arity):"
|
|
128
128
|
lines.append(label)
|
|
129
129
|
for index in required..<arity {
|
|
130
|
-
lines.append(" " +
|
|
130
|
+
lines.append(contentsOf: decodeLines(at: index).map { " " + $0 })
|
|
131
131
|
}
|
|
132
132
|
let assignment = returnType != nil ? "result = " : ""
|
|
133
133
|
lines.append(" \(assignment)\(callExpression(receiver: receiver, arity: arity))")
|
|
@@ -160,14 +160,17 @@ internal struct JSFunction {
|
|
|
160
160
|
return lines
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/// `
|
|
163
|
+
/// The lines binding `arg<index>`: a decode of the slot at `index` by its static type through
|
|
164
164
|
/// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
|
|
165
165
|
/// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
|
|
166
166
|
/// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
|
|
167
|
-
/// path would use.
|
|
168
|
-
private func
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
/// path would use. A closure parameter takes several lines instead, wrapping the JS function.
|
|
168
|
+
private func decodeLines(at index: Int) -> [String] {
|
|
169
|
+
return argumentDecodeStatements(
|
|
170
|
+
type: parameters[index].type,
|
|
171
|
+
into: "arg\(index)",
|
|
172
|
+
from: "arguments.unownedValue(at: \(index))"
|
|
173
|
+
)
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
|
|
@@ -205,7 +208,7 @@ internal struct JSFunction {
|
|
|
205
208
|
private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
|
|
206
209
|
var lines: [String] = []
|
|
207
210
|
for index in decodingFrom..<arity {
|
|
208
|
-
lines.append(
|
|
211
|
+
lines.append(contentsOf: decodeLines(at: index))
|
|
209
212
|
}
|
|
210
213
|
if returnType != nil {
|
|
211
214
|
lines.append("let result = \(callExpression(receiver: receiver, arity: arity))")
|
|
@@ -14,7 +14,7 @@ internal struct JSConstructor {
|
|
|
14
14
|
|
|
15
15
|
/// The body statements, indented with `indent`: arity guard, per-argument decode through
|
|
16
16
|
/// `JavaScriptDecodable.decode` on a zero-copy `arguments.unownedValue(at:)` (the arity guard proves
|
|
17
|
-
/// each index is in bounds), then `return <Type>(label: arg0, …)`.
|
|
17
|
+
/// each index is in bounds) or a wrapper for a closure argument, then `return <Type>(label: arg0, …)`.
|
|
18
18
|
private func bodyStatements(typeName: String, indent: String) -> String {
|
|
19
19
|
var lines: [String] = []
|
|
20
20
|
|
|
@@ -27,8 +27,12 @@ internal struct JSConstructor {
|
|
|
27
27
|
|
|
28
28
|
var callArguments: [String] = []
|
|
29
29
|
for (index, parameter) in parameters.enumerated() {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
lines.append(
|
|
31
|
+
contentsOf: argumentDecodeStatements(
|
|
32
|
+
type: parameter.type,
|
|
33
|
+
into: "arg\(index)",
|
|
34
|
+
from: "arguments.unownedValue(at: \(index))"
|
|
35
|
+
))
|
|
32
36
|
|
|
33
37
|
let label = parameter.firstName.text
|
|
34
38
|
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
@@ -32,6 +32,7 @@ public struct JSMacro: PeerMacro {
|
|
|
32
32
|
in context: some MacroExpansionContext
|
|
33
33
|
) throws -> [DeclSyntax] {
|
|
34
34
|
diagnoseFreeFormTypes(in: declaration, in: context)
|
|
35
|
+
diagnoseClosureTypes(in: declaration, in: context)
|
|
35
36
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
36
37
|
|
|
37
38
|
guard let member = boundaryMember(of: declaration),
|
|
@@ -39,6 +40,7 @@ public struct JSMacro: PeerMacro {
|
|
|
39
40
|
name: member.name,
|
|
40
41
|
decodableTypes: member.decodableTypes,
|
|
41
42
|
encodableType: member.encodableType,
|
|
43
|
+
callbackArgumentTypes: member.callbackArgumentTypes,
|
|
42
44
|
isStatic: member.isStatic
|
|
43
45
|
) else {
|
|
44
46
|
return []
|
|
@@ -237,11 +239,14 @@ private struct JSFixItMessage: FixItMessage {
|
|
|
237
239
|
/// property's outgoing value) are encoded, so each is asserted against the protocol for its direction.
|
|
238
240
|
private struct BoundaryMember {
|
|
239
241
|
let name: String
|
|
240
|
-
/// Types decoded from JS: function/constructor arguments,
|
|
242
|
+
/// Types decoded from JS: function/constructor arguments, a settable property's value type, and the
|
|
243
|
+
/// return types of closure arguments.
|
|
241
244
|
let decodableTypes: [String]
|
|
242
245
|
/// The single type encoded to JS: a function's return type, or a property's value type on read;
|
|
243
246
|
/// `nil` when the member produces nothing JS-visible (a `Void` function).
|
|
244
247
|
let encodableType: String?
|
|
248
|
+
/// The parameter types of closure arguments, encoded to JS when native code calls the closure.
|
|
249
|
+
var callbackArgumentTypes: [String] = []
|
|
245
250
|
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
246
251
|
let isStatic: Bool
|
|
247
252
|
}
|
|
@@ -255,13 +260,34 @@ private struct BoundaryMember {
|
|
|
255
260
|
/// recover it).
|
|
256
261
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
257
262
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
258
|
-
|
|
263
|
+
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
264
|
+
// A closure in any other position is diagnosed by `diagnoseClosureTypes` and left out here, since
|
|
265
|
+
// its metatype isn't a valid expression in the assertion.
|
|
266
|
+
var decodableTypes: [String] = []
|
|
267
|
+
var callbackArgumentTypes: [String] = []
|
|
268
|
+
for parameter in funcDecl.signature.parameterClause.parameters {
|
|
269
|
+
guard let closure = JSClosureType(parameter.type) else {
|
|
270
|
+
if !containsFunctionType(parameter.type) {
|
|
271
|
+
decodableTypes.append(parameter.type.trimmedDescription)
|
|
272
|
+
}
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
for parameter in closure.parameters where !containsFunctionType(parameter.type) {
|
|
276
|
+
callbackArgumentTypes.append(parameter.type.trimmedDescription)
|
|
277
|
+
}
|
|
278
|
+
if let returnType = closure.returnType, !containsFunctionType(closure.functionType.returnClause.type) {
|
|
279
|
+
decodableTypes.append(returnType)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
259
282
|
let returnType = funcDecl.signature.returnClause?.type
|
|
260
|
-
let encodableType = returnType.flatMap {
|
|
283
|
+
let encodableType = returnType.flatMap {
|
|
284
|
+
isVoidType($0) || containsFunctionType($0) ? nil : $0.trimmedDescription
|
|
285
|
+
}
|
|
261
286
|
return BoundaryMember(
|
|
262
287
|
name: funcDecl.name.text,
|
|
263
288
|
decodableTypes: decodableTypes,
|
|
264
289
|
encodableType: encodableType,
|
|
290
|
+
callbackArgumentTypes: callbackArgumentTypes,
|
|
265
291
|
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
266
292
|
)
|
|
267
293
|
}
|
|
@@ -269,7 +295,8 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
269
295
|
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
270
296
|
let binding = varDecl.bindings.first,
|
|
271
297
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
272
|
-
let type = binding.typeAnnotation?.type
|
|
298
|
+
let type = binding.typeAnnotation?.type,
|
|
299
|
+
!containsFunctionType(type) {
|
|
273
300
|
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
274
301
|
// decoded on write, so its value type is asserted in both directions.
|
|
275
302
|
let typeText = type.trimmedDescription
|
|
@@ -66,24 +66,29 @@ internal struct ConformanceAssertion {
|
|
|
66
66
|
/// direction.
|
|
67
67
|
///
|
|
68
68
|
/// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
|
|
69
|
-
/// the single return type (or a property's value type on read), or `nil` when there's none.
|
|
70
|
-
/// are
|
|
71
|
-
///
|
|
69
|
+
/// the single return type (or a property's value type on read), or `nil` when there's none.
|
|
70
|
+
/// `callbackArgumentTypes` are the parameter types of closure arguments, which are encoded when native
|
|
71
|
+
/// code calls the closure. Primitives are dropped from each; when nothing is left in any of them,
|
|
72
|
+
/// returns `nil` so the caller emits no peer. `isStatic` mirrors a `static`/`class` member so the peer
|
|
73
|
+
/// sits in the right metatype context.
|
|
72
74
|
internal func directionalConformanceAssertion(
|
|
73
75
|
name: String,
|
|
74
76
|
decodableTypes: [String],
|
|
75
77
|
encodableType: String?,
|
|
78
|
+
callbackArgumentTypes: [String] = [],
|
|
76
79
|
isStatic: Bool
|
|
77
80
|
) -> DeclSyntax? {
|
|
78
81
|
let decodables = distinctAssertableTypes(decodableTypes)
|
|
82
|
+
let callbackArguments = distinctAssertableTypes(callbackArgumentTypes)
|
|
79
83
|
let encodable = encodableType.flatMap(assertableBoundaryType)
|
|
80
|
-
guard !decodables.isEmpty || encodable != nil else {
|
|
84
|
+
guard !decodables.isEmpty || !callbackArguments.isEmpty || encodable != nil else {
|
|
81
85
|
return nil
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
// Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
|
|
85
|
-
// constrained `JavaScriptDecodable` per argument,
|
|
86
|
-
//
|
|
89
|
+
// constrained `JavaScriptDecodable` per argument, one `E0…` slot constrained `JavaScriptEncodable`
|
|
90
|
+
// per closure-argument parameter, and a single `Return` slot constrained `JavaScriptEncodable` for
|
|
91
|
+
// the return/getter value (there's only ever one, so it isn't indexed).
|
|
87
92
|
var parameters: [String] = []
|
|
88
93
|
var typeParameters: [String] = []
|
|
89
94
|
var arguments: [String] = []
|
|
@@ -92,6 +97,11 @@ internal func directionalConformanceAssertion(
|
|
|
92
97
|
typeParameters.append("_: A\(index).Type")
|
|
93
98
|
arguments.append("\(type).self")
|
|
94
99
|
}
|
|
100
|
+
for (index, type) in callbackArguments.enumerated() {
|
|
101
|
+
parameters.append("E\(index): \(javaScriptEncodableProtocolName)")
|
|
102
|
+
typeParameters.append("_: E\(index).Type")
|
|
103
|
+
arguments.append("\(type).self")
|
|
104
|
+
}
|
|
95
105
|
if let encodable {
|
|
96
106
|
parameters.append("Return: \(javaScriptEncodableProtocolName)")
|
|
97
107
|
typeParameters.append("_: Return.Type")
|
|
@@ -8,9 +8,13 @@ import SwiftSyntaxMacros
|
|
|
8
8
|
/// level, so the author switches over it exhaustively with each payload keeping its static type, and
|
|
9
9
|
/// the macro synthesizes the conversion surface that makes it a JS boundary type:
|
|
10
10
|
///
|
|
11
|
+
/// - `decodableKinds`: the union of the payloads' `decodableKinds`, the JS kinds the union can decode.
|
|
11
12
|
/// - `decode(_:in:)`: an ordered decode that tries each case's payload converter in declaration order
|
|
12
13
|
/// and returns the first case that decodes; when none does, it throws
|
|
13
14
|
/// `Exceptions.UnionCaseMismatch` naming the union, the JS kind received, and the alternatives.
|
|
15
|
+
/// Only the overload taking a borrowed `JavaScriptUnownedValue` is emitted. The overload taking an
|
|
16
|
+
/// owning `JavaScriptValue` comes from the `JavaScriptDecodable` default, which borrows the value
|
|
17
|
+
/// and calls this one.
|
|
14
18
|
/// - `encode(_:in:)`: a `switch` over the cases, encoding the payload through its own type.
|
|
15
19
|
/// - `as(_:)`: one throwing overload per case, keyed by the payload's metatype, returning that payload
|
|
16
20
|
/// (`try source.as(String.self)` is `String`; `try? source.as(String.self)` is `String?`). It unwraps
|
|
@@ -57,6 +61,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
|
|
|
57
61
|
members.append(assertionMember)
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
members.append(decodableKindsProperty(union: union))
|
|
60
65
|
members.append(decodeMethod(union: union))
|
|
61
66
|
members.append(encodeMethod(union: union))
|
|
62
67
|
members.append(contentsOf: accessorMethods(union: union))
|
|
@@ -229,31 +234,50 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
229
234
|
// MARK: - Synthesized members
|
|
230
235
|
|
|
231
236
|
/// `decode(_:in:)`: tries each case's payload converter in declaration order and returns the first
|
|
232
|
-
/// case that decodes.
|
|
237
|
+
/// case that decodes. Only the overload taking a borrowed `JavaScriptUnownedValue` is emitted; the
|
|
238
|
+
/// overload taking an owning `JavaScriptValue` comes from the `JavaScriptDecodable` default, which
|
|
239
|
+
/// borrows the value and calls this one. Each payload decode receives the same borrowed value. The
|
|
240
|
+
/// value's kind is read once, and each candidate is gated on its payload's `decodableKinds`, so a case
|
|
241
|
+
/// that can't accept that kind is skipped with a mask test instead of a thrown and discarded error. `try?` turns a candidate's failure into "try the next one" without erasing the
|
|
233
242
|
/// payload (each `payload` local keeps its concrete type); the candidate's own error is discarded, since
|
|
234
243
|
/// with several alternatives there is no single failure to surface. When no alternative accepts the
|
|
235
244
|
/// value the factory throws `Exceptions.UnionCaseMismatch`, naming the union, the JS kind of the value
|
|
236
|
-
/// received, and every payload type the union accepts.
|
|
245
|
+
/// received, and every payload type the union accepts. `JavaScriptUnownedValue` has no `kind`, so the
|
|
246
|
+
/// error reads it from an owning copy; the copy is made only on this failure path.
|
|
237
247
|
private func decodeMethod(union: UnionType) -> DeclSyntax {
|
|
238
|
-
var lines: [String] = []
|
|
248
|
+
var lines: [String] = [" let kind = JavaScriptValueKinds(of: value)"]
|
|
239
249
|
for unionCase in union.cases {
|
|
240
250
|
let payloadType = expressionType(unionCase.payloadType)
|
|
241
|
-
lines.append(" if let payload = try? \(payloadType).decode(value, in: runtime) {")
|
|
251
|
+
lines.append(" if \(payloadType).decodableKinds.contains(kind), let payload = try? \(payloadType).decode(value, in: runtime) {")
|
|
242
252
|
lines.append(" return \(unionCase.construction)")
|
|
243
253
|
lines.append(" }")
|
|
244
254
|
}
|
|
245
255
|
let expected = union.cases.map { "\"\($0.payloadType)\"" }.joined(separator: ", ")
|
|
246
|
-
let mismatch = "(unionName: \"\(union.name)\", received: value.kind.rawValue, expected: [\(expected)])"
|
|
256
|
+
let mismatch = "(unionName: \"\(union.name)\", received: value.copied(in: runtime).kind.rawValue, expected: [\(expected)])"
|
|
247
257
|
lines.append(" throw Exceptions.UnionCaseMismatch(\(mismatch))")
|
|
248
258
|
let body = lines.joined(separator: "\n")
|
|
249
259
|
return """
|
|
250
260
|
@JavaScriptActor
|
|
251
|
-
public static func decode(_ value: borrowing
|
|
261
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self {
|
|
252
262
|
\(raw: body)
|
|
253
263
|
}
|
|
254
264
|
"""
|
|
255
265
|
}
|
|
256
266
|
|
|
267
|
+
/// `decodableKinds`: the union of the payloads' kinds, so a union nested in another union, or wrapped
|
|
268
|
+
/// in an optional, is skipped as precisely as its payloads are.
|
|
269
|
+
private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
|
|
270
|
+
let kinds = union.cases.map { "\(expressionType($0.payloadType)).decodableKinds" }
|
|
271
|
+
// An option set's array literal is the union of its elements, which reads like the case list. The
|
|
272
|
+
// optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
|
|
273
|
+
let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
|
|
274
|
+
return """
|
|
275
|
+
public static var decodableKinds: JavaScriptValueKinds {
|
|
276
|
+
return \(raw: expression)
|
|
277
|
+
}
|
|
278
|
+
"""
|
|
279
|
+
}
|
|
280
|
+
|
|
257
281
|
/// `encode(_:in:)`: a `switch` over the cases, each encoding its payload through the payload type's
|
|
258
282
|
/// own `encode`. Exhaustive by construction, so a case added later without re-expansion can't slip
|
|
259
283
|
/// through silently.
|
|
@@ -170,10 +170,25 @@ struct ExportedModule: Encodable, Equatable {
|
|
|
170
170
|
let properties: [ExportedProperty]
|
|
171
171
|
let events: [ExportedEvent]
|
|
172
172
|
|
|
173
|
+
/// The classes listed in `@ExpoModule(classes: [...])`, in source order. Each is reachable from JS
|
|
174
|
+
/// as a member of the module.
|
|
175
|
+
let classes: [ExportedClass]
|
|
176
|
+
|
|
173
177
|
/// Absolute source path, matching `scan-modules`.
|
|
174
178
|
let file: String
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
/// One entry of `@ExpoModule(classes: [...])`.
|
|
182
|
+
struct ExportedClass: Encodable, Equatable {
|
|
183
|
+
/// The Swift name as spelled in the argument, e.g. `Database` or `Outer.Statement`.
|
|
184
|
+
let name: String
|
|
185
|
+
|
|
186
|
+
/// The name of the module member JS reads the class from: the matching `@SharedObject`'s `jsName`.
|
|
187
|
+
/// `nil` when the scan found no `@SharedObject` with this name, for example a class defined
|
|
188
|
+
/// outside the scanned paths.
|
|
189
|
+
let jsName: String?
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
/// A `@SharedObject` type: a JS class with an optional `@JS init` constructor plus its `@JS` members.
|
|
178
193
|
struct ExportedSharedObject: Encodable, Equatable {
|
|
179
194
|
/// The Swift class name.
|
|
@@ -307,8 +322,8 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
307
322
|
/// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
|
|
308
323
|
/// and change for different reasons. Version 2 added `events` to modules and shared objects; version 3
|
|
309
324
|
/// added `enums`; version 4 added `unions`; version 5 resolves refs, adding `refKind` and correcting
|
|
310
|
-
/// an enum ref's `typeof
|
|
311
|
-
let scanExportsSchemaVersion =
|
|
325
|
+
/// an enum ref's `typeof`; version 6 added `classes` to modules.
|
|
326
|
+
let scanExportsSchemaVersion = 6
|
|
312
327
|
|
|
313
328
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
314
329
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -23,12 +23,16 @@ struct RefIndex {
|
|
|
23
23
|
/// `name` -> the kind declaring it, plus the JS category a ref to it crosses as.
|
|
24
24
|
private var entries: [String: (kind: RefKind, jsType: JSType)] = [:]
|
|
25
25
|
|
|
26
|
+
/// A `@SharedObject`'s Swift name -> its JS name.
|
|
27
|
+
private var sharedObjectJSNames: [String: String] = [:]
|
|
28
|
+
|
|
26
29
|
init(surface: ExportedSurface) {
|
|
27
30
|
for record in surface.records {
|
|
28
31
|
entries[record.name] = (.record, .object)
|
|
29
32
|
}
|
|
30
33
|
for sharedObject in surface.sharedObjects {
|
|
31
34
|
entries[sharedObject.name] = (.sharedObject, .object)
|
|
35
|
+
sharedObjectJSNames[sharedObject.name] = sharedObject.jsName
|
|
32
36
|
}
|
|
33
37
|
for union in surface.unions {
|
|
34
38
|
// A union crosses as whichever alternative matched, so its category is only meaningful per
|
|
@@ -42,18 +46,28 @@ struct RefIndex {
|
|
|
42
46
|
|
|
43
47
|
/// How a ref to `name` should be reported, or `nil` when the scan declares no such type.
|
|
44
48
|
func lookup(_ name: String) -> (kind: RefKind, jsType: JSType)? {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return nil
|
|
52
|
-
}
|
|
53
|
-
return entries[trailing]
|
|
49
|
+
lookupQualified(name, in: entries)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// The JS name of the `@SharedObject` declared as `name`, or `nil` when the scan declares none.
|
|
53
|
+
func sharedObjectJSName(_ name: String) -> String? {
|
|
54
|
+
lookupQualified(name, in: sharedObjectJSNames)
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/// The value for `name` in `table`, falling back to its trailing component. A qualified use site
|
|
59
|
+
/// (`Media.Status`) names the same type a bare declaration did, which is how the conformance and
|
|
60
|
+
/// raw-type checks already match names.
|
|
61
|
+
private func lookupQualified<Value>(_ name: String, in table: [String: Value]) -> Value? {
|
|
62
|
+
if let value = table[name] {
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
guard let trailing = name.split(separator: ".").last.map(String.init), trailing != name else {
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
return table[trailing]
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
/// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
|
|
58
72
|
/// no raw type has nothing to go on, so it stays an object.
|
|
59
73
|
private func jsType(ofRawType rawType: TypeNode?) -> JSType {
|
|
@@ -114,6 +128,9 @@ extension ExportedSurface {
|
|
|
114
128
|
functions: module.functions.map { $0.resolvingRefs(using: index) },
|
|
115
129
|
properties: module.properties.map { $0.resolvingRefs(using: index) },
|
|
116
130
|
events: module.events.map { $0.resolvingRefs(using: index) },
|
|
131
|
+
classes: module.classes.map {
|
|
132
|
+
ExportedClass(name: $0.name, jsName: index.sharedObjectJSName($0.name))
|
|
133
|
+
},
|
|
117
134
|
file: module.file
|
|
118
135
|
)
|
|
119
136
|
},
|
|
@@ -78,6 +78,11 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
78
78
|
functions: functions,
|
|
79
79
|
properties: properties,
|
|
80
80
|
events: events,
|
|
81
|
+
// `jsName` is filled in by `ExportedSurface.resolvingRefs()`, since the `@SharedObject` can
|
|
82
|
+
// be declared in another file.
|
|
83
|
+
classes: classListArgument(of: attribute, label: "classes").map {
|
|
84
|
+
ExportedClass(name: $0, jsName: nil)
|
|
85
|
+
},
|
|
81
86
|
file: file
|
|
82
87
|
))
|
|
83
88
|
return
|
|
@@ -590,6 +595,25 @@ extension AttributeListSyntax {
|
|
|
590
595
|
}
|
|
591
596
|
}
|
|
592
597
|
|
|
598
|
+
/// The type names in an attribute's `label: [A.self, B.self]` argument, in source order. A qualified
|
|
599
|
+
/// `Outer.A.self` keeps its full spelling. Returns an empty array when the argument is absent or isn't
|
|
600
|
+
/// an array literal. A local copy of the macros' helper (the macro target can't be imported here).
|
|
601
|
+
private func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
602
|
+
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
603
|
+
let array = args.first(where: { $0.label?.text == label })?.expression.as(ArrayExprSyntax.self) else {
|
|
604
|
+
return []
|
|
605
|
+
}
|
|
606
|
+
return array.elements.compactMap { element in
|
|
607
|
+
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
608
|
+
memberAccess.declName.baseName.text == "self",
|
|
609
|
+
let base = memberAccess.base,
|
|
610
|
+
base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self) else {
|
|
611
|
+
return nil
|
|
612
|
+
}
|
|
613
|
+
return base.trimmedDescription
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
593
617
|
/// The node for a property/field value type: the annotation, else the literal default's inferred
|
|
594
618
|
/// primitive (`var n = 1` -> `Int`), else `nil`.
|
|
595
619
|
private func valueTypeNode(annotation: TypeSyntax?, initializer: ExprSyntax?) -> TypeNode? {
|
|
@@ -8,8 +8,8 @@ import SwiftSyntax
|
|
|
8
8
|
/// never silently dropped.
|
|
9
9
|
|
|
10
10
|
/// The runtime category mirroring JavaScript's `typeof`. A coarse companion to `TypeNode.kind`: every
|
|
11
|
-
/// structured object kind (array, dictionary, promise, ref) reports `object`. `
|
|
12
|
-
///
|
|
11
|
+
/// structured object kind (array, dictionary, promise, ref) reports `object`. `symbol` is included for
|
|
12
|
+
/// completeness; the scanner doesn't produce it.
|
|
13
13
|
enum JSType: String, Encodable {
|
|
14
14
|
case undefined
|
|
15
15
|
case object
|
|
@@ -37,8 +37,8 @@ enum RefKind: String, Encodable {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
indirect enum TypeNode: Equatable {
|
|
40
|
-
///
|
|
41
|
-
/// `jsType` is `boolean`/`number`/`string`.
|
|
40
|
+
/// A Swift type that crosses as a JS primitive: `Bool`, `String`, or a numeric type. `name` is the
|
|
41
|
+
/// Swift spelling; `jsType` is `boolean`/`number`/`bigint`/`string`.
|
|
42
42
|
case primitive(name: String, jsType: JSType)
|
|
43
43
|
|
|
44
44
|
/// `T?` / `T!` / `Optional<T>`.
|
|
@@ -148,13 +148,27 @@ extension TypeNode: Encodable {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/// The
|
|
152
|
-
///
|
|
151
|
+
/// The Swift types that cross as a JS primitive, each paired with that primitive. A bare name here is
|
|
152
|
+
/// a `.primitive`; anything else is a `.ref`.
|
|
153
153
|
private let primitiveJSTypes: [String: JSType] = [
|
|
154
154
|
"Bool": .boolean,
|
|
155
|
+
"String": .string,
|
|
156
|
+
// These numeric types encode to a JS `number`.
|
|
155
157
|
"Int": .number,
|
|
158
|
+
"Int8": .number,
|
|
159
|
+
"Int16": .number,
|
|
160
|
+
"Int32": .number,
|
|
161
|
+
"UInt": .number,
|
|
162
|
+
"UInt8": .number,
|
|
163
|
+
"UInt16": .number,
|
|
164
|
+
"UInt32": .number,
|
|
156
165
|
"Double": .number,
|
|
157
|
-
"
|
|
166
|
+
"Float": .number,
|
|
167
|
+
"CGFloat": .number,
|
|
168
|
+
// The 64-bit fixed-width integers decode from a `number` or a `bigint`, and always encode to a
|
|
169
|
+
// `bigint`. The reported category is the encoded one.
|
|
170
|
+
"Int64": .bigint,
|
|
171
|
+
"UInt64": .bigint,
|
|
158
172
|
]
|
|
159
173
|
|
|
160
174
|
/// Parses a `TypeSyntax` into a `TypeNode`: the single place Swift type syntax is interpreted.
|
|
@@ -183,6 +197,11 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
|
|
|
183
197
|
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
184
198
|
return typeNode(from: attributed.baseType)
|
|
185
199
|
}
|
|
200
|
+
// `(T)`: one unlabeled element in parentheses is the same type as `T`, as in `((Int) -> Void)?`.
|
|
201
|
+
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
202
|
+
let element = tuple.elements.first, element.firstName == nil {
|
|
203
|
+
return typeNode(from: element.type)
|
|
204
|
+
}
|
|
186
205
|
// A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
|
|
187
206
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
188
207
|
return nominalNode(name: identifier.name.text, generics: identifier.genericArgumentClause)
|
package/build/types.d.ts
CHANGED
|
@@ -112,9 +112,25 @@ export interface ExportedModule {
|
|
|
112
112
|
functions: ExportedFunction[];
|
|
113
113
|
properties: ExportedProperty[];
|
|
114
114
|
events: ExportedEvent[];
|
|
115
|
+
/**
|
|
116
|
+
* The classes listed in `@ExpoModule(classes: [...])`, in source order. Each is reachable from JS
|
|
117
|
+
* as a member of the module.
|
|
118
|
+
*/
|
|
119
|
+
classes: ExportedClass[];
|
|
115
120
|
/** Absolute source path. */
|
|
116
121
|
file: string;
|
|
117
122
|
}
|
|
123
|
+
/** One entry of `@ExpoModule(classes: [...])`. */
|
|
124
|
+
export interface ExportedClass {
|
|
125
|
+
/** The Swift name as spelled in the argument, e.g. `Database` or `Outer.Statement`. */
|
|
126
|
+
name: string;
|
|
127
|
+
/**
|
|
128
|
+
* The name of the module member JS reads the class from: the matching shared object's `jsName`.
|
|
129
|
+
* Absent when the scan found no shared object with this name, for example a class defined outside
|
|
130
|
+
* the scanned paths.
|
|
131
|
+
*/
|
|
132
|
+
jsName?: string;
|
|
133
|
+
}
|
|
118
134
|
/** One `@Event var`: a typed event JS listens for by name, rather than calls. */
|
|
119
135
|
export interface ExportedEvent {
|
|
120
136
|
/** The Swift property name, e.g. `onStatusChange`. */
|
|
@@ -261,4 +277,4 @@ export interface ScanExportsResult {
|
|
|
261
277
|
* as a clear error instead of silently misread fields.
|
|
262
278
|
*/
|
|
263
279
|
export declare const SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
264
|
-
export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION =
|
|
280
|
+
export declare const SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 6;
|
package/build/types.js
CHANGED
|
@@ -12,4 +12,4 @@ exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = exports.SUPPORTED_SCAN_MODULES_S
|
|
|
12
12
|
* as a clear error instead of silently misread fields.
|
|
13
13
|
*/
|
|
14
14
|
exports.SUPPORTED_SCAN_MODULES_SCHEMA_VERSION = 2;
|
|
15
|
-
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION =
|
|
15
|
+
exports.SUPPORTED_SCAN_EXPORTS_SCHEMA_VERSION = 6;
|