expo-modules-macros 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +28 -4
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +3 -2
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +23 -10
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +13 -1
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +71 -54
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +11 -4
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +16 -5
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +13 -1
- package/apple/Sources/ExpoModulesMacros/TupleTypes.swift +234 -0
- package/apple/Sources/ExpoModulesMacros/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +104 -41
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +16 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +9 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesScanner/Exports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +47 -4
- package/build/types.d.ts +32 -1
- package/build/types.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -25,12 +25,14 @@ This package lists them as optional dependencies, and their `os`, `cpu` and (on
|
|
|
25
25
|
# Macros
|
|
26
26
|
|
|
27
27
|
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none.
|
|
28
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels
|
|
28
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels. Outside a module or shared object, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol or the module's default isolation, the error points into the generated code, so mark the function `nonisolated`. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
29
29
|
- **`@Event(_ name: String? = nil, sync: Bool = false)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `sync: true` dispatches inline instead of asynchronously.
|
|
30
30
|
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses.
|
|
31
31
|
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
32
32
|
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first.
|
|
33
33
|
|
|
34
|
+
A tuple can cross the boundary as a `@JS` argument, result or property, a `@Record` property, a `@Union` payload, an `@Event` payload, or a parameter or result of a closure argument that isn't `async`. An unlabeled tuple such as `(Int, String)` maps to an array with one value per element, and a labeled tuple such as `(x: Double, y: Double)` to an object with one property per label. A tuple must have two or more elements, all labeled or none, and each element must be convertible. Tuples nest in other tuples, optionals, arrays and `String`-keyed dictionaries. `@ExpoModule` and `@SharedObject` also resolve a type alias of a tuple, such as `typealias Span = (start: Int, end: Int)`, when the class body declares it. A macro sees only the declaration it is attached to, so an alias declared anywhere else doesn't resolve.
|
|
35
|
+
|
|
34
36
|
How that looks in a module:
|
|
35
37
|
|
|
36
38
|
```swift
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -146,13 +146,20 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
|
|
|
146
146
|
// parameters instead would send non-`Sendable` values to the `@JavaScriptActor` closure, which
|
|
147
147
|
// Swift 6 rejects. The closure captures nothing, so it needs no allocation. Without parameters
|
|
148
148
|
// there is nothing to emplace, and `invokeAsync` takes the parameters variadically.
|
|
149
|
-
|
|
149
|
+
//
|
|
150
|
+
// A result type that holds a tuple can't be passed as `returning:`, so a sync closure returning one
|
|
151
|
+
// decodes it through a `decoding:` closure, which only the `emplacing` variant takes. With no
|
|
152
|
+
// parameters it emplaces nothing.
|
|
153
|
+
let returnTuple = closure.returnType.flatMap { tupleConversion(of: $0) }
|
|
154
|
+
let emplaces = !closure.isAsync && (!parameters.isEmpty || returnTuple != nil)
|
|
150
155
|
var arguments = parameters
|
|
151
156
|
if emplaces {
|
|
152
157
|
let values = parameters.count == 1 ? "p0" : "(" + parameters.joined(separator: ", ") + ")"
|
|
153
158
|
arguments = [values, "argumentCount: \(parameters.count)"]
|
|
154
159
|
}
|
|
155
|
-
if let
|
|
160
|
+
if let returnTuple {
|
|
161
|
+
arguments.append("decoding: \(elementClosure(returning: returnTuple.decodeExpression(of: "value")))")
|
|
162
|
+
} else if let returnType = closure.returnType {
|
|
156
163
|
arguments.append("returning: \(expressionType(returnType)).self")
|
|
157
164
|
}
|
|
158
165
|
let argumentList = arguments.joined(separator: ", ")
|
|
@@ -168,9 +175,14 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
|
|
|
168
175
|
|
|
169
176
|
if emplaces {
|
|
170
177
|
var lines = [header, " \(call) { _values, _arguments, _runtime in"]
|
|
171
|
-
for index in parameters.
|
|
178
|
+
for (index, parameter) in closure.parameters.enumerated() {
|
|
172
179
|
let value = parameters.count == 1 ? "_values" : "_values.\(index)"
|
|
173
|
-
|
|
180
|
+
// A parameter that holds a tuple has no `encode(_:into:in:)`, so its encoded value is moved in.
|
|
181
|
+
if let tuple = tupleConversion(of: parameter.type) {
|
|
182
|
+
lines.append(" _arguments.emplace(try \(tuple.encodeExpression(of: value, in: "_runtime")), at: \(index))")
|
|
183
|
+
} else {
|
|
184
|
+
lines.append(" try _arguments.emplace(\(value), at: \(index), in: _runtime)")
|
|
185
|
+
}
|
|
174
186
|
}
|
|
175
187
|
return lines + [" }", "}"]
|
|
176
188
|
}
|
|
@@ -336,6 +348,18 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
336
348
|
)
|
|
337
349
|
)
|
|
338
350
|
)
|
|
351
|
+
} else if closure.isAsync && tupleConversion(of: type) != nil {
|
|
352
|
+
// `invokeAsync` takes its arguments and result as `JavaScriptEncodable` / `JavaScriptDecodable`
|
|
353
|
+
// types, and a type that holds a tuple can't conform.
|
|
354
|
+
context.diagnose(
|
|
355
|
+
Diagnostic(
|
|
356
|
+
node: type,
|
|
357
|
+
message: ClosureDiagnosticMessage(
|
|
358
|
+
"An async @JS closure argument can't take or return a tuple. Use a @Record type, or a closure that isn't async.",
|
|
359
|
+
id: "js-closure-async-tuple"
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
)
|
|
339
363
|
}
|
|
340
364
|
}
|
|
341
365
|
|
|
@@ -499,11 +499,12 @@ private let resultSlotClosureParameters =
|
|
|
499
499
|
+ "result: borrowing JavaScriptValue.Slot, runtime: borrowing JavaScriptRuntime)"
|
|
500
500
|
|
|
501
501
|
/// The statement that encodes `valueExpression` of a boundary type into the result slot `result`. A type
|
|
502
|
-
/// that mentions `Any`
|
|
502
|
+
/// that mentions `Any` and a tuple have no `encode(_:into:in:)`, so their encode result is moved into
|
|
503
|
+
/// the slot.
|
|
503
504
|
private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String)
|
|
504
505
|
-> String
|
|
505
506
|
{
|
|
506
|
-
if freeForm != nil {
|
|
507
|
+
if freeForm != nil || tupleConversion(of: type) != nil {
|
|
507
508
|
return "result.emplace(try \(encodeCall(type, freeForm: freeForm, of: valueExpression)))"
|
|
508
509
|
}
|
|
509
510
|
return "try \(expressionType(type)).encode(\(valueExpression), into: result, in: runtime)"
|
|
@@ -53,11 +53,19 @@ public struct EventMacro: AccessorMacro {
|
|
|
53
53
|
// The closure's parameter and return types are inferred from the property's declared type
|
|
54
54
|
// through the getter, so the body never has to spell the payload type. A sync event calls
|
|
55
55
|
// `emitSync` (inline dispatch, JS thread only); the default calls the scheduling `emit`.
|
|
56
|
+
// A payload that holds a tuple can't conform to the payload protocols, so it's passed with a
|
|
57
|
+
// closure that encodes it.
|
|
56
58
|
let emitMethod = event.isSync ? "emitSync" : "emit"
|
|
57
|
-
let closure
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
let closure: String
|
|
60
|
+
if let tuple = event.payloadType.flatMap({ tupleConversion(of: $0) }) {
|
|
61
|
+
let encode = elementClosure(returning: tuple.encodeExpression(of: "value"))
|
|
62
|
+
closure =
|
|
63
|
+
"{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload, encode: \(encode)) }"
|
|
64
|
+
} else if event.hasPayload {
|
|
65
|
+
closure = "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
|
|
66
|
+
} else {
|
|
67
|
+
closure = "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
|
|
68
|
+
}
|
|
61
69
|
return [
|
|
62
70
|
"""
|
|
63
71
|
get {
|
|
@@ -85,15 +93,20 @@ extension EventMacro: PeerMacro {
|
|
|
85
93
|
// through `self.emit`). Named after the member so the member shows up in either diagnostic,
|
|
86
94
|
// and asserting the enclosing type by its spelled name so the conformance error names the
|
|
87
95
|
// user's type rather than 'Self'.
|
|
96
|
+
//
|
|
97
|
+
// A payload that holds a tuple is encoded element by element (see `TupleConversion`), so the types
|
|
98
|
+
// in it are asserted `JavaScriptEncodable`, which the encode closure needs, in its place.
|
|
88
99
|
let name = event.swiftName
|
|
89
|
-
let
|
|
100
|
+
let tupleElementTypes = event.payloadType.flatMap { tupleConversion(of: $0)?.conformingTypes }
|
|
101
|
+
let payloadConstraint = tupleElementTypes == nil ? jsConvertibleProtocolName : javaScriptEncodableProtocolName
|
|
102
|
+
let payloads =
|
|
103
|
+
tupleElementTypes.map { distinctAssertableTypes($0, constraint: payloadConstraint) }
|
|
104
|
+
?? event.payloadType.flatMap { assertableBoundaryType($0, constraint: payloadConstraint) }.map { [$0] } ?? []
|
|
90
105
|
let owner = enclosingTypeName(in: context) ?? "Self"
|
|
91
106
|
let emitterParameter = opaqueMetatypeParameter(constraint: eventEmitterProtocolName)
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: "func \(name)(\(emitterParameter)) {}"
|
|
96
|
-
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
107
|
+
let payloadParameters = payloads.map { _ in opaqueMetatypeParameter(constraint: payloadConstraint) }
|
|
108
|
+
let helper = "func \(name)(\((payloadParameters + [emitterParameter]).joined(separator: ", "))) {}"
|
|
109
|
+
let call = "\(name)(\((payloads + [owner]).map { "\($0).self" }.joined(separator: ", ")))"
|
|
97
110
|
let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
|
|
98
111
|
return [
|
|
99
112
|
"""
|
|
@@ -49,9 +49,15 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
49
49
|
// DSL entry.
|
|
50
50
|
var functions: [JSFunction] = []
|
|
51
51
|
var properties: [JSProperty] = []
|
|
52
|
+
var assertions: [MemberConformanceAssertion] = []
|
|
53
|
+
let aliases = TupleTypeAliases(in: classDecl.memberBlock)
|
|
52
54
|
|
|
53
55
|
for member in classDecl.memberBlock.members {
|
|
54
|
-
|
|
56
|
+
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
57
|
+
let decl = aliases.resolving(member.decl)
|
|
58
|
+
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
59
|
+
assertions.append(assertion)
|
|
60
|
+
}
|
|
55
61
|
|
|
56
62
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
57
63
|
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
@@ -126,6 +132,12 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
126
132
|
emitted.append(buildDecorateJavaScriptObject(functions: functions, properties: properties))
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
// The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
|
|
136
|
+
// `jsConformanceAssertionBody`).
|
|
137
|
+
if let assertion = classConformanceAssertions(assertions) {
|
|
138
|
+
emitted.append(assertion)
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
return emitted
|
|
130
142
|
}
|
|
131
143
|
}
|
|
@@ -37,21 +37,43 @@ public struct JSMacro: PeerMacro {
|
|
|
37
37
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
38
38
|
diagnoseAsyncVariantOption(of: node, on: declaration, in: context)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
decodableTypes: member.decodableTypes,
|
|
44
|
-
encodableType: member.encodableType,
|
|
45
|
-
callbackArgumentTypes: member.callbackArgumentTypes,
|
|
46
|
-
isStatic: member.isStatic
|
|
47
|
-
)
|
|
48
|
-
else {
|
|
40
|
+
// In an `@ExpoModule` or `@SharedObject` class, the class macro emits the assertion instead: it sees
|
|
41
|
+
// the type aliases declared in the class body, which this macro can't (see `TupleTypeAliases`).
|
|
42
|
+
guard !isInBindingClass(context), let assertion = jsConformanceAssertion(for: declaration) else {
|
|
49
43
|
return []
|
|
50
44
|
}
|
|
51
45
|
return [assertion]
|
|
52
46
|
}
|
|
53
47
|
}
|
|
54
48
|
|
|
49
|
+
/// The conformance-assertion peer for a `@JS` member, or `nil` when the member has no type to assert.
|
|
50
|
+
private func jsConformanceAssertion(for declaration: some DeclSyntaxProtocol) -> DeclSyntax? {
|
|
51
|
+
guard let member = boundaryMember(of: declaration) else {
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
return directionalConformanceAssertion(name: member.name, types: member.types, isStatic: member.isStatic)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The conformance assertion of a `@JS` member, for an `@ExpoModule` or `@SharedObject` class to fold
|
|
58
|
+
/// into its `_assertTypesConformance()` (see `classConformanceAssertions`), or `nil` when the member has
|
|
59
|
+
/// no type to assert.
|
|
60
|
+
internal func jsConformanceAssertionBody(for declaration: some DeclSyntaxProtocol) -> MemberConformanceAssertion? {
|
|
61
|
+
guard let member = boundaryMember(of: declaration) else {
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
return memberConformanceAssertion(name: member.name, types: member.types)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// True when the member is declared directly in an `@ExpoModule` or `@SharedObject` class, whose macro
|
|
68
|
+
/// binds it.
|
|
69
|
+
private func isInBindingClass(_ context: some MacroExpansionContext) -> Bool {
|
|
70
|
+
guard let classDecl = context.lexicalContext.first?.as(ClassDeclSyntax.self) else {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
return classDecl.attributes.firstAttribute(named: "ExpoModule") != nil
|
|
74
|
+
|| classDecl.attributes.firstAttribute(named: "SharedObject") != nil
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
/// Emits the diagnostic for `@JS(.concurrent)` on a member that can't take it. The option maps to
|
|
56
78
|
/// Swift's `@concurrent`, which requires an `async` function: a synchronous member has nowhere to
|
|
57
79
|
/// suspend, and a property or initializer can't be async at all. Diagnosing here points at the
|
|
@@ -323,28 +345,24 @@ private struct JSFixItMessage: FixItMessage {
|
|
|
323
345
|
}
|
|
324
346
|
}
|
|
325
347
|
|
|
326
|
-
/// What an assertion
|
|
327
|
-
///
|
|
328
|
-
/// type-level. Arguments (and a settable property's incoming value) are decoded;
|
|
329
|
-
/// property's outgoing value) are encoded, so each is asserted against the
|
|
348
|
+
/// What an assertion needs about a `@JS` member: a name (which names the helper), the boundary types in
|
|
349
|
+
/// signature order, each with the protocol for the direction it travels and its argument label, and
|
|
350
|
+
/// whether the member is type-level. Arguments (and a settable property's incoming value) are decoded;
|
|
351
|
+
/// return values (and a property's outgoing value) are encoded, so each is asserted against the
|
|
352
|
+
/// protocol for its direction.
|
|
330
353
|
private struct BoundaryMember {
|
|
331
354
|
let name: String
|
|
332
|
-
///
|
|
333
|
-
|
|
334
|
-
let decodableTypes: [String]
|
|
335
|
-
/// The single type encoded to JS: a function's return type, or a property's value type on read;
|
|
336
|
-
/// `nil` when the member produces nothing JS-visible (a `Void` function).
|
|
337
|
-
let encodableType: String?
|
|
338
|
-
/// The parameter types of closure arguments, encoded to JS when native code calls the closure.
|
|
339
|
-
var callbackArgumentTypes: [String] = []
|
|
355
|
+
/// The types crossing the boundary, in signature order.
|
|
356
|
+
let types: [AssertedType]
|
|
340
357
|
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
341
358
|
let isStatic: Bool
|
|
342
359
|
}
|
|
343
360
|
|
|
344
|
-
/// Reads the boundary member off a `@JS` declaration,
|
|
345
|
-
///
|
|
346
|
-
/// an initializer contributes its parameter types; a property
|
|
347
|
-
///
|
|
361
|
+
/// Reads the boundary member off a `@JS` declaration, with each type's conversion direction. A function
|
|
362
|
+
/// contributes its parameter types (decodable) under their argument labels and its return type when
|
|
363
|
+
/// non-Void (encodable) under `returning`; an initializer contributes its parameter types; a property
|
|
364
|
+
/// contributes its value type, unlabeled, as decodable when settable (the setter) and as encodable (the
|
|
365
|
+
/// getter). Composed types (`[Int]`, `String?`,
|
|
348
366
|
/// …) are kept verbatim, their conditional conformances transitively constraining the elements. A type
|
|
349
367
|
/// that mentions `Any` is left out: it converts through the free-form methods, or is diagnosed by
|
|
350
368
|
/// `diagnoseFreeFormTypes`. Returns `nil` for declaration kinds `@JS` doesn't read types from, a
|
|
@@ -352,30 +370,28 @@ private struct BoundaryMember {
|
|
|
352
370
|
/// mentions `Any`.
|
|
353
371
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
354
372
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
355
|
-
|
|
356
|
-
let returnType = funcDecl.signature.returnClause?.type
|
|
357
|
-
|
|
358
|
-
|
|
373
|
+
var types = argumentBoundaryTypes(of: funcDecl.signature.parameterClause.parameters)
|
|
374
|
+
if let returnType = funcDecl.signature.returnClause?.type,
|
|
375
|
+
!isVoidType(returnType) && !containsFunctionType(returnType) && !mentionsFreeFormAny(returnType)
|
|
376
|
+
{
|
|
377
|
+
types.append(
|
|
378
|
+
AssertedType(
|
|
379
|
+
label: "returning",
|
|
380
|
+
type: returnType.trimmedDescription,
|
|
381
|
+
constraint: javaScriptEncodableProtocolName
|
|
382
|
+
)
|
|
383
|
+
)
|
|
359
384
|
}
|
|
360
|
-
return BoundaryMember(
|
|
361
|
-
name: funcDecl.name.text,
|
|
362
|
-
decodableTypes: argumentTypes.decodable,
|
|
363
|
-
encodableType: encodableType,
|
|
364
|
-
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
365
|
-
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
366
|
-
)
|
|
385
|
+
return BoundaryMember(name: funcDecl.name.text, types: types, isStatic: isTypeLevel(funcDecl.modifiers))
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
370
389
|
// A constructor decodes its arguments like a function does, and the instance it returns isn't
|
|
371
390
|
// encoded. The name is escaped because it also names the assertion's nested helper, and `init` is
|
|
372
391
|
// a keyword.
|
|
373
|
-
let argumentTypes = argumentBoundaryTypes(of: initDecl.signature.parameterClause.parameters)
|
|
374
392
|
return BoundaryMember(
|
|
375
393
|
name: "`init`",
|
|
376
|
-
|
|
377
|
-
encodableType: nil,
|
|
378
|
-
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
394
|
+
types: argumentBoundaryTypes(of: initDecl.signature.parameterClause.parameters),
|
|
379
395
|
isStatic: false
|
|
380
396
|
)
|
|
381
397
|
}
|
|
@@ -395,10 +411,11 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
395
411
|
let typeText = type.trimmedDescription
|
|
396
412
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
397
413
|
let isSettable = isVar && !isLazy(varDecl.modifiers) && bindingIsSettable(binding)
|
|
414
|
+
let decodable = AssertedType(label: nil, type: typeText, constraint: javaScriptDecodableProtocolName)
|
|
415
|
+
let encodable = AssertedType(label: nil, type: typeText, constraint: javaScriptEncodableProtocolName)
|
|
398
416
|
return BoundaryMember(
|
|
399
417
|
name: identifier.identifier.text,
|
|
400
|
-
|
|
401
|
-
encodableType: typeText,
|
|
418
|
+
types: (isSettable ? [decodable] : []) + [encodable],
|
|
402
419
|
isStatic: isTypeLevel(varDecl.modifiers)
|
|
403
420
|
)
|
|
404
421
|
}
|
|
@@ -406,18 +423,16 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
406
423
|
return nil
|
|
407
424
|
}
|
|
408
425
|
|
|
409
|
-
/// The boundary types of a function's or constructor's arguments
|
|
410
|
-
/// a closure argument instead contributes its parameter types (encoded
|
|
411
|
-
/// its return type (decoded).
|
|
412
|
-
private func argumentBoundaryTypes(
|
|
413
|
-
of parameters: FunctionParameterListSyntax
|
|
414
|
-
) -> (decodable: [String], callbackArguments: [String]) {
|
|
426
|
+
/// The boundary types of a function's or constructor's arguments, under their argument labels: an
|
|
427
|
+
/// argument is decoded from JS, and a closure argument instead contributes its parameter types (encoded
|
|
428
|
+
/// when native code calls it) and its return type (decoded).
|
|
429
|
+
private func argumentBoundaryTypes(of parameters: FunctionParameterListSyntax) -> [AssertedType] {
|
|
415
430
|
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
416
431
|
// A closure in any other position is diagnosed by `diagnoseClosureTypes` and left out here, since
|
|
417
432
|
// its metatype isn't a valid expression in the assertion.
|
|
418
|
-
var
|
|
419
|
-
var callbackArgumentTypes: [String] = []
|
|
433
|
+
var types: [AssertedType] = []
|
|
420
434
|
for parameter in parameters {
|
|
435
|
+
let label = parameter.firstName.tokenKind == .wildcard ? nil : parameter.firstName.text
|
|
421
436
|
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
422
437
|
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
423
438
|
let specified = SpecifiedType(parameter.type)
|
|
@@ -426,23 +441,25 @@ private func argumentBoundaryTypes(
|
|
|
426
441
|
}
|
|
427
442
|
guard let closure = JSClosureType(specified.type) else {
|
|
428
443
|
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
429
|
-
|
|
444
|
+
let type = specified.type.trimmedDescription
|
|
445
|
+
types.append(AssertedType(label: label, type: type, constraint: javaScriptDecodableProtocolName))
|
|
430
446
|
}
|
|
431
447
|
continue
|
|
432
448
|
}
|
|
433
449
|
// A closure type that mentions `Any` is reported by `diagnoseClosureTypes` instead.
|
|
434
450
|
for parameter in closure.parameters
|
|
435
451
|
where !containsFunctionType(parameter.type) && !mentionsFreeFormAny(parameter.type) {
|
|
436
|
-
|
|
452
|
+
let type = parameter.type.trimmedDescription
|
|
453
|
+
types.append(AssertedType(label: label, type: type, constraint: javaScriptEncodableProtocolName))
|
|
437
454
|
}
|
|
438
455
|
let closureReturnType = closure.functionType.returnClause.type
|
|
439
456
|
if let returnType = closure.returnType,
|
|
440
457
|
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType)
|
|
441
458
|
{
|
|
442
|
-
|
|
459
|
+
types.append(AssertedType(label: label, type: returnType, constraint: javaScriptDecodableProtocolName))
|
|
443
460
|
}
|
|
444
461
|
}
|
|
445
|
-
return
|
|
462
|
+
return types
|
|
446
463
|
}
|
|
447
464
|
|
|
448
465
|
/// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
|
|
@@ -493,23 +493,30 @@ internal func expressionType(_ type: String) -> String {
|
|
|
493
493
|
|
|
494
494
|
/// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
|
|
495
495
|
/// as it appears after `try`. A type that mentions `Any` can't conform to `JavaScriptDecodable`, so it
|
|
496
|
-
/// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names
|
|
497
|
-
///
|
|
498
|
-
/// `T?`).
|
|
496
|
+
/// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names, and a tuple
|
|
497
|
+
/// through the `JavaScriptValue` tuple methods (see `TupleConversion`); every other type decodes through
|
|
498
|
+
/// its own static `decode`, spelled in expression position (`T!` rewritten to `T?`).
|
|
499
499
|
internal func decodeCall(_ type: String, freeForm: FreeFormConversion?, from valueExpression: String) -> String {
|
|
500
500
|
if let freeForm {
|
|
501
501
|
return freeForm.decodeExpression(of: valueExpression, as: expressionType(type))
|
|
502
502
|
}
|
|
503
|
+
if let tuple = tupleConversion(of: type) {
|
|
504
|
+
return tuple.decodeExpression(of: valueExpression)
|
|
505
|
+
}
|
|
503
506
|
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
504
507
|
}
|
|
505
508
|
|
|
506
509
|
/// The encode expression for the native value `valueExpression` of a boundary type, as it appears
|
|
507
510
|
/// after `try`. The counterpart of `decodeCall`: a type that mentions `Any` encodes through the
|
|
508
|
-
/// `JavaScriptValue` free-form methods,
|
|
511
|
+
/// `JavaScriptValue` free-form methods, a tuple through the `JavaScriptValue` tuple methods, and every
|
|
512
|
+
/// other type through its own static `encode`.
|
|
509
513
|
internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String) -> String {
|
|
510
514
|
if let freeForm {
|
|
511
515
|
return freeForm.encodeExpression(of: valueExpression)
|
|
512
516
|
}
|
|
517
|
+
if let tuple = tupleConversion(of: type) {
|
|
518
|
+
return tuple.encodeExpression(of: valueExpression)
|
|
519
|
+
}
|
|
513
520
|
return "\(expressionType(type)).encode(\(valueExpression), in: runtime)"
|
|
514
521
|
}
|
|
515
522
|
|
|
@@ -423,10 +423,17 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
423
423
|
let entry = "entry\(index)"
|
|
424
424
|
let exprType = expressionType(property.type)
|
|
425
425
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
426
|
-
let cast
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
let cast: String
|
|
427
|
+
if isBareAny {
|
|
428
|
+
cast = entry
|
|
429
|
+
} else if tupleConversion(of: property.type) != nil {
|
|
430
|
+
// A type that holds a tuple has no dynamic type, so the dictionary value must already be of the
|
|
431
|
+
// property's type.
|
|
432
|
+
cast =
|
|
433
|
+
"try { () throws -> \(exprType) in guard let value = \(entry) as? \(exprType) else { throw Conversions.ConvertingException<\(exprType)>(\(entry)) }; return value }()"
|
|
434
|
+
} else {
|
|
435
|
+
cast = "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
436
|
+
}
|
|
430
437
|
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
431
438
|
if property.isRequired {
|
|
432
439
|
lines.append(" guard let \(entry) else {")
|
|
@@ -544,7 +551,11 @@ private func objectWriteLines(properties: [RecordProperty], of record: String, c
|
|
|
544
551
|
encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
|
|
545
552
|
case nil:
|
|
546
553
|
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
547
|
-
|
|
554
|
+
// A type that holds a tuple has no `encode(_:into:in:)`, so its encoded value is written instead.
|
|
555
|
+
if let tuple = tupleConversion(of: property.type) {
|
|
556
|
+
return " object.setProperty(\(propName), value: try \(tuple.encodeExpression(of: value)))"
|
|
557
|
+
}
|
|
558
|
+
return " try object.setProperty(\(propName), encoding: \(value))"
|
|
548
559
|
}
|
|
549
560
|
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
550
561
|
return " object.setProperty(\(propName), value: \(encode))"
|
|
@@ -50,9 +50,15 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
50
50
|
var staticFunctions: [JSFunction] = []
|
|
51
51
|
var staticProperties: [JSProperty] = []
|
|
52
52
|
var constructor: JSConstructor?
|
|
53
|
+
var assertions: [MemberConformanceAssertion] = []
|
|
54
|
+
let aliases = TupleTypeAliases(in: classDecl.memberBlock)
|
|
53
55
|
|
|
54
56
|
for member in classDecl.memberBlock.members {
|
|
55
|
-
|
|
57
|
+
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
58
|
+
let decl = aliases.resolving(member.decl)
|
|
59
|
+
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
60
|
+
assertions.append(assertion)
|
|
61
|
+
}
|
|
56
62
|
|
|
57
63
|
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
58
64
|
initDecl.attributes.firstAttribute(named: "JS") != nil
|
|
@@ -129,6 +135,12 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
129
135
|
emitted.append(constructor.buildConstructor(typeName: typeName))
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
// The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
|
|
139
|
+
// `jsConformanceAssertionBody`).
|
|
140
|
+
if let assertion = classConformanceAssertions(assertions) {
|
|
141
|
+
emitted.append(assertion)
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
return emitted
|
|
133
145
|
}
|
|
134
146
|
}
|