@expo/expo-modules-macros-plugin 0.5.1 → 0.6.1
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-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +51 -116
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +8 -16
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +43 -22
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +6 -24
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +4 -5
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +50 -27
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +88 -34
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +23 -7
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +186 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +47 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +307 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +255 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +5 -5
- package/apple/Sources/ExpoModulesScannerCLI/main.swift +4 -3
- package/package.json +1 -1
|
Binary file
|
|
@@ -50,12 +50,11 @@ internal struct JSFunction {
|
|
|
50
50
|
/// The decode-call-encode statements that form the host-function body, indented with the given
|
|
51
51
|
/// prefix. Receiver unwrap (shared objects only), then an arity guard (an exact check when every
|
|
52
52
|
/// parameter is required, otherwise a range check) throwing `Exceptions.ArgumentsRangeMismatch`;
|
|
53
|
-
/// then the decode of the always-present required prefix
|
|
54
|
-
///
|
|
55
|
-
/// `
|
|
56
|
-
/// `
|
|
57
|
-
///
|
|
58
|
-
/// has — `arguments[i]` traps past `count`, so a slot the caller didn't pass is never indexed.
|
|
53
|
+
/// then the decode of the always-present required prefix via `JavaScriptDecodable.decode` on a
|
|
54
|
+
/// zero-copy `arguments.unownedValue(at:)`; then the call and result encode via
|
|
55
|
+
/// `JavaScriptEncodable.encode`. When a trailing run of parameters is omittable the call branches
|
|
56
|
+
/// on `arguments.count`, decoding only the slots that branch actually has — `unownedValue(at:)` is
|
|
57
|
+
/// unchecked, so a slot the caller didn't pass is never indexed.
|
|
59
58
|
private func bodyStatements(receiver: Receiver, indent: String) -> String {
|
|
60
59
|
let required = requiredArgumentCount
|
|
61
60
|
let maximum = parameters.count
|
|
@@ -91,23 +90,21 @@ internal struct JSFunction {
|
|
|
91
90
|
// No omittable trailing run: a single flat call with every argument decoded.
|
|
92
91
|
lines.append(contentsOf: callAndEncodeLines(receiver: receiver, arity: maximum, decodingFrom: required))
|
|
93
92
|
} else {
|
|
94
|
-
// One call shape per accepted arity.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if returnType != nil {
|
|
100
|
-
lines.append("let result = switch arguments.count {")
|
|
101
|
-
} else {
|
|
102
|
-
lines.append("switch arguments.count {")
|
|
93
|
+
// One call shape per accepted arity, branching on `arguments.count`. A branch decodes a
|
|
94
|
+
// trailing slot before the call, so this is a `switch` statement (not an expression): a
|
|
95
|
+
// value-returning function declares `result` up front and each branch assigns it.
|
|
96
|
+
if let returnType {
|
|
97
|
+
lines.append("let result: \(expressionType(returnType))")
|
|
103
98
|
}
|
|
99
|
+
lines.append("switch arguments.count {")
|
|
104
100
|
for arity in required...maximum {
|
|
105
101
|
let label = arity == maximum ? "default:" : "case \(arity):"
|
|
106
102
|
lines.append(label)
|
|
107
103
|
for index in required..<arity {
|
|
108
104
|
lines.append(" " + decodeStatement(at: index))
|
|
109
105
|
}
|
|
110
|
-
|
|
106
|
+
let assignment = returnType != nil ? "result = " : ""
|
|
107
|
+
lines.append(" \(assignment)\(callExpression(receiver: receiver, arity: arity))")
|
|
111
108
|
}
|
|
112
109
|
lines.append("}")
|
|
113
110
|
lines.append(contentsOf: encodeResultLines())
|
|
@@ -119,18 +116,14 @@ internal struct JSFunction {
|
|
|
119
116
|
.joined(separator: "\n")
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
/// `let arg<index> = …` decoding the slot at `index` by its static type
|
|
123
|
-
///
|
|
124
|
-
///
|
|
125
|
-
///
|
|
126
|
-
///
|
|
119
|
+
/// `let arg<index> = …` decoding the slot at `index` by its static type through
|
|
120
|
+
/// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
|
|
121
|
+
/// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
|
|
122
|
+
/// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
|
|
123
|
+
/// path would use.
|
|
127
124
|
private func decodeStatement(at index: Int) -> String {
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
return "let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()"
|
|
131
|
-
}
|
|
132
|
-
let exprType = expressionType(type)
|
|
133
|
-
return "let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)"
|
|
125
|
+
let exprType = expressionType(parameters[index].type.trimmedDescription)
|
|
126
|
+
return "let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)"
|
|
134
127
|
}
|
|
135
128
|
|
|
136
129
|
/// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
|
|
@@ -178,17 +171,17 @@ internal struct JSFunction {
|
|
|
178
171
|
return lines
|
|
179
172
|
}
|
|
180
173
|
|
|
181
|
-
/// Encode the `result` local back to JS and return it
|
|
182
|
-
///
|
|
183
|
-
///
|
|
174
|
+
/// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode`, the same
|
|
175
|
+
/// for every type. Unlike the decode side there's no primitive fast path: `encode` produces the same
|
|
176
|
+
/// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
177
|
+
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
178
|
+
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
179
|
+
/// no-return function returns `.undefined` instead.
|
|
184
180
|
private func encodeResultLines() -> [String] {
|
|
185
181
|
guard let returnType else {
|
|
186
182
|
return ["return .undefined"]
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
return ["return result.toJavaScriptValue(in: runtime)"]
|
|
190
|
-
}
|
|
191
|
-
return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
|
|
184
|
+
return ["return try \(expressionType(returnType)).encode(result, in: runtime)"]
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
@@ -201,10 +194,6 @@ internal struct JSFunction {
|
|
|
201
194
|
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
202
195
|
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
203
196
|
/// captures nothing of the instance: it recovers the typed receiver from the JS `this` per call.
|
|
204
|
-
/// `appContext` is captured **weak** (and guarded) so it doesn't form a real retain cycle through
|
|
205
|
-
/// the app context. When no argument or return value goes through the dynamic-type converter the
|
|
206
|
-
/// body never references `appContext`, so the capture and guard are omitted to avoid the
|
|
207
|
-
/// unused-capture warning.
|
|
208
197
|
func decorateStatements(receiver: Receiver) -> String {
|
|
209
198
|
// Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
|
|
210
199
|
// `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
|
|
@@ -215,42 +204,19 @@ internal struct JSFunction {
|
|
|
215
204
|
// on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
|
|
216
205
|
// shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
|
|
217
206
|
// escapes into the task anyway.
|
|
218
|
-
let captures = receiver.captureClause
|
|
207
|
+
let captures = receiver.captureClause
|
|
219
208
|
let parameters =
|
|
220
209
|
isAsync
|
|
221
210
|
? "this, arguments"
|
|
222
211
|
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
223
212
|
|
|
224
213
|
let object = receiver.decoratedObject
|
|
225
|
-
if usesAppContext {
|
|
226
|
-
return """
|
|
227
|
-
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
228
|
-
guard let appContext else {
|
|
229
|
-
throw Exceptions.AppContextLost()
|
|
230
|
-
}
|
|
231
|
-
\(bodyStatements(receiver: receiver, indent: " "))
|
|
232
|
-
}
|
|
233
|
-
"""
|
|
234
|
-
}
|
|
235
214
|
return """
|
|
236
215
|
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
237
216
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
238
217
|
}
|
|
239
218
|
"""
|
|
240
219
|
}
|
|
241
|
-
|
|
242
|
-
/// True when the host-function body references `appContext` — i.e. some parameter or the return
|
|
243
|
-
/// type lacks a fast accessor and decodes/encodes through `getDynamicType()`, which threads
|
|
244
|
-
/// `appContext` in.
|
|
245
|
-
private var usesAppContext: Bool {
|
|
246
|
-
if parameters.contains(where: { fastDecodeAccessor(for: $0.type.trimmedDescription) == nil }) {
|
|
247
|
-
return true
|
|
248
|
-
}
|
|
249
|
-
if let returnType, fastDecodeAccessor(for: returnType) == nil {
|
|
250
|
-
return true
|
|
251
|
-
}
|
|
252
|
-
return false
|
|
253
|
-
}
|
|
254
220
|
}
|
|
255
221
|
|
|
256
222
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
@@ -263,9 +229,8 @@ internal struct JSFunction {
|
|
|
263
229
|
///
|
|
264
230
|
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
265
231
|
/// unwrapped from the JS `this` for a shared object. The getter reads `<callee>.<name>` and the setter
|
|
266
|
-
/// writes `<callee>.<name> = …`. Decode/encode of the value
|
|
267
|
-
///
|
|
268
|
-
/// the `getDynamicType()` converter).
|
|
232
|
+
/// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
|
|
233
|
+
/// `JavaScriptEncodable.encode`, the same uniform path as functions.
|
|
269
234
|
internal struct JSProperty {
|
|
270
235
|
let swiftName: String
|
|
271
236
|
let jsName: String
|
|
@@ -282,18 +247,11 @@ internal struct JSProperty {
|
|
|
282
247
|
/// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
|
|
283
248
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
284
249
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
285
|
-
/// instance
|
|
286
|
-
/// are omitted from an accessor whose body never references it (a primitive value, decoded/encoded
|
|
287
|
-
/// without the dynamic converter), to avoid the unused-capture warning. Getter and setter are gated
|
|
288
|
-
/// independently.
|
|
250
|
+
/// instance. Getter and setter are gated independently.
|
|
289
251
|
func decorateStatements(receiver: Receiver) -> String {
|
|
290
252
|
let descriptorName = "\(swiftName)Descriptor"
|
|
291
253
|
let callee = receiver.callee
|
|
292
254
|
let object = receiver.decoratedObject
|
|
293
|
-
// A primitive value type encodes/decodes without `getDynamicType()`, so its accessor body never
|
|
294
|
-
// references `appContext`. `nil` (untyped) goes through the dynamic-less `toJavaScriptValue`
|
|
295
|
-
// getter, which also doesn't use it.
|
|
296
|
-
let usesAppContext = valueType.map { fastDecodeAccessor(for: $0) == nil } ?? false
|
|
297
255
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
298
256
|
// reads `self` directly. The unwrap leads each accessor body.
|
|
299
257
|
let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
|
|
@@ -302,39 +260,28 @@ internal struct JSProperty {
|
|
|
302
260
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
303
261
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
304
262
|
|
|
305
|
-
// Getter: read `<callee>.<name>` and encode the result back to JS
|
|
263
|
+
// Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
|
|
264
|
+
// type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
|
|
265
|
+
// no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
|
|
306
266
|
let getEncode: String
|
|
307
|
-
if let valueType
|
|
308
|
-
getEncode = "return \(callee).\(swiftName)
|
|
309
|
-
} else if let valueType {
|
|
310
|
-
getEncode =
|
|
311
|
-
"return try \(expressionType(valueType)).getDynamicType().castToJS(\(callee).\(swiftName), appContext: appContext, in: runtime)"
|
|
267
|
+
if let valueType {
|
|
268
|
+
getEncode = "return try \(expressionType(valueType)).encode(\(callee).\(swiftName), in: runtime)"
|
|
312
269
|
} else {
|
|
313
|
-
// No known type: fall back to converting whatever `<callee>.<name>` is. This only happens when
|
|
314
|
-
// the declaration has neither an annotation nor a literal default, which is rare for a stored
|
|
315
|
-
// var.
|
|
316
270
|
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
317
271
|
}
|
|
318
272
|
lines.append(
|
|
319
|
-
accessorClosure(
|
|
320
|
-
descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
|
|
273
|
+
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)"))
|
|
321
274
|
|
|
322
|
-
// Setter: decode argument 0 by the static type and write `<callee>.<name>`. A
|
|
323
|
-
// a known value type; when the type couldn't be inferred the property is bound
|
|
324
|
-
// settable var with neither an annotation nor a literal default is rare and can't
|
|
275
|
+
// Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
|
|
276
|
+
// typed setter needs a known value type; when the type couldn't be inferred the property is bound
|
|
277
|
+
// getter-only (a settable var with neither an annotation nor a literal default is rare and can't
|
|
278
|
+
// be decoded).
|
|
325
279
|
if isSettable, let valueType {
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
setDecode = "\(callee).\(swiftName) = try arguments.unownedValue(at: 0).\(accessor)()"
|
|
329
|
-
} else {
|
|
330
|
-
let exprType = expressionType(valueType)
|
|
331
|
-
setDecode =
|
|
332
|
-
"\(callee).\(swiftName) = try \(exprType).getDynamicType().cast(jsValue: arguments[0], appContext: appContext) as! \(exprType)"
|
|
333
|
-
}
|
|
280
|
+
let exprType = expressionType(valueType)
|
|
281
|
+
let setDecode = "\(callee).\(swiftName) = try \(exprType).decode(arguments.unownedValue(at: 0), in: runtime)"
|
|
334
282
|
lines.append(
|
|
335
283
|
accessorClosure(
|
|
336
|
-
descriptorName, "set", receiver: receiver,
|
|
337
|
-
body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
284
|
+
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
338
285
|
}
|
|
339
286
|
|
|
340
287
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
@@ -346,15 +293,13 @@ internal struct JSProperty {
|
|
|
346
293
|
}
|
|
347
294
|
|
|
348
295
|
/// One `descriptor.setProperty("get"/"set") { … }` accessor entry. The capture list follows the
|
|
349
|
-
/// receiver
|
|
350
|
-
/// when `usesAppContext`, adds `appContext` weak + guarded (matching the function bindings);
|
|
351
|
-
/// otherwise the guard is omitted so a primitive accessor doesn't warn on an unused capture.
|
|
296
|
+
/// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
|
|
352
297
|
private func accessorClosure(
|
|
353
|
-
_ descriptorName: String, _ key: String, receiver: Receiver,
|
|
298
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, body: String
|
|
354
299
|
) -> String {
|
|
355
|
-
let captures = receiver.captureClause
|
|
356
|
-
// Indent each line of a (possibly multi-line) body to sit one level inside the closure
|
|
357
|
-
//
|
|
300
|
+
let captures = receiver.captureClause
|
|
301
|
+
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
302
|
+
// `\(body)` interpolation would only indent the first line.
|
|
358
303
|
let indentedBody = body
|
|
359
304
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
360
305
|
.map { " \($0)" }
|
|
@@ -365,16 +310,6 @@ internal struct JSProperty {
|
|
|
365
310
|
// `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
|
|
366
311
|
// a shared object unwraps it in the body.
|
|
367
312
|
let parameters = "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
368
|
-
if usesAppContext {
|
|
369
|
-
return """
|
|
370
|
-
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
371
|
-
guard let appContext else {
|
|
372
|
-
throw Exceptions.AppContextLost()
|
|
373
|
-
}
|
|
374
|
-
\(indentedBody)
|
|
375
|
-
}
|
|
376
|
-
"""
|
|
377
|
-
}
|
|
378
313
|
return """
|
|
379
314
|
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
380
315
|
\(indentedBody)
|
|
@@ -403,7 +338,7 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
403
338
|
let body = decorateBody(functions: functions, properties: properties, receiver: .module)
|
|
404
339
|
return """
|
|
405
340
|
@JavaScriptActor
|
|
406
|
-
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime
|
|
341
|
+
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
407
342
|
\(raw: body)
|
|
408
343
|
}
|
|
409
344
|
"""
|
|
@@ -424,7 +359,7 @@ internal func buildDecorateSharedObject(
|
|
|
424
359
|
let body = decorateBody(functions: functions, properties: properties, receiver: .sharedObject(typeName: typeName))
|
|
425
360
|
return """
|
|
426
361
|
@JavaScriptActor
|
|
427
|
-
public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime
|
|
362
|
+
public override class func _decorateSharedObject(prototype: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
428
363
|
\(raw: body)
|
|
429
364
|
}
|
|
430
365
|
"""
|
|
@@ -12,8 +12,9 @@ internal struct JSConstructor {
|
|
|
12
12
|
self.parameters = Array(initDecl.signature.parameterClause.parameters)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/// The body statements, indented with `indent`: arity guard, per-argument decode
|
|
16
|
-
///
|
|
15
|
+
/// The body statements, indented with `indent`: arity guard, per-argument decode through
|
|
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
18
|
private func bodyStatements(typeName: String, indent: String) -> String {
|
|
18
19
|
var lines: [String] = []
|
|
19
20
|
|
|
@@ -26,15 +27,8 @@ internal struct JSConstructor {
|
|
|
26
27
|
|
|
27
28
|
var callArguments: [String] = []
|
|
28
29
|
for (index, parameter) in parameters.enumerated() {
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
if let accessor = fastDecodeAccessor(for: type) {
|
|
32
|
-
lines.append("let arg\(index) = try arguments.unownedValue(at: \(index)).\(accessor)()")
|
|
33
|
-
} else {
|
|
34
|
-
let exprType = expressionType(type)
|
|
35
|
-
lines.append(
|
|
36
|
-
"let arg\(index) = try \(exprType).getDynamicType().cast(jsValue: arguments[\(index)], appContext: appContext) as! \(exprType)")
|
|
37
|
-
}
|
|
30
|
+
let exprType = expressionType(parameter.type.trimmedDescription)
|
|
31
|
+
lines.append("let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)")
|
|
38
32
|
|
|
39
33
|
let label = parameter.firstName.text
|
|
40
34
|
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
@@ -48,14 +42,12 @@ internal struct JSConstructor {
|
|
|
48
42
|
.joined(separator: "\n")
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
///
|
|
52
|
-
///
|
|
53
|
-
/// concrete type's metatype; the body returns the concrete instance, which promotes to the base
|
|
54
|
-
/// `SharedObject?` return type. `this`/`appContext` may go unreferenced, which is harmless.
|
|
45
|
+
/// Builds an instance from the JS arguments. Overrides the base `SharedObject` class method; returns
|
|
46
|
+
/// the concrete instance, which promotes to the base `SharedObject?` return type.
|
|
55
47
|
func buildConstructor(typeName: String) -> DeclSyntax {
|
|
56
48
|
return """
|
|
57
49
|
@JavaScriptActor
|
|
58
|
-
public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime
|
|
50
|
+
public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
|
|
59
51
|
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
60
52
|
}
|
|
61
53
|
"""
|
|
@@ -6,11 +6,13 @@ import SwiftSyntaxMacros
|
|
|
6
6
|
/// corresponding `Function` / `AsyncFunction` / `Property` / `Constructor` registrations; that part
|
|
7
7
|
/// of the expansion lives in those macros.
|
|
8
8
|
///
|
|
9
|
-
/// On its own, `@JS` emits one thing: a never-called peer that asserts
|
|
10
|
-
/// boundary is
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
///
|
|
9
|
+
/// On its own, `@JS` emits one thing: a never-called peer that asserts each type crossing the JS
|
|
10
|
+
/// boundary is convertible in the direction it travels — arguments are `JavaScriptDecodable`, return
|
|
11
|
+
/// values are `JavaScriptEncodable` (a settable property's value type is both). Because it's a
|
|
12
|
+
/// **peer** of the marked member, a non-conforming type produces a compile error located on the
|
|
13
|
+
/// user's own `@JS` declaration rather than on the enclosing `@ExpoModule`. The assertion mechanism
|
|
14
|
+
/// itself is shared (see `directionalConformanceAssertion`); `@JS` only supplies the boundary types,
|
|
15
|
+
/// split by direction, that it reads off the declaration.
|
|
14
16
|
///
|
|
15
17
|
/// Usage:
|
|
16
18
|
///
|
|
@@ -29,8 +31,10 @@ public struct JSMacro: PeerMacro {
|
|
|
29
31
|
in context: some MacroExpansionContext
|
|
30
32
|
) throws -> [DeclSyntax] {
|
|
31
33
|
guard let member = boundaryMember(of: declaration),
|
|
32
|
-
let assertion =
|
|
33
|
-
|
|
34
|
+
let assertion = directionalConformanceAssertion(
|
|
35
|
+
name: member.name,
|
|
36
|
+
decodableTypes: member.decodableTypes,
|
|
37
|
+
encodableType: member.encodableType,
|
|
34
38
|
isStatic: member.isStatic
|
|
35
39
|
) else {
|
|
36
40
|
return []
|
|
@@ -39,37 +43,54 @@ public struct JSMacro: PeerMacro {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
/// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer
|
|
43
|
-
///
|
|
46
|
+
/// What an assertion peer needs about the `@JS` member it sits beside: a name (to keep the peer unique
|
|
47
|
+
/// among siblings), the boundary types split by conversion direction, and whether the member is
|
|
48
|
+
/// type-level. Arguments (and a settable property's incoming value) are decoded; return values (and a
|
|
49
|
+
/// property's outgoing value) are encoded, so each is asserted against the protocol for its direction.
|
|
44
50
|
private struct BoundaryMember {
|
|
45
51
|
let name: String
|
|
46
|
-
///
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
/// Types decoded from JS: function/constructor arguments, and a settable property's value type.
|
|
53
|
+
let decodableTypes: [String]
|
|
54
|
+
/// The single type encoded to JS: a function's return type, or a property's value type on read;
|
|
55
|
+
/// `nil` when the member produces nothing JS-visible (a `Void` function).
|
|
56
|
+
let encodableType: String?
|
|
49
57
|
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
50
58
|
let isStatic: Bool
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
/// Reads the boundary member off a `@JS` declaration
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
61
|
+
/// Reads the boundary member off a `@JS` declaration, splitting its types by conversion direction. A
|
|
62
|
+
/// function contributes its parameter types (decodable) and its return type when non-Void (encodable);
|
|
63
|
+
/// a property contributes its value type as encodable (the getter) and also as decodable when settable
|
|
64
|
+
/// (the setter). Composed types (`[Int]`, `String?`, …) are kept verbatim, their conditional
|
|
65
|
+
/// conformances transitively constraining the elements. Returns `nil` for declaration kinds `@JS`
|
|
66
|
+
/// doesn't read types from, or a property whose type isn't spelled out (a syntactic macro can't
|
|
67
|
+
/// recover it).
|
|
57
68
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
58
69
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
let decodableTypes = funcDecl.signature.parameterClause.parameters.map { $0.type.trimmedDescription }
|
|
71
|
+
let returnType = funcDecl.signature.returnClause?.type
|
|
72
|
+
let encodableType = returnType.flatMap { isVoidType($0) ? nil : $0.trimmedDescription }
|
|
73
|
+
return BoundaryMember(
|
|
74
|
+
name: funcDecl.name.text,
|
|
75
|
+
decodableTypes: decodableTypes,
|
|
76
|
+
encodableType: encodableType,
|
|
77
|
+
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
78
|
+
)
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
67
82
|
let binding = varDecl.bindings.first,
|
|
68
83
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
69
84
|
let type = binding.typeAnnotation?.type {
|
|
85
|
+
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
86
|
+
// decoded on write, so its value type is asserted in both directions.
|
|
87
|
+
let typeText = type.trimmedDescription
|
|
88
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
89
|
+
let isSettable = isVar && bindingIsSettable(binding)
|
|
70
90
|
return BoundaryMember(
|
|
71
91
|
name: identifier.identifier.text,
|
|
72
|
-
|
|
92
|
+
decodableTypes: isSettable ? [typeText] : [],
|
|
93
|
+
encodableType: typeText,
|
|
73
94
|
isStatic: isTypeLevel(varDecl.modifiers)
|
|
74
95
|
)
|
|
75
96
|
}
|
|
@@ -246,10 +246,11 @@ extension AttributeListSyntax {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
/// A type spelled so it's valid in expression position (before `.
|
|
250
|
-
/// Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
251
|
-
/// `!` is rewritten to `?` (`T!` and `T?` are both
|
|
252
|
-
/// treats identically). Other type spellings pass through
|
|
249
|
+
/// A type spelled so it's valid in expression position (before `.decode`/`.encode`, before
|
|
250
|
+
/// `.getDynamicType()`, or after `as!`). Implicitly-unwrapped optionals (`T!`) are only allowed in
|
|
251
|
+
/// type-annotation position, so a trailing `!` is rewritten to `?` (`T!` and `T?` are both
|
|
252
|
+
/// `Optional<T>`, which the conversion layer treats identically). Other type spellings pass through
|
|
253
|
+
/// unchanged.
|
|
253
254
|
internal func expressionType(_ type: String) -> String {
|
|
254
255
|
guard type.hasSuffix("!") else {
|
|
255
256
|
return type
|
|
@@ -292,7 +293,7 @@ internal func collectProperties(
|
|
|
292
293
|
/// a computed property is settable only when it declares an explicit `set` accessor. A getter-only
|
|
293
294
|
/// computed property (`{ get }` or a single getter body) stays read-only. `willSet`/`didSet`
|
|
294
295
|
/// observers imply stored storage, which is also settable.
|
|
295
|
-
|
|
296
|
+
internal func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
296
297
|
guard let accessorBlock = binding.accessorBlock else {
|
|
297
298
|
return true
|
|
298
299
|
}
|
|
@@ -310,22 +311,3 @@ private func bindingIsSettable(_ binding: PatternBindingSyntax) -> Bool {
|
|
|
310
311
|
return false
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
/// The throwing `JavaScriptUnownedValue` accessor that decodes the given primitive type directly
|
|
315
|
-
/// (`asDouble()` for `Double`, etc.), bypassing the dynamic-type converter. Returns `nil` for types
|
|
316
|
-
/// without a dedicated accessor (arrays, records, optionals, shared objects, other numeric widths),
|
|
317
|
-
/// which decode through `getDynamicType().cast(...)`.
|
|
318
|
-
func fastDecodeAccessor(for type: String) -> String? {
|
|
319
|
-
switch type {
|
|
320
|
-
case "Bool":
|
|
321
|
-
return "asBool"
|
|
322
|
-
case "Int":
|
|
323
|
-
return "asInt"
|
|
324
|
-
case "Double":
|
|
325
|
-
return "asDouble"
|
|
326
|
-
case "String":
|
|
327
|
-
return "asString"
|
|
328
|
-
default:
|
|
329
|
-
return nil
|
|
330
|
-
}
|
|
331
|
-
}
|
|
@@ -45,14 +45,13 @@ internal enum Receiver {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/// The capture-clause fragment (with a trailing space, or empty when nothing is captured). A module
|
|
48
|
-
/// captures `self` strong; a shared object captures nothing of the instance.
|
|
49
|
-
|
|
50
|
-
func captureClause(usesAppContext: Bool) -> String {
|
|
48
|
+
/// captures `self` strong; a shared object captures nothing of the instance.
|
|
49
|
+
var captureClause: String {
|
|
51
50
|
switch self {
|
|
52
51
|
case .module:
|
|
53
|
-
return
|
|
52
|
+
return "[self] "
|
|
54
53
|
case .sharedObject:
|
|
55
|
-
return
|
|
54
|
+
return ""
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|