expo-modules-macros 0.19.1 → 0.21.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/.github/workflows/linux.yml +132 -0
- package/.github/workflows/publish.yml +160 -11
- package/.github/workflows/windows.yml +3 -28
- package/AGENTS.md +11 -6
- package/README.md +15 -9
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +23 -10
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +12 -1
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +2 -0
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +64 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +176 -91
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +3 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +14 -1
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +259 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +151 -32
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +48 -65
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +1 -1
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/build/index.d.ts +16 -6
- package/build/index.js +51 -28
- package/build/types.d.ts +86 -19
- package/build/types.js +1 -1
- package/package.json +5 -3
|
@@ -122,7 +122,7 @@ private struct EventMember {
|
|
|
122
122
|
/// Validates the declaration `@Event` is attached to and reads the event out of it. The checks
|
|
123
123
|
/// mirror what the expansion relies on: a single-binding instance `var` (the macro synthesizes a
|
|
124
124
|
/// computed getter, so `let`, accessors, and initializers are all incompatible) whose type is a
|
|
125
|
-
/// function type returning `Void` with at most one payload parameter.
|
|
125
|
+
/// function type returning `Void` with at most one payload parameter, which holds no closure.
|
|
126
126
|
private func validatedEvent(
|
|
127
127
|
of node: AttributeSyntax,
|
|
128
128
|
on declaration: some DeclSyntaxProtocol
|
|
@@ -169,6 +169,17 @@ private func validatedEvent(
|
|
|
169
169
|
throw MacroExpansionErrorMessage(
|
|
170
170
|
"@Event function type takes at most one payload parameter; combine multiple values into a single record")
|
|
171
171
|
}
|
|
172
|
+
// The payload is encoded to JavaScript, and a native closure can't be encoded.
|
|
173
|
+
if let payloadClosure = functionType.parameters.first.flatMap({ firstFunctionType(in: $0.type) }) {
|
|
174
|
+
throw DiagnosticsError(diagnostics: [
|
|
175
|
+
Diagnostic(
|
|
176
|
+
node: payloadClosure,
|
|
177
|
+
message: EventDiagnosticMessage(
|
|
178
|
+
"@Event payload can't hold a closure: the payload is encoded to JavaScript, and a native closure can't be encoded.",
|
|
179
|
+
id: "event-closure-payload"
|
|
180
|
+
))
|
|
181
|
+
])
|
|
182
|
+
}
|
|
172
183
|
|
|
173
184
|
let swiftName = identifier.identifier.text
|
|
174
185
|
return EventMember(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/// Reports each `@JS` member whose JavaScript name an earlier `@JS` member already binds on the same
|
|
6
|
+
/// JavaScript object. Pass the members of one object only: a shared object's static members bind onto
|
|
7
|
+
/// the constructor and its instance members onto the prototype, so the two sets are checked separately.
|
|
8
|
+
internal func diagnoseJSNameCollisions(
|
|
9
|
+
functions: [JSFunction],
|
|
10
|
+
properties: [JSProperty],
|
|
11
|
+
in context: some MacroExpansionContext
|
|
12
|
+
) {
|
|
13
|
+
// Each member binds with its own `setProperty` or `defineProperty` call, so of two members with one
|
|
14
|
+
// name, JavaScript would reach only one. Members are compared in source order, so the error lands on
|
|
15
|
+
// every member after the first, whichever kind each one is.
|
|
16
|
+
let members =
|
|
17
|
+
functions.map { BoundMember(jsName: $0.jsName, kind: "function", nameToken: $0.nameToken) }
|
|
18
|
+
+ properties.map { BoundMember(jsName: $0.jsName, kind: "property", nameToken: $0.nameToken) }
|
|
19
|
+
var firstMembers: [String: BoundMember] = [:]
|
|
20
|
+
|
|
21
|
+
for member in members.sorted(by: { $0.nameToken.position < $1.nameToken.position }) {
|
|
22
|
+
guard let first = firstMembers[member.jsName] else {
|
|
23
|
+
firstMembers[member.jsName] = member
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
context.diagnose(
|
|
27
|
+
Diagnostic(
|
|
28
|
+
node: member.nameToken,
|
|
29
|
+
message: JSNameCollisionMessage(
|
|
30
|
+
"The JavaScript name '\(member.jsName)' is already bound by an earlier @JS \(first.kind). A JavaScript object holds one member per name, so JavaScript could reach only one of them. Give one of them another name with @JS(\"...\")."
|
|
31
|
+
),
|
|
32
|
+
notes: [
|
|
33
|
+
Note(
|
|
34
|
+
node: Syntax(first.nameToken),
|
|
35
|
+
message: JSNameCollisionNote("'\(member.jsName)' is first bound here"))
|
|
36
|
+
]
|
|
37
|
+
))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private struct BoundMember {
|
|
42
|
+
let jsName: String
|
|
43
|
+
let kind: String
|
|
44
|
+
let nameToken: TokenSyntax
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private struct JSNameCollisionMessage: DiagnosticMessage {
|
|
48
|
+
let message: String
|
|
49
|
+
let diagnosticID = MessageID(domain: "ExpoModulesMacros", id: "js-name-collision")
|
|
50
|
+
let severity: DiagnosticSeverity = .error
|
|
51
|
+
|
|
52
|
+
init(_ message: String) {
|
|
53
|
+
self.message = message
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private struct JSNameCollisionNote: NoteMessage {
|
|
58
|
+
let message: String
|
|
59
|
+
let noteID = MessageID(domain: "ExpoModulesMacros", id: "js-name-collision-first")
|
|
60
|
+
|
|
61
|
+
init(_ message: String) {
|
|
62
|
+
self.message = message
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -441,7 +441,8 @@ internal func collectProperties(
|
|
|
441
441
|
valueType: valueType,
|
|
442
442
|
// A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
|
|
443
443
|
freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
|
|
444
|
-
isSettable: isVar && bindingIsSettable(binding)
|
|
444
|
+
isSettable: isVar && bindingIsSettable(binding),
|
|
445
|
+
nameToken: ident.identifier
|
|
445
446
|
)
|
|
446
447
|
}
|
|
447
448
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
1
2
|
import SwiftSyntax
|
|
2
3
|
import SwiftSyntaxBuilder
|
|
3
4
|
import SwiftSyntaxMacros
|
|
@@ -8,14 +9,16 @@ import SwiftSyntaxMacros
|
|
|
8
9
|
needed — and synthesizes the conversion surface from the property's static type:
|
|
9
10
|
|
|
10
11
|
- an explicit memberwise `init` (so the static factories have a single construction point),
|
|
11
|
-
- `
|
|
12
|
-
|
|
13
|
-
- `from(
|
|
14
|
-
- `
|
|
12
|
+
- `decode(_:in:)` and `encode(_:in:)` — the fast paths, reading each property straight off a JS
|
|
13
|
+
object and writing it straight into one,
|
|
14
|
+
- `from(object:appContext:)` and `toObject(appContext:)`, which forward to `decode` and `encode`,
|
|
15
|
+
- `from(dictionary:appContext:)` and `toDictionary(appContext:)` — converting each property from and
|
|
16
|
+
to a `[String: Any]` dictionary.
|
|
15
17
|
|
|
16
|
-
The type is auto-conformed to `Record` (a core protocol whose requirements are
|
|
17
|
-
|
|
18
|
-
shape — no conformance to
|
|
18
|
+
The type is auto-conformed to `Record` (a core protocol whose requirements are this surface); the
|
|
19
|
+
synthesized methods override `Record`'s reflection-based defaults, and the synthesized `decode` and
|
|
20
|
+
`encode` replace its default `JavaScriptCodable` witnesses. Author-facing shape — no conformance to
|
|
21
|
+
spell out:
|
|
19
22
|
|
|
20
23
|
@Record
|
|
21
24
|
struct Options {
|
|
@@ -30,7 +33,7 @@ import SwiftSyntaxMacros
|
|
|
30
33
|
default is required (the factories throw when the source omits it).
|
|
31
34
|
|
|
32
35
|
The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
33
|
-
(`
|
|
36
|
+
(`decode(_:in:)` / `encode(_:in:)`); the native-`Any` dictionary paths still go through the
|
|
34
37
|
public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
|
|
35
38
|
(`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
|
|
36
39
|
values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
@@ -54,10 +57,15 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
54
57
|
conformingTo protocols: [TypeSyntax],
|
|
55
58
|
in context: some MacroExpansionContext
|
|
56
59
|
) throws -> [DeclSyntax] {
|
|
57
|
-
let
|
|
58
|
-
|
|
60
|
+
let typeName: String
|
|
61
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
62
|
+
typeName = structDecl.name.text
|
|
63
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
64
|
+
typeName = classDecl.name.text
|
|
65
|
+
} else {
|
|
59
66
|
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
60
67
|
}
|
|
68
|
+
let isClass = declaration.is(ClassDeclSyntax.self)
|
|
61
69
|
|
|
62
70
|
let properties = try recordProperties(of: declaration)
|
|
63
71
|
let inheritsRecord = isClass && classHasInheritance(declaration)
|
|
@@ -98,10 +106,13 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
98
106
|
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
99
107
|
members.append(memberwiseInit(properties: properties))
|
|
100
108
|
}
|
|
101
|
-
members.append(fromJSObjectFactory(properties: properties))
|
|
109
|
+
members.append(fromJSObjectFactory(properties: properties, inheritsRecord: inheritsRecord))
|
|
102
110
|
members.append(fromDictionaryFactory(properties: properties))
|
|
103
111
|
members.append(toDictionaryMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
104
112
|
members.append(toObjectMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
113
|
+
if !inheritsRecord {
|
|
114
|
+
members.append(contentsOf: javaScriptCodableWitnesses(properties: properties, typeName: typeName))
|
|
115
|
+
}
|
|
105
116
|
return members
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -226,8 +237,21 @@ private func recordProperties(
|
|
|
226
237
|
"@Record properties must declare an explicit type — '\(ident.identifier.text)' has none"
|
|
227
238
|
)
|
|
228
239
|
}
|
|
229
|
-
// A literal-inferred type never mentions `Any
|
|
240
|
+
// A literal-inferred type never mentions `Any` or a closure, so only an explicit annotation can.
|
|
230
241
|
let annotatedType = binding.typeAnnotation?.type
|
|
242
|
+
// A closure has no `decode`, `encode` or dynamic type, and the record converts every property both
|
|
243
|
+
// ways. A JavaScript function decodes into a native closure only as a `@JS` argument; a native
|
|
244
|
+
// closure can't be encoded.
|
|
245
|
+
if let functionType = annotatedType.flatMap(firstFunctionType(in:)) {
|
|
246
|
+
throw DiagnosticsError(diagnostics: [
|
|
247
|
+
Diagnostic(
|
|
248
|
+
node: functionType,
|
|
249
|
+
message: RecordDiagnosticMessage(
|
|
250
|
+
"@Record property '\(ident.identifier.text)' can't hold a closure: a record property is also encoded to JavaScript, and a native closure can't be encoded",
|
|
251
|
+
id: "record-closure-property"
|
|
252
|
+
))
|
|
253
|
+
])
|
|
254
|
+
}
|
|
231
255
|
let freeForm = try annotatedType.flatMap { annotatedType -> FreeFormConversion? in
|
|
232
256
|
guard mentionsFreeFormAny(annotatedType) else {
|
|
233
257
|
return nil
|
|
@@ -312,15 +336,24 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
312
336
|
}
|
|
313
337
|
|
|
314
338
|
/**
|
|
315
|
-
`from(object:appContext:)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
undefined; optional properties become `nil` when undefined/null. Each property is decoded with
|
|
319
|
-
`JavaScriptDecodable.decode`, so the factory binds the `runtime` from the app context once and
|
|
320
|
-
threads it to every read. A free-form property decodes through the `JavaScriptValue` free-form
|
|
321
|
-
methods, which take the runtime too.
|
|
339
|
+
`from(object:appContext:)`. A record forwards to its own unowned `decode(_:in:)` witness (see
|
|
340
|
+
`javaScriptCodableWitnesses`), which holds the property reads. A subclass, which gets no witnesses,
|
|
341
|
+
reads the properties here, in the app context's runtime.
|
|
322
342
|
*/
|
|
323
|
-
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
343
|
+
private func fromJSObjectFactory(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
344
|
+
guard inheritsRecord else {
|
|
345
|
+
// The record declares only the unowned `decode`, so the call lends the value unowned. With an owning
|
|
346
|
+
// value it would reach an owning `decode` default, and `Record`'s converts through this factory.
|
|
347
|
+
return """
|
|
348
|
+
@JavaScriptActor
|
|
349
|
+
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
350
|
+
let runtime = try appContext.runtime
|
|
351
|
+
return try object.asValue().withUnownedValue(in: runtime) { unownedValue in
|
|
352
|
+
return try decode(unownedValue, in: runtime)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
"""
|
|
356
|
+
}
|
|
324
357
|
var lines: [String] = []
|
|
325
358
|
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
326
359
|
// otherwise leave it unused.
|
|
@@ -328,11 +361,10 @@ private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
328
361
|
lines.append(" let runtime = try appContext.runtime")
|
|
329
362
|
}
|
|
330
363
|
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
331
|
-
let body = lines.joined(separator: "\n")
|
|
332
364
|
return """
|
|
333
365
|
@JavaScriptActor
|
|
334
366
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
335
|
-
\(raw:
|
|
367
|
+
\(raw: lines.joined(separator: "\n"))
|
|
336
368
|
}
|
|
337
369
|
"""
|
|
338
370
|
}
|
|
@@ -359,69 +391,68 @@ private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
359
391
|
private func factoryBody(properties: [RecordProperty], readLines: [String]) -> String {
|
|
360
392
|
var lines: [String] = []
|
|
361
393
|
lines.append(contentsOf: readLines)
|
|
362
|
-
let initArgs = properties.map { "\($
|
|
394
|
+
let initArgs = properties.enumerated().map { "\($1.name): \(fieldLocal($0))" }.joined(separator: ", ")
|
|
363
395
|
lines.append(" return Self(\(initArgs))")
|
|
364
396
|
return lines.joined(separator: "\n")
|
|
365
397
|
}
|
|
366
398
|
|
|
367
|
-
///
|
|
368
|
-
/// the
|
|
369
|
-
///
|
|
370
|
-
|
|
399
|
+
/// The local that holds the value of the property at `index` in a factory. It's numbered rather than
|
|
400
|
+
/// named after the property, so no property can collide with it or with the factory's other parameters
|
|
401
|
+
/// and locals (`object`, `runtime`, `dictionary`, `appContext`).
|
|
402
|
+
private func fieldLocal(_ index: Int) -> String {
|
|
403
|
+
return "field\(index)"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Per-property read statements, each producing a `let field<index>` from the property of the JS object
|
|
407
|
+
/// named `object`. The property is lent to a closure as a `JavaScriptUnownedValue` and decoded there the
|
|
408
|
+
/// same way as a `@JS` argument (see `decodeCall`), so the read doesn't allocate a `JavaScriptValue`.
|
|
409
|
+
/// Only the generic free-form decode copies the value into one.
|
|
371
410
|
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
372
411
|
var lines: [String] = []
|
|
373
|
-
for property in properties {
|
|
412
|
+
for (index, property) in properties.enumerated() {
|
|
374
413
|
let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
|
|
375
|
-
let
|
|
376
|
-
let
|
|
377
|
-
switch property.freeForm {
|
|
378
|
-
case .shape(let decodeMethod, _, let isOptional, _):
|
|
379
|
-
let call = "try JavaScriptValue.\(decodeMethod)(\(valueVar), in: runtime)"
|
|
380
|
-
decode = isOptional ? "\(valueVar).isUndefined() || \(valueVar).isNull() ? nil : \(call)" : call
|
|
381
|
-
case .generic:
|
|
382
|
-
decode = "try JavaScriptValue.decodeAny(\(valueVar), as: \(exprType).self, in: runtime)"
|
|
383
|
-
case nil:
|
|
384
|
-
decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
385
|
-
}
|
|
386
|
-
lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
|
|
414
|
+
let decode = "try \(decodeCall(property.type, freeForm: property.freeForm, from: valueVar))"
|
|
415
|
+
lines.append(" let \(fieldLocal(index)) = try object.withUnownedProperty(\"\(property.key)\") { \(valueVar) in")
|
|
387
416
|
if property.isRequired {
|
|
388
|
-
lines.append("
|
|
389
|
-
lines.append("
|
|
390
|
-
lines.append("
|
|
391
|
-
lines.append("
|
|
417
|
+
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
418
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
419
|
+
lines.append(" }")
|
|
420
|
+
lines.append(" return \(decode)")
|
|
392
421
|
} else if property.isOptional {
|
|
393
|
-
// `Optional.decode` (and the free-form
|
|
394
|
-
// the read is a plain decode.
|
|
395
|
-
lines.append("
|
|
422
|
+
// `Optional.decode` (and the free-form decodes of optional shapes) already map `undefined` and
|
|
423
|
+
// `null` to `nil`, so the read is a plain decode.
|
|
424
|
+
lines.append(" return \(decode)")
|
|
396
425
|
} else {
|
|
397
|
-
lines.append("
|
|
426
|
+
lines.append(" return \(valueVar).isUndefined() ? \(property.defaultValue!) : \(decode)")
|
|
398
427
|
}
|
|
428
|
+
lines.append(" }")
|
|
399
429
|
}
|
|
400
430
|
return lines
|
|
401
431
|
}
|
|
402
432
|
|
|
403
|
-
/// Per-property read statements for the dictionary factory, each producing a `let <
|
|
404
|
-
/// property has no dynamic type to cast through, and
|
|
405
|
-
/// read unchanged.
|
|
433
|
+
/// Per-property read statements for the dictionary factory, each producing a `let field<index>` from the
|
|
434
|
+
/// raw dictionary value in `entry<index>`. A bare `Any` property has no dynamic type to cast through, and
|
|
435
|
+
/// the dictionary value already is an `Any`, so it's read unchanged.
|
|
406
436
|
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
407
437
|
var lines: [String] = []
|
|
408
|
-
for property in properties {
|
|
409
|
-
let
|
|
438
|
+
for (index, property) in properties.enumerated() {
|
|
439
|
+
let field = fieldLocal(index)
|
|
440
|
+
let entry = "entry\(index)"
|
|
410
441
|
let exprType = expressionType(property.type)
|
|
411
442
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
412
443
|
let cast = isBareAny
|
|
413
|
-
?
|
|
414
|
-
: "try \(exprType).getDynamicType().cast(\(
|
|
415
|
-
lines.append(" let \(
|
|
444
|
+
? entry
|
|
445
|
+
: "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
446
|
+
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
416
447
|
if property.isRequired {
|
|
417
|
-
lines.append(" guard let \(
|
|
448
|
+
lines.append(" guard let \(entry) else {")
|
|
418
449
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
419
450
|
lines.append(" }")
|
|
420
|
-
lines.append(" let \(
|
|
451
|
+
lines.append(" let \(field) = \(cast)")
|
|
421
452
|
} else if property.isOptional {
|
|
422
|
-
lines.append(" let \(
|
|
453
|
+
lines.append(" let \(field): \(property.type) = (\(entry) == nil || \(entry)! is NSNull) ? nil : \(cast)")
|
|
423
454
|
} else {
|
|
424
|
-
lines.append(" let \(
|
|
455
|
+
lines.append(" let \(field) = \(entry) == nil ? \(property.defaultValue!) : \(isBareAny ? entry + "!" : cast)")
|
|
425
456
|
}
|
|
426
457
|
}
|
|
427
458
|
return lines
|
|
@@ -460,51 +491,94 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
|
|
|
460
491
|
}
|
|
461
492
|
|
|
462
493
|
/**
|
|
463
|
-
`toObject(appContext:)
|
|
464
|
-
`
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
properties are written first.
|
|
494
|
+
`toObject(appContext:)`. A record forwards to its own `encode(_:in:)` witness (see
|
|
495
|
+
`javaScriptCodableWitnesses`), which holds the property writes. A subclass, which gets no witnesses,
|
|
496
|
+
overrides it to chain to `super`, so inherited properties are written first, and writes its own
|
|
497
|
+
properties in the app context's runtime.
|
|
468
498
|
*/
|
|
469
499
|
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
470
|
-
|
|
500
|
+
guard inheritsRecord else {
|
|
501
|
+
return """
|
|
502
|
+
@JavaScriptActor
|
|
503
|
+
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
504
|
+
return try Self.encode(self, in: appContext.runtime).asObject()
|
|
505
|
+
}
|
|
506
|
+
"""
|
|
507
|
+
}
|
|
471
508
|
var lines: [String] = []
|
|
472
|
-
// The
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
if !inheritsRecord || !properties.isEmpty {
|
|
509
|
+
// The runtime is only needed when there's a property to encode (the object itself comes from
|
|
510
|
+
// `super`). Binding it when unused would warn.
|
|
511
|
+
if !properties.isEmpty {
|
|
476
512
|
lines.append(" let runtime = try appContext.runtime")
|
|
477
513
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
514
|
+
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
515
|
+
lines.append(contentsOf: objectWriteLines(properties: properties, of: "self"))
|
|
516
|
+
lines.append(" return object")
|
|
517
|
+
return """
|
|
518
|
+
@JavaScriptActor
|
|
519
|
+
public override func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
520
|
+
\(raw: lines.joined(separator: "\n"))
|
|
521
|
+
}
|
|
522
|
+
"""
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
The record's own `JavaScriptDecodable` and `JavaScriptEncodable` witnesses, which convert in the
|
|
527
|
+
runtime they're given. Without them the record would convert through `Record`'s default witnesses,
|
|
528
|
+
which recover the app context from the runtime and convert in the app context's runtime.
|
|
529
|
+
|
|
530
|
+
`decode` reads each property off the JS object into a local, then constructs the record through the
|
|
531
|
+
memberwise init. Required properties throw when undefined; defaulted properties fall back to the
|
|
532
|
+
property's declared default (inlined) when undefined; optional properties become `nil` when
|
|
533
|
+
undefined/null. It's the unowned overload: an owning value decodes through `JavaScriptDecodable`'s
|
|
534
|
+
default, which forwards to it. `encode` builds the JS object, writing each property. Its parameter
|
|
535
|
+
names the type, because a class can't take `Self` as a parameter type.
|
|
536
|
+
*/
|
|
537
|
+
private func javaScriptCodableWitnesses(properties: [RecordProperty], typeName: String) -> [DeclSyntax] {
|
|
538
|
+
// An empty record still decodes only from an object, but has no property to read off it.
|
|
539
|
+
let objectLine = properties.isEmpty
|
|
540
|
+
? " _ = try value.asObject(in: runtime)"
|
|
541
|
+
: " let object = try value.asObject(in: runtime)"
|
|
542
|
+
let decodeBody = objectLine + "\n" + factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties))
|
|
543
|
+
let encodeLines = [" let object = runtime.createObject()"]
|
|
544
|
+
+ objectWriteLines(properties: properties, of: "value")
|
|
545
|
+
+ [" return object.asValue()"]
|
|
546
|
+
return [
|
|
547
|
+
"""
|
|
548
|
+
@JavaScriptActor
|
|
549
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self {
|
|
550
|
+
\(raw: decodeBody)
|
|
551
|
+
}
|
|
552
|
+
""",
|
|
553
|
+
"""
|
|
554
|
+
@JavaScriptActor
|
|
555
|
+
public static func encode(_ value: \(raw: typeName), in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
|
|
556
|
+
\(raw: encodeLines.joined(separator: "\n"))
|
|
557
|
+
}
|
|
558
|
+
""",
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// The statements that write each property of `record` (`self` or the `encode` parameter) into the
|
|
563
|
+
/// `object` local, encoding it with `JavaScriptEncodable.encode` or, for a free-form property, the
|
|
564
|
+
/// matching `JavaScriptValue` free-form method.
|
|
565
|
+
private func objectWriteLines(properties: [RecordProperty], of record: String) -> [String] {
|
|
566
|
+
return properties.map { property in
|
|
567
|
+
let value = "\(record).\(property.name)"
|
|
485
568
|
let encode: String
|
|
486
569
|
switch property.freeForm {
|
|
487
570
|
case .shape(_, let encodeMethod, let isOptional, _):
|
|
488
571
|
encode = isOptional
|
|
489
|
-
? "
|
|
490
|
-
: "try JavaScriptValue.\(encodeMethod)(
|
|
572
|
+
? "\(value) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(\(value)!, in: runtime)"
|
|
573
|
+
: "try JavaScriptValue.\(encodeMethod)(\(value), in: runtime)"
|
|
491
574
|
case .generic:
|
|
492
575
|
// The explicit `as Any` keeps an optional property from warning about its implicit coercion.
|
|
493
|
-
encode = "try JavaScriptValue.encodeAny(
|
|
576
|
+
encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
|
|
494
577
|
case nil:
|
|
495
|
-
encode = "try \(
|
|
578
|
+
encode = "try \(expressionType(property.type)).encode(\(value), in: runtime)"
|
|
496
579
|
}
|
|
497
|
-
|
|
580
|
+
return " object.setProperty(\"\(property.key)\", value: \(encode))"
|
|
498
581
|
}
|
|
499
|
-
lines.append(" return object")
|
|
500
|
-
let body = lines.joined(separator: "\n")
|
|
501
|
-
|
|
502
|
-
return """
|
|
503
|
-
@JavaScriptActor
|
|
504
|
-
public \(raw: overrideKeyword)func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
505
|
-
\(raw: body)
|
|
506
|
-
}
|
|
507
|
-
"""
|
|
508
582
|
}
|
|
509
583
|
|
|
510
584
|
// MARK: - Helpers
|
|
@@ -545,3 +619,14 @@ private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
|
|
|
545
619
|
}
|
|
546
620
|
return true
|
|
547
621
|
}
|
|
622
|
+
|
|
623
|
+
private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
624
|
+
let message: String
|
|
625
|
+
let diagnosticID: MessageID
|
|
626
|
+
let severity: DiagnosticSeverity = .error
|
|
627
|
+
|
|
628
|
+
init(_ message: String, id: String) {
|
|
629
|
+
self.message = message
|
|
630
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
@@ -93,6 +93,9 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
diagnoseJSNameCollisions(functions: instanceFunctions, properties: instanceProperties, in: context)
|
|
97
|
+
diagnoseJSNameCollisions(functions: staticFunctions, properties: staticProperties, in: context)
|
|
98
|
+
|
|
96
99
|
let lines = entries.map { " \($0)" }.joined(separator: "\n")
|
|
97
100
|
let body = entries.isEmpty
|
|
98
101
|
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
|
@@ -141,7 +141,8 @@ private struct UnionCase {
|
|
|
141
141
|
|
|
142
142
|
/// Reads and validates the union off the attached declaration. Every check is a compile error located
|
|
143
143
|
/// on the offending node: the macro must be on a non-generic `enum` with at least one case, and every
|
|
144
|
-
/// case must carry exactly one associated value, its payload type distinct from
|
|
144
|
+
/// case must carry exactly one associated value that holds no closure, its payload type distinct from
|
|
145
|
+
/// every earlier case's.
|
|
145
146
|
private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> UnionType {
|
|
146
147
|
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
|
|
147
148
|
throw MacroExpansionErrorMessage("@Union can only be applied to an enum")
|
|
@@ -197,6 +198,18 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
197
198
|
continue
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
// A payload is decoded from and encoded to JavaScript, and a native closure can't be encoded.
|
|
202
|
+
if let payloadClosure = firstFunctionType(in: parameter.type) {
|
|
203
|
+
diagnostics.append(
|
|
204
|
+
Diagnostic(
|
|
205
|
+
node: payloadClosure,
|
|
206
|
+
message: UnionDiagnosticMessage(
|
|
207
|
+
"@Union case '\(name)' can't carry a closure: the payload is also encoded to JavaScript, and a native closure can't be encoded.",
|
|
208
|
+
id: "union-closure-payload"
|
|
209
|
+
)))
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
let payloadType = parameter.type.trimmedDescription
|
|
201
214
|
// Alternatives decode in declaration order and the first success wins, so a case whose payload
|
|
202
215
|
// type repeats an earlier case's is unreachable: an exact-spelling duplicate is an error. (An
|