expo-modules-macros 0.17.0-0 → 0.18.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 +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +8 -15
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +9 -9
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +3 -2
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +10 -7
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +49 -9
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +63 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +20 -13
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +0 -12
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +22 -13
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +1 -1
- package/package.json +3 -3
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -68,29 +68,20 @@ internal struct JSClosureType {
|
|
|
68
68
|
let isVariadic: Bool
|
|
69
69
|
|
|
70
70
|
init(_ element: TupleTypeElementSyntax) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
attributed.attributes.isEmpty
|
|
75
|
-
? attributed.baseType : TypeSyntax(attributed.with(\.specifiers, []))
|
|
76
|
-
} else {
|
|
77
|
-
self.specifiers = []
|
|
78
|
-
self.type = element.type
|
|
79
|
-
}
|
|
71
|
+
let specified = SpecifiedType(element.type)
|
|
72
|
+
self.specifiers = specified.specifiers
|
|
73
|
+
self.type = specified.type
|
|
80
74
|
self.isVariadic = element.ellipsis != nil
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
/// The first specifier the wrapper can't support, such as `inout` or `isolated`.
|
|
77
|
+
/// The first specifier the wrapper can't support, such as `inout` or `isolated`. The wrapper passes
|
|
78
|
+
/// each parameter on to the callback, so an ownership convention doesn't change what it does.
|
|
84
79
|
var unsupportedSpecifier: String? {
|
|
85
|
-
return specifiers.first { !
|
|
80
|
+
return specifiers.first { !ownershipSpecifiers.contains($0) }
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
/// The ownership specifiers a closure parameter may carry. The wrapper passes each parameter on to the
|
|
91
|
-
/// callback, so the ownership convention doesn't change what it does.
|
|
92
|
-
private let supportedParameterSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
93
|
-
|
|
94
85
|
/// The statements that bind `name` to the native value of a `@JS` argument read from
|
|
95
86
|
/// `valueExpression` (a `JavaScriptUnownedValue`). A closure gets a wrapper around the JS function,
|
|
96
87
|
/// and every other type decodes through `decodeCall`.
|
|
@@ -99,6 +90,8 @@ internal func argumentDecodeStatements(
|
|
|
99
90
|
into name: String,
|
|
100
91
|
from valueExpression: String
|
|
101
92
|
) -> [String] {
|
|
93
|
+
// A specifier such as `borrowing` belongs to the parameter, not to the type that decodes.
|
|
94
|
+
let type = SpecifiedType(type).type
|
|
102
95
|
guard let closure = JSClosureType(type) else {
|
|
103
96
|
return ["let \(name) = try \(decodeCall(type.trimmedDescription, from: valueExpression))"]
|
|
104
97
|
}
|
|
@@ -24,7 +24,7 @@ internal struct JSFunction {
|
|
|
24
24
|
|
|
25
25
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
26
26
|
self.swiftName = funcDecl.name.text
|
|
27
|
-
self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
|
|
27
|
+
self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
|
|
28
28
|
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
29
29
|
|
|
30
30
|
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
@@ -154,7 +154,7 @@ internal struct JSFunction {
|
|
|
154
154
|
lines.append(contentsOf: encodeResultLines().map { " " + $0 })
|
|
155
155
|
} else {
|
|
156
156
|
lines.append(" \(callExpression(receiver: receiver, arity: arity))")
|
|
157
|
-
lines.append(" return .undefined")
|
|
157
|
+
lines.append(" return .undefined()")
|
|
158
158
|
}
|
|
159
159
|
lines.append("}")
|
|
160
160
|
return lines
|
|
@@ -204,7 +204,7 @@ internal struct JSFunction {
|
|
|
204
204
|
|
|
205
205
|
/// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
|
|
206
206
|
/// `let result = <callee>.f(...)` then the return encode (or the no-return `<callee>.f(...)` +
|
|
207
|
-
/// `.undefined`).
|
|
207
|
+
/// `.undefined()`).
|
|
208
208
|
private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
|
|
209
209
|
var lines: [String] = []
|
|
210
210
|
for index in decodingFrom..<arity {
|
|
@@ -215,7 +215,7 @@ internal struct JSFunction {
|
|
|
215
215
|
lines.append(contentsOf: encodeResultLines())
|
|
216
216
|
} else {
|
|
217
217
|
lines.append(callExpression(receiver: receiver, arity: arity))
|
|
218
|
-
lines.append("return .undefined")
|
|
218
|
+
lines.append("return .undefined()")
|
|
219
219
|
}
|
|
220
220
|
return lines
|
|
221
221
|
}
|
|
@@ -225,7 +225,7 @@ internal struct JSFunction {
|
|
|
225
225
|
/// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
226
226
|
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
227
227
|
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
228
|
-
/// no-return function returns `.undefined` instead.
|
|
228
|
+
/// no-return function returns `.undefined()` instead.
|
|
229
229
|
///
|
|
230
230
|
/// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
|
|
231
231
|
/// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
|
|
@@ -235,7 +235,7 @@ internal struct JSFunction {
|
|
|
235
235
|
/// which always run on the JS thread — keep encoding directly without the wrapper.
|
|
236
236
|
private func encodeResultLines() -> [String] {
|
|
237
237
|
guard let returnType else {
|
|
238
|
-
return ["return .undefined"]
|
|
238
|
+
return ["return .undefined()"]
|
|
239
239
|
}
|
|
240
240
|
let encode = "try \(expressionType(returnType)).encode(result, in: runtime)"
|
|
241
241
|
if isAsync {
|
|
@@ -309,7 +309,7 @@ internal struct JSProperty {
|
|
|
309
309
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
310
310
|
/// instance. Getter and setter are gated independently.
|
|
311
311
|
func decorateStatements(object: String, receiver: Receiver) -> String {
|
|
312
|
-
let descriptorName =
|
|
312
|
+
let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
|
|
313
313
|
let callee = receiver.callee
|
|
314
314
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
315
315
|
// reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
|
|
@@ -340,7 +340,7 @@ internal struct JSProperty {
|
|
|
340
340
|
let setDecode = "\(callee).\(swiftName) = try \(decodeCall(valueType, from: "arguments.unownedValue(at: 0)"))"
|
|
341
341
|
lines.append(
|
|
342
342
|
accessorClosure(
|
|
343
|
-
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
|
|
343
|
+
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined()"))
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
@@ -437,7 +437,7 @@ internal func buildDecorateSharedObjectPhase(
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
|
|
440
|
-
/// nothing JS-visible, so the binding returns `.undefined`.
|
|
440
|
+
/// nothing JS-visible, so the binding returns `.undefined()`.
|
|
441
441
|
private func isVoidType(_ type: TypeSyntax?) -> Bool {
|
|
442
442
|
guard let type else {
|
|
443
443
|
return true
|
|
@@ -91,9 +91,10 @@ extension EventMacro: PeerMacro {
|
|
|
91
91
|
? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
|
|
92
92
|
: "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
|
|
93
93
|
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
94
|
+
let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
|
|
94
95
|
return [
|
|
95
96
|
"""
|
|
96
|
-
private func
|
|
97
|
+
private func \(raw: peerName)() {
|
|
97
98
|
\(raw: helper)
|
|
98
99
|
\(raw: call)
|
|
99
100
|
}
|
|
@@ -172,7 +173,7 @@ private func validatedEvent(
|
|
|
172
173
|
let swiftName = identifier.identifier.text
|
|
173
174
|
return EventMember(
|
|
174
175
|
swiftName: swiftName,
|
|
175
|
-
jsName: jsNameArgument(of: node) ?? defaultEventName(for: swiftName),
|
|
176
|
+
jsName: jsNameArgument(of: node) ?? defaultEventName(for: withoutBackticks(swiftName)),
|
|
176
177
|
payloadType: functionType.parameters.first?.type.trimmedDescription,
|
|
177
178
|
isSync: boolArgument(of: node, label: "sync") == true
|
|
178
179
|
)
|
|
@@ -45,7 +45,7 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
45
45
|
// class name) and emitted as a non-optional `_jsName` static below — so the
|
|
46
46
|
// macro no longer emits a `Name(…)` DSL entry. Core reads the static for the module name,
|
|
47
47
|
// ahead of its type-name fallback (which then only applies to non-macro DSL modules).
|
|
48
|
-
let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
|
|
48
|
+
let moduleName = jsNameArgument(of: node) ?? withoutBackticks(classDecl.name.text)
|
|
49
49
|
var entries: [String] = []
|
|
50
50
|
|
|
51
51
|
// `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
|
|
@@ -30,13 +30,16 @@ private let freeFormDecodeMethods: [String: String] = [
|
|
|
30
30
|
"[String:Any]": "decodeAnyDictionary",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
-
/// The type-safe alternative a free-form type's diagnostic steers to: the
|
|
34
|
-
///
|
|
35
|
-
///
|
|
33
|
+
/// The type-safe alternative a free-form type's diagnostic steers to: the JS wrapper of the same shape
|
|
34
|
+
/// (`JavaScriptArray` for an array, `JavaScriptObject` for a dictionary, `JavaScriptValue` for an
|
|
35
|
+
/// untyped value). The wrappers conform to the codable protocols and keep the value unconverted; a
|
|
36
|
+
/// non-copyable `JavaScriptValue` cannot be an element of a Swift array or dictionary, so the container
|
|
37
|
+
/// shapes map to a wrapper rather than to a container of values. Keyed by the free-form type's
|
|
38
|
+
/// normalized spelling.
|
|
36
39
|
private let typedFreeFormReplacements: [String: String] = [
|
|
37
40
|
"Any": "JavaScriptValue",
|
|
38
|
-
"[Any]": "
|
|
39
|
-
"[String:Any]": "
|
|
41
|
+
"[Any]": "JavaScriptArray",
|
|
42
|
+
"[String:Any]": "JavaScriptObject",
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
/// True when a boundary type (as written) is one of the free-form spellings, ignoring internal
|
|
@@ -54,8 +57,8 @@ internal func freeFormDecodeMethod(for type: String) -> String? {
|
|
|
54
57
|
return freeFormDecodeMethods[normalizedTypeSpelling(type)]
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
/// The type-safe alternative to suggest in place of a free-form type (
|
|
58
|
-
///
|
|
60
|
+
/// The type-safe alternative to suggest in place of a free-form type (the JS wrapper of the same shape),
|
|
61
|
+
/// or `nil` when the type isn't free-form.
|
|
59
62
|
internal func typedFreeFormReplacement(for type: String) -> String? {
|
|
60
63
|
return typedFreeFormReplacements[normalizedTypeSpelling(type)]
|
|
61
64
|
}
|
|
@@ -51,7 +51,7 @@ internal struct JSConstructor {
|
|
|
51
51
|
func buildConstructor(typeName: String) -> DeclSyntax {
|
|
52
52
|
return """
|
|
53
53
|
@JavaScriptActor
|
|
54
|
-
public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
|
|
54
|
+
public override class func _constructSharedObject(this: borrowing JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
|
|
55
55
|
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
56
56
|
}
|
|
57
57
|
"""
|
|
@@ -31,6 +31,7 @@ public struct JSMacro: PeerMacro {
|
|
|
31
31
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
32
32
|
in context: some MacroExpansionContext
|
|
33
33
|
) throws -> [DeclSyntax] {
|
|
34
|
+
diagnoseParameterSpecifiers(in: declaration, in: context)
|
|
34
35
|
diagnoseFreeFormTypes(in: declaration, in: context)
|
|
35
36
|
diagnoseClosureTypes(in: declaration, in: context)
|
|
36
37
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
@@ -114,13 +115,46 @@ private func insertAsyncFixIt(for funcDecl: FunctionDeclSyntax) -> FixIt {
|
|
|
114
115
|
)
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/// Reports a function or constructor parameter whose specifier a decoded argument can't honor: an
|
|
119
|
+
/// `inout` parameter expects its writes to reach the caller, and an `isolated` one expects an actor,
|
|
120
|
+
/// while a `@JS` argument is a fresh value decoded from JavaScript. Ownership specifiers (`borrowing`,
|
|
121
|
+
/// `consuming`, `sending`) are fine: the binding passes the decoded value under whichever convention
|
|
122
|
+
/// the parameter asks for. The specifier is left off the decode and the assertion (see
|
|
123
|
+
/// `SpecifiedType`); a parameter reported here is also left out of the assertion.
|
|
124
|
+
private func diagnoseParameterSpecifiers(
|
|
125
|
+
in declaration: some DeclSyntaxProtocol,
|
|
126
|
+
in context: some MacroExpansionContext
|
|
127
|
+
) {
|
|
128
|
+
let parameters: FunctionParameterListSyntax
|
|
129
|
+
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
130
|
+
parameters = funcDecl.signature.parameterClause.parameters
|
|
131
|
+
} else if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
132
|
+
parameters = initDecl.signature.parameterClause.parameters
|
|
133
|
+
} else {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
for parameter in parameters {
|
|
137
|
+
guard let specifier = SpecifiedType(parameter.type).unsupportedSpecifier else {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
context.diagnose(
|
|
141
|
+
Diagnostic(
|
|
142
|
+
node: parameter.type,
|
|
143
|
+
message: JSDiagnosticMessage(
|
|
144
|
+
"A @JS parameter can't be '\(specifier)': its argument is a value decoded from JavaScript, which can't be written back or carry an isolation.",
|
|
145
|
+
id: "js-parameter-specifier",
|
|
146
|
+
severity: .error
|
|
147
|
+
)))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
117
151
|
/// Emits the free-form (`Any` / `[Any]` / `[String: Any]`) diagnostics for a `@JS` declaration.
|
|
118
152
|
///
|
|
119
153
|
/// A free-form type is only supported crossing the boundary as an **argument**, decoded through
|
|
120
154
|
/// `JavaScriptValue.decodeAny…`; there is no free-form encode, so any position that encodes is a hard
|
|
121
155
|
/// error. That means:
|
|
122
156
|
/// - a function/constructor **parameter** typed free-form gets a warning steering to
|
|
123
|
-
/// `
|
|
157
|
+
/// `JavaScriptValue` (the type-safe alternative), but compiles;
|
|
124
158
|
/// - a function **return** typed free-form is an error (it would need to encode);
|
|
125
159
|
/// - a **property** typed free-form is an error regardless of settability, because its getter always
|
|
126
160
|
/// encodes.
|
|
@@ -156,9 +190,8 @@ private func diagnoseFreeFormTypes(
|
|
|
156
190
|
}
|
|
157
191
|
}
|
|
158
192
|
|
|
159
|
-
/// The tail of a free-form encode error: the
|
|
160
|
-
///
|
|
161
|
-
/// for a bare `Any`. Both conform to the codable protocols, so either is a valid fix.
|
|
193
|
+
/// The tail of a free-form encode error: the typed wrapper of the same shape (`JavaScriptArray`,
|
|
194
|
+
/// `JavaScriptObject`) when there is one, otherwise the plain `JavaScriptValue` passthrough suggestion.
|
|
162
195
|
private func suggestedAlternative(for freeFormType: String) -> String {
|
|
163
196
|
if let reshaped = typedFreeFormReplacement(for: freeFormType), reshaped != "JavaScriptValue" {
|
|
164
197
|
return "Use '\(reshaped)', or 'JavaScriptValue' to pass a JS value through unchanged."
|
|
@@ -173,7 +206,8 @@ private func warnFreeFormArguments(
|
|
|
173
206
|
in context: some MacroExpansionContext
|
|
174
207
|
) {
|
|
175
208
|
for parameter in parameters {
|
|
176
|
-
|
|
209
|
+
// Matched on the bare type: `borrowing [String: Any]` decodes as free-form too.
|
|
210
|
+
let type = SpecifiedType(parameter.type).type.trimmedDescription
|
|
177
211
|
guard let suggested = typedFreeFormReplacement(for: type) else {
|
|
178
212
|
continue
|
|
179
213
|
}
|
|
@@ -189,7 +223,7 @@ private func freeFormArgumentWarning(
|
|
|
189
223
|
suggesting suggestedType: String
|
|
190
224
|
) -> JSDiagnosticMessage {
|
|
191
225
|
return JSDiagnosticMessage(
|
|
192
|
-
"Prefer '\(suggestedType)' over the free-form '\(freeFormType)' for a @JS argument. Free-form decoding boxes every value as 'Any' (slower, no static typing);
|
|
226
|
+
"Prefer '\(suggestedType)' over the free-form '\(freeFormType)' for a @JS argument. Free-form decoding boxes every value as 'Any' (slower, no static typing); '\(suggestedType)' keeps the JS value inspectable without converting it.",
|
|
193
227
|
id: "js-free-form-argument",
|
|
194
228
|
severity: .warning
|
|
195
229
|
)
|
|
@@ -266,9 +300,15 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
266
300
|
var decodableTypes: [String] = []
|
|
267
301
|
var callbackArgumentTypes: [String] = []
|
|
268
302
|
for parameter in funcDecl.signature.parameterClause.parameters {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
303
|
+
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
304
|
+
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
305
|
+
let specified = SpecifiedType(parameter.type)
|
|
306
|
+
guard specified.unsupportedSpecifier == nil else {
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
guard let closure = JSClosureType(specified.type) else {
|
|
310
|
+
if !containsFunctionType(specified.type) {
|
|
311
|
+
decodableTypes.append(specified.type.trimmedDescription)
|
|
272
312
|
}
|
|
273
313
|
continue
|
|
274
314
|
}
|
|
@@ -72,6 +72,9 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
|
|
|
72
72
|
/// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
|
|
73
73
|
/// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
|
|
74
74
|
internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
75
|
+
if let attributed = type.as(AttributedTypeSyntax.self) {
|
|
76
|
+
return isOptionalType(attributed.baseType)
|
|
77
|
+
}
|
|
75
78
|
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
76
79
|
return true
|
|
77
80
|
}
|
|
@@ -81,6 +84,39 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
|
81
84
|
return false
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/// A parameter's type split from the specifiers written in front of it (`borrowing`, `consuming`,
|
|
88
|
+
/// `sending`, `inout`, `isolated`, ...). The specifiers aren't part of the type: the decode call and
|
|
89
|
+
/// the conformance assertion need the bare type (`borrowing Point.decode(...)` isn't valid Swift), and
|
|
90
|
+
/// a decoded argument is a fresh owned value, so it can be passed under any ownership convention.
|
|
91
|
+
internal struct SpecifiedType {
|
|
92
|
+
/// The type without its specifiers.
|
|
93
|
+
let type: TypeSyntax
|
|
94
|
+
/// The specifiers written before the type, in source order.
|
|
95
|
+
let specifiers: [String]
|
|
96
|
+
|
|
97
|
+
init(_ type: TypeSyntax) {
|
|
98
|
+
if let attributed = type.as(AttributedTypeSyntax.self), !attributed.specifiers.isEmpty {
|
|
99
|
+
self.specifiers = attributed.specifiers.map(\.trimmedDescription)
|
|
100
|
+
self.type =
|
|
101
|
+
attributed.attributes.isEmpty
|
|
102
|
+
? attributed.baseType : TypeSyntax(attributed.with(\.specifiers, []))
|
|
103
|
+
} else {
|
|
104
|
+
self.specifiers = []
|
|
105
|
+
self.type = type
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The first specifier a decoded argument can't honor: `inout` (JavaScript can't observe a write
|
|
110
|
+
/// back) or `isolated` (the argument doesn't carry an isolation).
|
|
111
|
+
var unsupportedSpecifier: String? {
|
|
112
|
+
return specifiers.first { !ownershipSpecifiers.contains($0) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// The ownership specifiers a parameter may carry. They only change how the argument is passed, which
|
|
117
|
+
/// a fresh decoded value satisfies either way.
|
|
118
|
+
internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
|
|
119
|
+
|
|
84
120
|
/// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
|
|
85
121
|
/// optional.
|
|
86
122
|
internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
@@ -341,6 +377,32 @@ internal func decodeCall(_ type: String, from valueExpression: String) -> String
|
|
|
341
377
|
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
342
378
|
}
|
|
343
379
|
|
|
380
|
+
// MARK: - Escaped names
|
|
381
|
+
|
|
382
|
+
/// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
|
|
383
|
+
/// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
|
|
384
|
+
/// name, a record key) is the bare name.
|
|
385
|
+
internal func withoutBackticks(_ name: String) -> String {
|
|
386
|
+
var name = Substring(name)
|
|
387
|
+
while name.first == "`" {
|
|
388
|
+
name.removeFirst()
|
|
389
|
+
}
|
|
390
|
+
while name.last == "`" {
|
|
391
|
+
name.removeLast()
|
|
392
|
+
}
|
|
393
|
+
return String(name)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// A generated identifier built from a member's name, such as `countDescriptor` from `count`. The
|
|
397
|
+
/// affixes join the bare name, and an escaped name keeps the backticks around the whole result
|
|
398
|
+
/// (`` `class` `` gives `` `classDescriptor` ``), since the bare name may not be an identifier on
|
|
399
|
+
/// its own.
|
|
400
|
+
internal func derivedIdentifier(from name: String, prefix: String = "", suffix: String = "") -> String {
|
|
401
|
+
let bareName = withoutBackticks(name)
|
|
402
|
+
let identifier = prefix + bareName + suffix
|
|
403
|
+
return bareName == name ? identifier : "`\(identifier)`"
|
|
404
|
+
}
|
|
405
|
+
|
|
344
406
|
// MARK: - @JS property collection
|
|
345
407
|
|
|
346
408
|
/// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
|
|
@@ -365,7 +427,7 @@ internal func collectProperties(
|
|
|
365
427
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
366
428
|
return JSProperty(
|
|
367
429
|
swiftName: swiftName,
|
|
368
|
-
jsName: jsNameOverride ?? swiftName,
|
|
430
|
+
jsName: jsNameOverride ?? withoutBackticks(swiftName),
|
|
369
431
|
valueType: valueType,
|
|
370
432
|
isSettable: isVar && bindingIsSettable(binding)
|
|
371
433
|
)
|
|
@@ -95,7 +95,7 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
95
95
|
members.append(defaultInit)
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
if !existingInitLabels.contains(properties.map { $0.
|
|
98
|
+
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
99
99
|
members.append(memberwiseInit(properties: properties))
|
|
100
100
|
}
|
|
101
101
|
members.append(fromJSObjectFactory(properties: properties))
|
|
@@ -195,10 +195,11 @@ private enum FreeFormConversion {
|
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
197
|
A single record property discovered on the type, paired with everything the synthesized
|
|
198
|
-
conversions need: its property name
|
|
199
|
-
source may omit it.
|
|
198
|
+
conversions need: its property name, its written type, and how the source may omit it.
|
|
200
199
|
*/
|
|
201
200
|
private struct RecordProperty {
|
|
201
|
+
/// The property name as written, backticks included for an escaped name (`` `default` ``), since
|
|
202
|
+
/// every identifier position in the generated code needs them.
|
|
202
203
|
let name: String
|
|
203
204
|
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
|
|
204
205
|
/// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
|
|
@@ -222,6 +223,12 @@ private struct RecordProperty {
|
|
|
222
223
|
var isRequired: Bool {
|
|
223
224
|
return !hasDefault && !isOptional
|
|
224
225
|
}
|
|
226
|
+
|
|
227
|
+
/// The key the property reads from and writes to, in both the JS object and the dictionary: the
|
|
228
|
+
/// name without escaping backticks.
|
|
229
|
+
var key: String {
|
|
230
|
+
return withoutBackticks(name)
|
|
231
|
+
}
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
/**
|
|
@@ -414,7 +421,7 @@ private func factoryBody(properties: [RecordProperty], readLines: [String]) -> S
|
|
|
414
421
|
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
415
422
|
var lines: [String] = []
|
|
416
423
|
for property in properties {
|
|
417
|
-
let valueVar =
|
|
424
|
+
let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
|
|
418
425
|
let exprType = expressionType(property.type)
|
|
419
426
|
let decode: String
|
|
420
427
|
switch property.freeForm {
|
|
@@ -426,10 +433,10 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
426
433
|
case nil:
|
|
427
434
|
decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
428
435
|
}
|
|
429
|
-
lines.append(" let \(valueVar) = object.getProperty(\"\(property.
|
|
436
|
+
lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
|
|
430
437
|
if property.isRequired {
|
|
431
438
|
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
432
|
-
lines.append(" throw RecordPropertyRequiredException(\"\(property.
|
|
439
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
433
440
|
lines.append(" }")
|
|
434
441
|
lines.append(" let \(property.name) = \(decode)")
|
|
435
442
|
} else if property.isOptional {
|
|
@@ -449,16 +456,16 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
449
456
|
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
450
457
|
var lines: [String] = []
|
|
451
458
|
for property in properties {
|
|
452
|
-
let valueVar =
|
|
459
|
+
let valueVar = derivedIdentifier(from: property.name, suffix: "Value")
|
|
453
460
|
let exprType = expressionType(property.type)
|
|
454
461
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
455
462
|
let cast = isBareAny
|
|
456
463
|
? valueVar
|
|
457
464
|
: "try \(exprType).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(exprType)"
|
|
458
|
-
lines.append(" let \(valueVar) = dictionary[\"\(property.
|
|
465
|
+
lines.append(" let \(valueVar) = dictionary[\"\(property.key)\"]")
|
|
459
466
|
if property.isRequired {
|
|
460
467
|
lines.append(" guard let \(valueVar) else {")
|
|
461
|
-
lines.append(" throw RecordPropertyRequiredException(\"\(property.
|
|
468
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
462
469
|
lines.append(" }")
|
|
463
470
|
lines.append(" let \(property.name) = \(cast)")
|
|
464
471
|
} else if property.isOptional {
|
|
@@ -483,7 +490,7 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
|
|
|
483
490
|
lines.append(" var dictionary: [String: Any] = [:]")
|
|
484
491
|
}
|
|
485
492
|
for property in properties {
|
|
486
|
-
lines.append(" dictionary[\"\(property.
|
|
493
|
+
lines.append(" dictionary[\"\(property.key)\"] = self.\(property.name)")
|
|
487
494
|
}
|
|
488
495
|
lines.append(" return dictionary")
|
|
489
496
|
let body = lines.joined(separator: "\n")
|
|
@@ -529,7 +536,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
|
|
|
529
536
|
switch property.freeForm {
|
|
530
537
|
case .shape(_, let encodeMethod, let isOptional, _):
|
|
531
538
|
encode = isOptional
|
|
532
|
-
? "self.\(property.name) == nil ? .null : try JavaScriptValue.\(encodeMethod)(self.\(property.name)!, in: runtime)"
|
|
539
|
+
? "self.\(property.name) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(self.\(property.name)!, in: runtime)"
|
|
533
540
|
: "try JavaScriptValue.\(encodeMethod)(self.\(property.name), in: runtime)"
|
|
534
541
|
case .generic:
|
|
535
542
|
// The explicit `as Any` keeps an optional property from warning about its implicit coercion.
|
|
@@ -537,7 +544,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
|
|
|
537
544
|
case nil:
|
|
538
545
|
encode = "try \(exprType).encode(self.\(property.name), in: runtime)"
|
|
539
546
|
}
|
|
540
|
-
lines.append(" object.setProperty(\"\(property.
|
|
547
|
+
lines.append(" object.setProperty(\"\(property.key)\", value: \(encode))")
|
|
541
548
|
}
|
|
542
549
|
lines.append(" return object")
|
|
543
550
|
let body = lines.joined(separator: "\n")
|
|
@@ -567,7 +574,7 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
|
|
|
567
574
|
}
|
|
568
575
|
let labels = initDecl.signature.parameterClause.parameters.map { parameter in
|
|
569
576
|
// `firstName` is the external label (or `_`); fall back to the internal name when omitted.
|
|
570
|
-
let label = parameter.firstName.text
|
|
577
|
+
let label = withoutBackticks(parameter.firstName.text)
|
|
571
578
|
return label == "_" ? "" : label
|
|
572
579
|
}
|
|
573
580
|
signatures.append(labels)
|
|
@@ -39,7 +39,7 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
let typeName = classDecl.name.text
|
|
42
|
-
let jsName = jsNameArgument(of: node) ?? typeName
|
|
42
|
+
let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
|
|
43
43
|
|
|
44
44
|
// `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
|
|
45
45
|
// the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
|
|
@@ -111,8 +111,9 @@ internal func directionalConformanceAssertion(
|
|
|
111
111
|
let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
|
|
112
112
|
let call = "\(name)(\(arguments.joined(separator: ", ")))"
|
|
113
113
|
let staticKeyword = isStatic ? "static " : ""
|
|
114
|
+
let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
|
|
114
115
|
return """
|
|
115
|
-
private \(raw: staticKeyword)func
|
|
116
|
+
private \(raw: staticKeyword)func \(raw: peerName)() {
|
|
116
117
|
\(raw: helper)
|
|
117
118
|
\(raw: call)
|
|
118
119
|
}
|
|
@@ -251,18 +251,6 @@ private struct ViewProp {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/// `name` without the backticks that escape it: `` `default` `` becomes `default`.
|
|
255
|
-
private func withoutBackticks(_ name: String) -> String {
|
|
256
|
-
var name = Substring(name)
|
|
257
|
-
while name.first == "`" {
|
|
258
|
-
name.removeFirst()
|
|
259
|
-
}
|
|
260
|
-
while name.last == "`" {
|
|
261
|
-
name.removeLast()
|
|
262
|
-
}
|
|
263
|
-
return String(name)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
254
|
private struct ViewPropsModel {
|
|
267
255
|
/// Value props, in declaration order. The order is the bit order, so it is part of the ABI
|
|
268
256
|
/// between a compiled view and the runtime that fills the mask.
|
|
@@ -20,7 +20,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
20
20
|
|
|
21
21
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
22
22
|
if isTopLevel(node) {
|
|
23
|
-
classify(name: node.name.
|
|
23
|
+
classify(name: node.name.identifierName, attributes: node.attributes, members: node.memberBlock.members)
|
|
24
24
|
}
|
|
25
25
|
// The member walk reads the body itself; nested types aren't part of this surface (matching
|
|
26
26
|
// `DetectionVisitor`'s top-level-only scope), so there's no reason to descend.
|
|
@@ -29,7 +29,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
29
29
|
|
|
30
30
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
31
31
|
if isTopLevel(node) {
|
|
32
|
-
classify(name: node.name.
|
|
32
|
+
classify(name: node.name.identifierName, attributes: node.attributes, members: node.memberBlock.members)
|
|
33
33
|
}
|
|
34
34
|
return .skipChildren
|
|
35
35
|
}
|
|
@@ -48,7 +48,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
48
48
|
if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
|
|
49
49
|
unions.append(
|
|
50
50
|
ExportedUnion(
|
|
51
|
-
name: node.name.
|
|
51
|
+
name: node.name.identifierName,
|
|
52
52
|
members: collectUnionMembers(node.memberBlock.members),
|
|
53
53
|
file: file
|
|
54
54
|
))
|
|
@@ -56,7 +56,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
56
56
|
let rawType = rawValueType(of: node.inheritanceClause)
|
|
57
57
|
enums.append(
|
|
58
58
|
ExportedEnum(
|
|
59
|
-
name: node.name.
|
|
59
|
+
name: node.name.identifierName,
|
|
60
60
|
rawType: rawType,
|
|
61
61
|
cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
|
|
62
62
|
file: file
|
|
@@ -178,7 +178,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
178
178
|
functionType.parameters.count <= 1 else {
|
|
179
179
|
continue
|
|
180
180
|
}
|
|
181
|
-
let name = ident.identifier.
|
|
181
|
+
let name = ident.identifier.identifierName
|
|
182
182
|
result.append(
|
|
183
183
|
ExportedEvent(
|
|
184
184
|
name: name,
|
|
@@ -196,8 +196,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
196
196
|
let effects = funcDecl.signature.effectSpecifiers
|
|
197
197
|
let returnType = funcDecl.signature.returnClause?.type
|
|
198
198
|
return ExportedFunction(
|
|
199
|
-
name: funcDecl.name.
|
|
200
|
-
jsName: stringArgument(of: attribute) ?? funcDecl.name.
|
|
199
|
+
name: funcDecl.name.identifierName,
|
|
200
|
+
jsName: stringArgument(of: attribute) ?? funcDecl.name.identifierName,
|
|
201
201
|
parameters: parameters(of: funcDecl.signature.parameterClause),
|
|
202
202
|
returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
|
|
203
203
|
isAsync: effects?.asyncSpecifier != nil,
|
|
@@ -219,7 +219,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
219
219
|
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
220
220
|
continue
|
|
221
221
|
}
|
|
222
|
-
let name = ident.identifier.
|
|
222
|
+
let name = ident.identifier.identifierName
|
|
223
223
|
let type = valueTypeNode(annotation: binding.typeAnnotation?.type, initializer: binding.initializer?.value)
|
|
224
224
|
result.append(
|
|
225
225
|
ExportedProperty(
|
|
@@ -284,7 +284,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
284
284
|
}
|
|
285
285
|
properties.append(
|
|
286
286
|
ExportedRecordProperty(
|
|
287
|
-
name: ident.identifier.
|
|
287
|
+
name: ident.identifier.identifierName,
|
|
288
288
|
type: type,
|
|
289
289
|
isOptional: annotation.map { isOptionalType($0) } ?? false,
|
|
290
290
|
hasDefault: binding.initializer != nil
|
|
@@ -312,7 +312,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
312
312
|
continue
|
|
313
313
|
}
|
|
314
314
|
for element in caseDecl.elements where element.parameterClause == nil {
|
|
315
|
-
let name = element.name.
|
|
315
|
+
let name = element.name.identifierName
|
|
316
316
|
cases.append(
|
|
317
317
|
ExportedEnumCase(
|
|
318
318
|
name: name,
|
|
@@ -343,7 +343,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
343
343
|
continue
|
|
344
344
|
}
|
|
345
345
|
result.append(
|
|
346
|
-
ExportedUnionMember(name: element.name.
|
|
346
|
+
ExportedUnionMember(name: element.name.identifierName, type: typeNode(from: parameter.type)))
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
return result
|
|
@@ -353,8 +353,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
353
353
|
/// first), and `optional` when it has a default value or an optional type.
|
|
354
354
|
private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
|
|
355
355
|
clause.parameters.map { parameter in
|
|
356
|
-
let label = parameter.firstName.
|
|
357
|
-
let name = parameter.secondName?.
|
|
356
|
+
let label = parameter.firstName.identifierName
|
|
357
|
+
let name = parameter.secondName?.identifierName ?? label
|
|
358
358
|
return ExportedParameter(
|
|
359
359
|
label: label,
|
|
360
360
|
name: name,
|
|
@@ -376,6 +376,15 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
376
376
|
|
|
377
377
|
// MARK: - Syntactic helpers (shared spelling with the macros)
|
|
378
378
|
|
|
379
|
+
extension TokenSyntax {
|
|
380
|
+
/// The name a token spells, without the backticks Swift requires around a reserved word used as
|
|
381
|
+
/// a name: `` `default` `` reads as `default`. Tokens that aren't identifiers, such as the `_`
|
|
382
|
+
/// wildcard label, read as written.
|
|
383
|
+
var identifierName: String {
|
|
384
|
+
identifier?.name ?? text
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
379
388
|
/// True when the modifiers make a member type-level (`static` or `class`).
|
|
380
389
|
// MARK: - `@Event` helpers
|
|
381
390
|
|
|
@@ -204,7 +204,7 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
|
|
|
204
204
|
}
|
|
205
205
|
// A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
|
|
206
206
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
207
|
-
return nominalNode(name: identifier.name.
|
|
207
|
+
return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
|
|
208
208
|
}
|
|
209
209
|
// A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling.
|
|
210
210
|
if let member = type.as(MemberTypeSyntax.self) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"typescript": "^5.4.0"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@expo/modules-macros-win32-arm64": "0.
|
|
27
|
-
"@expo/modules-macros-win32-x64": "0.
|
|
26
|
+
"@expo/modules-macros-win32-arm64": "0.18.0",
|
|
27
|
+
"@expo/modules-macros-win32-x64": "0.18.0"
|
|
28
28
|
}
|
|
29
29
|
}
|