@expo/expo-modules-macros-plugin 0.5.1 → 0.6.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-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +41 -104
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +5 -11
- 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
|
|
@@ -119,18 +118,14 @@ internal struct JSFunction {
|
|
|
119
118
|
.joined(separator: "\n")
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/// `let arg<index> = …` decoding the slot at `index` by its static type
|
|
123
|
-
///
|
|
124
|
-
///
|
|
125
|
-
///
|
|
126
|
-
///
|
|
121
|
+
/// `let arg<index> = …` decoding the slot at `index` by its static type through
|
|
122
|
+
/// `JavaScriptDecodable.decode` on the borrowed `JavaScriptUnownedValue` — no owning value, no
|
|
123
|
+
/// `jsi::Value` copy, no `Any` boxing, no force-cast; it returns the concrete type directly. A
|
|
124
|
+
/// primitive's `decode` is `@inlinable` and lowers to the same direct accessor a hand-rolled fast
|
|
125
|
+
/// path would use.
|
|
127
126
|
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)"
|
|
127
|
+
let exprType = expressionType(parameters[index].type.trimmedDescription)
|
|
128
|
+
return "let arg\(index) = try \(exprType).decode(arguments.unownedValue(at: \(index)), in: runtime)"
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
/// The `<callee>.<name>(...)` call for the given arity. Slots `0..<arity` are passed their decoded
|
|
@@ -178,17 +173,17 @@ internal struct JSFunction {
|
|
|
178
173
|
return lines
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
/// Encode the `result` local back to JS and return it
|
|
182
|
-
///
|
|
183
|
-
///
|
|
176
|
+
/// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode`, the same
|
|
177
|
+
/// for every type. Unlike the decode side there's no primitive fast path: `encode` produces the same
|
|
178
|
+
/// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
179
|
+
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
180
|
+
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
181
|
+
/// no-return function returns `.undefined` instead.
|
|
184
182
|
private func encodeResultLines() -> [String] {
|
|
185
183
|
guard let returnType else {
|
|
186
184
|
return ["return .undefined"]
|
|
187
185
|
}
|
|
188
|
-
|
|
189
|
-
return ["return result.toJavaScriptValue(in: runtime)"]
|
|
190
|
-
}
|
|
191
|
-
return ["return try \(expressionType(returnType)).getDynamicType().castToJS(result, appContext: appContext, in: runtime)"]
|
|
186
|
+
return ["return try \(expressionType(returnType)).encode(result, in: runtime)"]
|
|
192
187
|
}
|
|
193
188
|
|
|
194
189
|
/// The `setProperty` statement that installs this function on the JS object. The decode-call-encode
|
|
@@ -201,10 +196,6 @@ internal struct JSFunction {
|
|
|
201
196
|
/// the host-function closure is what keeps the native callable alive for as long as JS can invoke
|
|
202
197
|
/// it; its lifetime is bounded by the JS VM's garbage collection of the object. A shared object
|
|
203
198
|
/// 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
199
|
func decorateStatements(receiver: Receiver) -> String {
|
|
209
200
|
// Synchronous `@JS` bindings bind through the unowned-`this` `setProperty` overload, which hands
|
|
210
201
|
// `this` in as a borrowed `JavaScriptUnownedValue` instead of allocating an owning
|
|
@@ -215,42 +206,19 @@ internal struct JSFunction {
|
|
|
215
206
|
// on a shorthand `{ [capture] name, name in }` parameter. Async functions keep the untyped
|
|
216
207
|
// shorthand and the owning-`this` overload: there is no unowned-`this` async variant and the buffer
|
|
217
208
|
// escapes into the task anyway.
|
|
218
|
-
let captures = receiver.captureClause
|
|
209
|
+
let captures = receiver.captureClause
|
|
219
210
|
let parameters =
|
|
220
211
|
isAsync
|
|
221
212
|
? "this, arguments"
|
|
222
213
|
: "(this: borrowing JavaScriptUnownedValue, arguments: consuming JavaScriptValuesBuffer)"
|
|
223
214
|
|
|
224
215
|
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
216
|
return """
|
|
236
217
|
\(object).setProperty("\(jsName)") { \(captures)\(parameters) in
|
|
237
218
|
\(bodyStatements(receiver: receiver, indent: " "))
|
|
238
219
|
}
|
|
239
220
|
"""
|
|
240
221
|
}
|
|
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
222
|
}
|
|
255
223
|
|
|
256
224
|
/// A `@JS var` collected for **direct JSI binding**. Instead of describing the property with a
|
|
@@ -263,9 +231,8 @@ internal struct JSFunction {
|
|
|
263
231
|
///
|
|
264
232
|
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
265
233
|
/// 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).
|
|
234
|
+
/// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
|
|
235
|
+
/// `JavaScriptEncodable.encode`, the same uniform path as functions.
|
|
269
236
|
internal struct JSProperty {
|
|
270
237
|
let swiftName: String
|
|
271
238
|
let jsName: String
|
|
@@ -282,18 +249,11 @@ internal struct JSProperty {
|
|
|
282
249
|
/// the closure-taking `setProperty(_:)` overload — with the read/write body inlined into each
|
|
283
250
|
/// closure — and installs it with `object.defineProperty(name, descriptor:)`. Capture matches the
|
|
284
251
|
/// 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.
|
|
252
|
+
/// instance. Getter and setter are gated independently.
|
|
289
253
|
func decorateStatements(receiver: Receiver) -> String {
|
|
290
254
|
let descriptorName = "\(swiftName)Descriptor"
|
|
291
255
|
let callee = receiver.callee
|
|
292
256
|
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
257
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
298
258
|
// reads `self` directly. The unwrap leads each accessor body.
|
|
299
259
|
let unwrap = receiver.unwrapStatement.map { "\($0)\n" } ?? ""
|
|
@@ -302,39 +262,28 @@ internal struct JSProperty {
|
|
|
302
262
|
lines.append("let \(descriptorName) = runtime.createObject()")
|
|
303
263
|
lines.append("\(descriptorName).setProperty(\"enumerable\", value: true)")
|
|
304
264
|
|
|
305
|
-
// Getter: read `<callee>.<name>` and encode the result back to JS
|
|
265
|
+
// Getter: read `<callee>.<name>` and encode the result back to JS through `encode`. When the value
|
|
266
|
+
// type couldn't be inferred (no annotation and no literal default, rare for a stored var) there's
|
|
267
|
+
// no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
|
|
306
268
|
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)"
|
|
269
|
+
if let valueType {
|
|
270
|
+
getEncode = "return try \(expressionType(valueType)).encode(\(callee).\(swiftName), in: runtime)"
|
|
312
271
|
} 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
272
|
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
317
273
|
}
|
|
318
274
|
lines.append(
|
|
319
|
-
accessorClosure(
|
|
320
|
-
descriptorName, "get", receiver: receiver, usesAppContext: usesAppContext, body: "\(unwrap)\(getEncode)"))
|
|
275
|
+
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)"))
|
|
321
276
|
|
|
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
|
|
277
|
+
// Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
|
|
278
|
+
// typed setter needs a known value type; when the type couldn't be inferred the property is bound
|
|
279
|
+
// getter-only (a settable var with neither an annotation nor a literal default is rare and can't
|
|
280
|
+
// be decoded).
|
|
325
281
|
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
|
-
}
|
|
282
|
+
let exprType = expressionType(valueType)
|
|
283
|
+
let setDecode = "\(callee).\(swiftName) = try \(exprType).decode(arguments.unownedValue(at: 0), in: runtime)"
|
|
334
284
|
lines.append(
|
|
335
285
|
accessorClosure(
|
|
336
|
-
descriptorName, "set", receiver: receiver,
|
|
337
|
-
body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
286
|
+
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
338
287
|
}
|
|
339
288
|
|
|
340
289
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
@@ -346,15 +295,13 @@ internal struct JSProperty {
|
|
|
346
295
|
}
|
|
347
296
|
|
|
348
297
|
/// 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.
|
|
298
|
+
/// receiver: a module captures `self` strong; a shared object captures nothing of the instance.
|
|
352
299
|
private func accessorClosure(
|
|
353
|
-
_ descriptorName: String, _ key: String, receiver: Receiver,
|
|
300
|
+
_ descriptorName: String, _ key: String, receiver: Receiver, body: String
|
|
354
301
|
) -> String {
|
|
355
|
-
let captures = receiver.captureClause
|
|
356
|
-
// Indent each line of a (possibly multi-line) body to sit one level inside the closure
|
|
357
|
-
//
|
|
302
|
+
let captures = receiver.captureClause
|
|
303
|
+
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
304
|
+
// `\(body)` interpolation would only indent the first line.
|
|
358
305
|
let indentedBody = body
|
|
359
306
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
360
307
|
.map { " \($0)" }
|
|
@@ -365,16 +312,6 @@ internal struct JSProperty {
|
|
|
365
312
|
// `borrowing JavaScriptUnownedValue` selects the unowned-`this` overload. A module ignores `this`;
|
|
366
313
|
// a shared object unwraps it in the body.
|
|
367
314
|
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
315
|
return """
|
|
379
316
|
\(descriptorName).setProperty("\(key)") { \(captures)\(parameters) in
|
|
380
317
|
\(indentedBody)
|
|
@@ -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)")
|
|
@@ -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
|
}
|
|
@@ -29,10 +29,13 @@ import SwiftSyntaxMacros
|
|
|
29
29
|
an optional type makes it nullable and optional, and a non-optional property without a
|
|
30
30
|
default is required (the factories throw when the source omits it).
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
33
|
+
(`from(object:)` / `toObject(appContext:)`); the native-`Any` dictionary paths still go through the
|
|
34
|
+
public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
|
|
35
|
+
(`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
|
|
36
|
+
values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
37
|
+
user modules without any internal core symbols. Every property type must therefore conform to both
|
|
38
|
+
`AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`.
|
|
36
39
|
|
|
37
40
|
For classes that inherit from another `@Record`-annotated class, the synthesized
|
|
38
41
|
methods chain to `super` so inherited properties are handled first.
|
|
@@ -60,14 +63,15 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
60
63
|
|
|
61
64
|
var members: [DeclSyntax] = []
|
|
62
65
|
|
|
63
|
-
// A single never-called member that makes the compiler verify each property type is
|
|
64
|
-
//
|
|
65
|
-
// own named assertion inside, so the compiler's
|
|
66
|
-
// property (see `typeConformanceAssertions`). Emitted
|
|
67
|
-
// this clear "requires that '…' conform to '…'" error
|
|
68
|
-
// "no member '
|
|
66
|
+
// A single never-called member that makes the compiler verify each property type is convertible
|
|
67
|
+
// both ways (the JS-value paths call `decode`/`encode`; the native-`Any` dictionary paths call
|
|
68
|
+
// the dynamic-type API). Each property keeps its own named assertion inside, so the compiler's
|
|
69
|
+
// conformance diagnostic names the offending property (see `typeConformanceAssertions`). Emitted
|
|
70
|
+
// first so that, for a non-conforming type, this clear "requires that '…' conform to '…'" error
|
|
71
|
+
// is reported ahead of the noisier "no member 'decode'"/"getDynamicType" errors from the
|
|
72
|
+
// conversion code below.
|
|
69
73
|
let assertions = properties.map { ConformanceAssertion(name: $0.name, types: [$0.type]) }
|
|
70
|
-
if let assertionMember = typeConformanceAssertions(for: assertions) {
|
|
74
|
+
if let assertionMember = typeConformanceAssertions(for: assertions, constraint: recordFieldProtocolName) {
|
|
71
75
|
members.append(assertionMember)
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -131,8 +135,9 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
131
135
|
*/
|
|
132
136
|
private struct RecordProperty {
|
|
133
137
|
let name: String
|
|
134
|
-
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build
|
|
135
|
-
///
|
|
138
|
+
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
|
|
139
|
+
/// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
|
|
140
|
+
/// dictionary paths, `getDynamicType()`) conversions.
|
|
136
141
|
let type: String
|
|
137
142
|
/// The default-value expression verbatim (`0`, `""`, `[]`), or `nil` when the property has none.
|
|
138
143
|
/// Inlined into the memberwise init and the factories' omitted-property branch so the synthesized
|
|
@@ -155,7 +160,7 @@ private struct RecordProperty {
|
|
|
155
160
|
/**
|
|
156
161
|
Discovers the record's properties: every stored `var`/`let` binding that is not `static`,
|
|
157
162
|
`private`, `fileprivate`, `lazy`, or computed. Each property must declare an explicit type
|
|
158
|
-
annotation, since the synthesized conversions
|
|
163
|
+
annotation, since the synthesized conversions name the type (e.g. `Type.decode(…)`).
|
|
159
164
|
*/
|
|
160
165
|
private func recordProperties(
|
|
161
166
|
of declaration: some DeclGroupSyntax
|
|
@@ -271,11 +276,20 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
271
276
|
/**
|
|
272
277
|
`from(object:appContext:)` — reads each property off the `JavaScriptObject` into a local,
|
|
273
278
|
then constructs the record through the memberwise init. Required properties throw when
|
|
274
|
-
undefined; defaulted properties fall back to the property's declared default (
|
|
275
|
-
|
|
279
|
+
undefined; defaulted properties fall back to the property's declared default (inlined) when
|
|
280
|
+
undefined; optional properties become `nil` when undefined/null. Each property is decoded with
|
|
281
|
+
`JavaScriptDecodable.decode`, so the factory binds the `runtime` from the app context once and
|
|
282
|
+
threads it to every read.
|
|
276
283
|
*/
|
|
277
284
|
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
278
|
-
|
|
285
|
+
var lines: [String] = []
|
|
286
|
+
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
287
|
+
// otherwise leave it unused.
|
|
288
|
+
if !properties.isEmpty {
|
|
289
|
+
lines.append(" let runtime = try appContext.runtime")
|
|
290
|
+
}
|
|
291
|
+
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
292
|
+
let body = lines.joined(separator: "\n")
|
|
279
293
|
return """
|
|
280
294
|
@JavaScriptActor
|
|
281
295
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
@@ -311,23 +325,25 @@ private func factoryBody(properties: [RecordProperty], readLines: [String]) -> S
|
|
|
311
325
|
return lines.joined(separator: "\n")
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
/// Per-property read statements for the JS-object factory, each producing a `let <name
|
|
328
|
+
/// Per-property read statements for the JS-object factory, each producing a `let <name>` by decoding
|
|
329
|
+
/// the JS value with `JavaScriptDecodable.decode` (recovering the app context from `runtime` itself).
|
|
315
330
|
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
316
331
|
var lines: [String] = []
|
|
317
332
|
for property in properties {
|
|
318
333
|
let valueVar = "\(property.name)JSValue"
|
|
319
334
|
let exprType = expressionType(property.type)
|
|
320
|
-
let
|
|
335
|
+
let decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
321
336
|
lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
|
|
322
337
|
if property.isRequired {
|
|
323
338
|
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
324
339
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
|
|
325
340
|
lines.append(" }")
|
|
326
|
-
lines.append(" let \(property.name) = \(
|
|
341
|
+
lines.append(" let \(property.name) = \(decode)")
|
|
327
342
|
} else if property.isOptional {
|
|
328
|
-
|
|
343
|
+
// `Optional.decode` already maps `undefined`/`null` to `nil`, so the read is a plain decode.
|
|
344
|
+
lines.append(" let \(property.name) = \(decode)")
|
|
329
345
|
} else {
|
|
330
|
-
lines.append(" let \(property.name) = \(valueVar).isUndefined() ? \(property.defaultValue!) : \(
|
|
346
|
+
lines.append(" let \(property.name) = \(valueVar).isUndefined() ? \(property.defaultValue!) : \(decode)")
|
|
331
347
|
}
|
|
332
348
|
}
|
|
333
349
|
return lines
|
|
@@ -388,20 +404,27 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
|
|
|
388
404
|
}
|
|
389
405
|
|
|
390
406
|
/**
|
|
391
|
-
`toObject(appContext:)` — builds a `JavaScriptObject` directly,
|
|
392
|
-
`
|
|
393
|
-
|
|
407
|
+
`toObject(appContext:)` — builds a `JavaScriptObject` directly, encoding each property with
|
|
408
|
+
`JavaScriptEncodable.encode`. The fast write path mirroring `from(object:)`. The `runtime` is bound
|
|
409
|
+
from the app context once and threaded to every write. Subclasses chain to `super` so inherited
|
|
410
|
+
properties are written first.
|
|
394
411
|
*/
|
|
395
412
|
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
396
413
|
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
397
414
|
var lines: [String] = []
|
|
415
|
+
// The base case needs the runtime to create the object; the inheriting case only needs it when
|
|
416
|
+
// there's a property to encode (it chains to `super` for the object itself). Binding it when unused
|
|
417
|
+
// would warn.
|
|
418
|
+
if !inheritsRecord || !properties.isEmpty {
|
|
419
|
+
lines.append(" let runtime = try appContext.runtime")
|
|
420
|
+
}
|
|
398
421
|
if inheritsRecord {
|
|
399
422
|
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
400
423
|
} else {
|
|
401
|
-
lines.append(" let object =
|
|
424
|
+
lines.append(" let object = runtime.createObject()")
|
|
402
425
|
}
|
|
403
426
|
for property in properties {
|
|
404
|
-
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).
|
|
427
|
+
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).encode(self.\(property.name), in: runtime))")
|
|
405
428
|
}
|
|
406
429
|
lines.append(" return object")
|
|
407
430
|
let body = lines.joined(separator: "\n")
|