expo-modules-macros 0.17.0 → 0.19.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 +12 -18
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +17 -9
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +3 -2
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +101 -52
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +80 -83
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +82 -8
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +19 -62
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +1 -1
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +7 -5
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +0 -12
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +19 -11
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +101 -67
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +117 -36
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +4 -2
- package/build/types.d.ts +18 -3
- package/build/types.js +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,8 +90,11 @@ 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
|
+
let decode = decodeCall(type.trimmedDescription, freeForm: freeFormConversion(of: type), from: valueExpression)
|
|
97
|
+
return ["let \(name) = try \(decode)"]
|
|
104
98
|
}
|
|
105
99
|
return closureArgumentStatements(closure, into: name, from: valueExpression)
|
|
106
100
|
}
|
|
@@ -286,7 +280,7 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
286
280
|
}
|
|
287
281
|
|
|
288
282
|
// The closure's parameters are encoded and its result is decoded, so neither can be another
|
|
289
|
-
// closure or a
|
|
283
|
+
// closure or a type that mentions `Any`, in any shape.
|
|
290
284
|
let returnType = functionType.returnClause.type
|
|
291
285
|
for type in closure.parameters.map(\.type) + [returnType] {
|
|
292
286
|
if let nested = firstFunctionType(in: type) {
|
|
@@ -297,7 +291,7 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
297
291
|
"A @JS closure argument can't take or return another closure.",
|
|
298
292
|
id: "js-closure-nested"
|
|
299
293
|
)))
|
|
300
|
-
} else if
|
|
294
|
+
} else if mentionsFreeFormAny(type) {
|
|
301
295
|
context.diagnose(
|
|
302
296
|
Diagnostic(
|
|
303
297
|
node: type,
|
|
@@ -19,16 +19,19 @@ internal struct JSFunction {
|
|
|
19
19
|
let parameters: [FunctionParameterSyntax]
|
|
20
20
|
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
21
21
|
let returnType: String?
|
|
22
|
+
/// How the return value encodes when its type mentions `Any`, or `nil` for any other type.
|
|
23
|
+
let returnFreeForm: FreeFormConversion?
|
|
22
24
|
let isThrowing: Bool
|
|
23
25
|
let isAsync: Bool
|
|
24
26
|
|
|
25
27
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
26
28
|
self.swiftName = funcDecl.name.text
|
|
27
|
-
self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
|
|
29
|
+
self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
|
|
28
30
|
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
29
31
|
|
|
30
32
|
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
31
33
|
self.returnType = isVoidType(declaredReturnType) ? nil : declaredReturnType?.trimmedDescription
|
|
34
|
+
self.returnFreeForm = declaredReturnType.flatMap(freeFormConversion(of:))
|
|
32
35
|
|
|
33
36
|
let effectSpecifiers = funcDecl.signature.effectSpecifiers
|
|
34
37
|
self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
@@ -220,9 +223,10 @@ internal struct JSFunction {
|
|
|
220
223
|
return lines
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
/// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode
|
|
224
|
-
///
|
|
225
|
-
///
|
|
226
|
+
/// Encode the `result` local back to JS and return it through `JavaScriptEncodable.encode` (or, for a
|
|
227
|
+
/// type that mentions `Any`, the matching `JavaScriptValue` free-form method). Unlike the decode side
|
|
228
|
+
/// there's no primitive fast path: `encode` produces the same value as the primitive's
|
|
229
|
+
/// `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
|
|
226
230
|
/// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
|
|
227
231
|
/// catchable error is the right behavior, and matches how non-primitive integers already encode. A
|
|
228
232
|
/// no-return function returns `.undefined()` instead.
|
|
@@ -237,7 +241,7 @@ internal struct JSFunction {
|
|
|
237
241
|
guard let returnType else {
|
|
238
242
|
return ["return .undefined()"]
|
|
239
243
|
}
|
|
240
|
-
let encode = "try \(
|
|
244
|
+
let encode = "try \(encodeCall(returnType, freeForm: returnFreeForm, of: "result"))"
|
|
241
245
|
if isAsync {
|
|
242
246
|
return [
|
|
243
247
|
"return try await runtime.execute {",
|
|
@@ -290,7 +294,8 @@ internal struct JSFunction {
|
|
|
290
294
|
/// The receiver (see `Receiver`) is the module's `self` for a module binding, or the per-call `_self`
|
|
291
295
|
/// unwrapped from the JS `this` for a shared object. The getter reads `<callee>.<name>` and the setter
|
|
292
296
|
/// writes `<callee>.<name> = …`. Decode/encode of the value go through `JavaScriptDecodable.decode` /
|
|
293
|
-
/// `JavaScriptEncodable.encode
|
|
297
|
+
/// `JavaScriptEncodable.encode` (or the free-form methods for a type that mentions `Any`), the same
|
|
298
|
+
/// path as functions.
|
|
294
299
|
internal struct JSProperty {
|
|
295
300
|
let swiftName: String
|
|
296
301
|
let jsName: String
|
|
@@ -298,6 +303,8 @@ internal struct JSProperty {
|
|
|
298
303
|
/// no literal default). When `nil` the getter still works (the encode infers from `self.<name>`)
|
|
299
304
|
/// but the setter uses an untyped closure parameter.
|
|
300
305
|
let valueType: String?
|
|
306
|
+
/// How the value converts when its type mentions `Any`, or `nil` for any other type.
|
|
307
|
+
let freeForm: FreeFormConversion?
|
|
301
308
|
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
302
309
|
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
303
310
|
let isSettable: Bool
|
|
@@ -309,7 +316,7 @@ internal struct JSProperty {
|
|
|
309
316
|
/// function bindings: a module captures `self` strong, a shared object captures nothing of the
|
|
310
317
|
/// instance. Getter and setter are gated independently.
|
|
311
318
|
func decorateStatements(object: String, receiver: Receiver) -> String {
|
|
312
|
-
let descriptorName =
|
|
319
|
+
let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
|
|
313
320
|
let callee = receiver.callee
|
|
314
321
|
// A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
|
|
315
322
|
// reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
|
|
@@ -325,7 +332,7 @@ internal struct JSProperty {
|
|
|
325
332
|
// no static type to call `encode` on, so the value's own `toJavaScriptValue(in:)` is the fallback.
|
|
326
333
|
let getEncode: String
|
|
327
334
|
if let valueType {
|
|
328
|
-
getEncode = "return try \(
|
|
335
|
+
getEncode = "return try \(encodeCall(valueType, freeForm: freeForm, of: "\(callee).\(swiftName)"))"
|
|
329
336
|
} else {
|
|
330
337
|
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
331
338
|
}
|
|
@@ -337,7 +344,8 @@ internal struct JSProperty {
|
|
|
337
344
|
// getter-only (a settable var with neither an annotation nor a literal default is rare and can't
|
|
338
345
|
// be decoded).
|
|
339
346
|
if isSettable, let valueType {
|
|
340
|
-
let
|
|
347
|
+
let decode = decodeCall(valueType, freeForm: freeForm, from: "arguments.unownedValue(at: 0)")
|
|
348
|
+
let setDecode = "\(callee).\(swiftName) = try \(decode)"
|
|
341
349
|
lines.append(
|
|
342
350
|
accessorClosure(
|
|
343
351
|
descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined()"))
|
|
@@ -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
|
|
@@ -1,74 +1,123 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
|
-
/// Recognition of the free-form (`Any`-bearing) boundary types and the
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
/// they can't cross the boundary through the usual `T.decode` / `T.encode` path.
|
|
7
|
-
///
|
|
8
|
-
/// `
|
|
9
|
-
///
|
|
3
|
+
/// Recognition of the free-form (`Any`-bearing) boundary types and the conversions the macros emit for
|
|
4
|
+
/// them. None of these types can conform to the JS codable protocols (`Any` can't conform to a
|
|
5
|
+
/// protocol, and the container conditional conformances require the element/value to conform), so
|
|
6
|
+
/// they can't cross the boundary through the usual `T.decode` / `T.encode` path. They convert through
|
|
7
|
+
/// the `JavaScriptValue` free-form methods instead: the three free-form shapes (`Any`, `[Any]` and
|
|
8
|
+
/// `[String: Any]`), optionally wrapped in one optional, use the dedicated `decodeAny…` and
|
|
9
|
+
/// `encodeAny…` methods, and every other type built from `Any`, arrays, `String`-keyed dictionaries and
|
|
10
|
+
/// optionals uses the generic `decodeAny(_:as:in:)` and `encodeAny(_:in:)`.
|
|
10
11
|
///
|
|
11
|
-
/// Shared across the macro pipeline: `
|
|
12
|
-
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
/// `[String: [Any]]`, …) and converts it on the JS-value paths without `decode`/`encode`: the three
|
|
16
|
-
/// free-form shapes, optionally wrapped in one optional, use the dedicated `JavaScriptValue.decodeAny…`
|
|
17
|
-
/// and `encodeAny…` methods, and every other type uses the generic `decodeAny(_:as:in:)` and
|
|
18
|
-
/// `encodeAny(_:in:)`.
|
|
12
|
+
/// Shared across the macro pipeline: `@Record` properties and `@JS` arguments, return values and
|
|
13
|
+
/// properties convert through `FreeFormConversion` (the `@JS` bindings by way of
|
|
14
|
+
/// `MacroHelpers.decodeCall` and `encodeCall`), `TypeConformanceAssertion` skips the free-form shapes,
|
|
15
|
+
/// and `JSMacro` uses them for its diagnostics.
|
|
19
16
|
|
|
20
17
|
/// The free-form boundary types: an untyped value (`Any`), an untyped array (`[Any]`), and an untyped
|
|
21
18
|
/// string-keyed dictionary (`[String: Any]`). Matched by whitespace-normalized spelling so
|
|
22
19
|
/// `[String: Any]` and `[String : Any]` both count.
|
|
23
20
|
private let freeFormBoundaryTypes: Set<String> = ["Any", "[Any]", "[String:Any]"]
|
|
24
21
|
|
|
25
|
-
///
|
|
26
|
-
///
|
|
27
|
-
|
|
28
|
-
"Any": "decodeAny",
|
|
29
|
-
"[Any]": "decodeAnyArray",
|
|
30
|
-
"[String:Any]": "decodeAnyDictionary",
|
|
31
|
-
]
|
|
32
|
-
|
|
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.
|
|
39
|
-
private let typedFreeFormReplacements: [String: String] = [
|
|
40
|
-
"Any": "JavaScriptValue",
|
|
41
|
-
"[Any]": "JavaScriptArray",
|
|
42
|
-
"[String:Any]": "JavaScriptObject",
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
/// True when a boundary type (as written) is one of the free-form spellings, ignoring internal
|
|
46
|
-
/// whitespace so `[String: Any]` and `[String :Any]` both match. Optionals are not free-form here: a
|
|
47
|
-
/// trailing `?` would route through `Optional.decode`, which free-form can't satisfy, so an optional
|
|
48
|
-
/// free-form type isn't recognized and stays a normal (failing) assertion.
|
|
22
|
+
/// True when a boundary type (as written) is exactly one of the free-form shapes, ignoring internal
|
|
23
|
+
/// whitespace so `[String: Any]` and `[String :Any]` both match. An optional or nested free-form type
|
|
24
|
+
/// doesn't match; `freeFormConversion(of:)` recognizes those.
|
|
49
25
|
internal func isFreeFormBoundaryType(_ type: String) -> Bool {
|
|
50
26
|
return freeFormBoundaryTypes.contains(normalizedTypeSpelling(type))
|
|
51
27
|
}
|
|
52
28
|
|
|
53
|
-
/// The `JavaScriptValue.decodeAny…` method name for a free-form boundary type, or `nil` when the type
|
|
54
|
-
/// isn't free-form. The binding emits `JavaScriptValue.<method>(arguments.unownedValue(at:), in:)` in
|
|
55
|
-
/// place of the type's own `.decode`.
|
|
56
|
-
internal func freeFormDecodeMethod(for type: String) -> String? {
|
|
57
|
-
return freeFormDecodeMethods[normalizedTypeSpelling(type)]
|
|
58
|
-
}
|
|
59
|
-
|
|
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.
|
|
62
|
-
internal func typedFreeFormReplacement(for type: String) -> String? {
|
|
63
|
-
return typedFreeFormReplacements[normalizedTypeSpelling(type)]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
29
|
/// A type spelling with all whitespace removed, so spelling variations of the same type
|
|
67
30
|
/// (`[String: Any]` vs `[String :Any]`) compare equal.
|
|
68
31
|
private func normalizedTypeSpelling(_ type: String) -> String {
|
|
69
32
|
return type.filter { !$0.isWhitespace }
|
|
70
33
|
}
|
|
71
34
|
|
|
35
|
+
/// How a type that mentions `Any` converts on the JS-value paths. Such a type can't conform to the JS
|
|
36
|
+
/// codable protocols, so it converts through the `JavaScriptValue` free-form methods.
|
|
37
|
+
internal enum FreeFormConversion {
|
|
38
|
+
/// `Any`, `[Any]` or `[String: Any]`, wrapped in at most one optional: the dedicated methods for
|
|
39
|
+
/// that shape, named by `decodeMethod` and `encodeMethod`. The optional is handled around them.
|
|
40
|
+
case shape(decodeMethod: String, encodeMethod: String, isOptional: Bool, isBareAny: Bool)
|
|
41
|
+
/// Any other type that mentions `Any`: the generic `decodeAny(_:as:in:)` and `encodeAny(_:in:)`.
|
|
42
|
+
case generic
|
|
43
|
+
|
|
44
|
+
/// Classifies a type that mentions `Any`; the caller checks `isConvertibleFreeFormType` first.
|
|
45
|
+
init(type: TypeSyntax) {
|
|
46
|
+
let wrapped = optionalWrappedType(type)
|
|
47
|
+
switch freeFormShape(of: wrapped ?? type) {
|
|
48
|
+
case .any:
|
|
49
|
+
self = .shape(
|
|
50
|
+
decodeMethod: "decodeAny",
|
|
51
|
+
encodeMethod: "encodeAny",
|
|
52
|
+
isOptional: wrapped != nil,
|
|
53
|
+
isBareAny: true
|
|
54
|
+
)
|
|
55
|
+
case .array:
|
|
56
|
+
self = .shape(
|
|
57
|
+
decodeMethod: "decodeAnyArray",
|
|
58
|
+
encodeMethod: "encodeAnyArray",
|
|
59
|
+
isOptional: wrapped != nil,
|
|
60
|
+
isBareAny: false
|
|
61
|
+
)
|
|
62
|
+
case .dictionary:
|
|
63
|
+
self = .shape(
|
|
64
|
+
decodeMethod: "decodeAnyDictionary",
|
|
65
|
+
encodeMethod: "encodeAnyDictionary",
|
|
66
|
+
isOptional: wrapped != nil,
|
|
67
|
+
isBareAny: false
|
|
68
|
+
)
|
|
69
|
+
case nil:
|
|
70
|
+
self = .generic
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// True for `Any` itself, optional or not.
|
|
75
|
+
var isBareAny: Bool {
|
|
76
|
+
guard case .shape(_, _, _, let isBareAny) = self else {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
return isBareAny
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The decode expression, as it appears after `try`, for a `@JS` value read from `valueExpression`
|
|
83
|
+
/// (a `JavaScriptUnownedValue`) into `type` (spelled in expression position). For an optional
|
|
84
|
+
/// free-form shape, `undefined` and `null` map to `nil` around the dedicated method, just like
|
|
85
|
+
/// `Optional.decode`. The generic method takes an owning value, so the borrowed one is copied first.
|
|
86
|
+
func decodeExpression(of valueExpression: String, as type: String) -> String {
|
|
87
|
+
switch self {
|
|
88
|
+
case .shape(let decodeMethod, _, let isOptional, _):
|
|
89
|
+
let call = "JavaScriptValue.\(decodeMethod)(\(valueExpression), in: runtime)"
|
|
90
|
+
return isOptional ? "\(valueExpression).isUndefined() || \(valueExpression).isNull() ? nil : \(call)" : call
|
|
91
|
+
case .generic:
|
|
92
|
+
return "JavaScriptValue.decodeAny(\(valueExpression).copied(in: runtime), as: \(type).self, in: runtime)"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The encode expression, as it appears after `try`, for the `@JS` value `valueExpression`. An
|
|
97
|
+
/// optional free-form shape encodes through the generic `encodeAny(_:in:)`, which encodes `nil` as
|
|
98
|
+
/// `null` and unwraps any other value, so the expression (which may call a computed getter) is
|
|
99
|
+
/// evaluated only once.
|
|
100
|
+
func encodeExpression(of valueExpression: String) -> String {
|
|
101
|
+
switch self {
|
|
102
|
+
case .shape(_, let encodeMethod, false, _):
|
|
103
|
+
return "JavaScriptValue.\(encodeMethod)(\(valueExpression), in: runtime)"
|
|
104
|
+
case .shape(_, _, true, _), .generic:
|
|
105
|
+
// The explicit `as Any` keeps an optional value from warning about its implicit coercion.
|
|
106
|
+
return "JavaScriptValue.encodeAny(\(valueExpression) as Any, in: runtime)"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// The conversion for a boundary type that mentions `Any`, or `nil` when the type doesn't mention
|
|
112
|
+
/// `Any`, or holds it in a shape the free-form methods can't convert (see `isConvertibleFreeFormType`),
|
|
113
|
+
/// which the caller diagnoses.
|
|
114
|
+
internal func freeFormConversion(of type: TypeSyntax) -> FreeFormConversion? {
|
|
115
|
+
guard mentionsFreeFormAny(type), isConvertibleFreeFormType(type) else {
|
|
116
|
+
return nil
|
|
117
|
+
}
|
|
118
|
+
return FreeFormConversion(type: type)
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
/// True when a type mentions `Any` anywhere in its spelling, at any nesting depth: `Any`, `[Any]?`,
|
|
73
122
|
/// `[String: Any]`, `Dictionary<String, [Any]>`, `[Int: Any]`, and so on. Such a type can't conform to
|
|
74
123
|
/// the JS codable protocols.
|
|
@@ -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,98 +115,79 @@ private func insertAsyncFixIt(for funcDecl: FunctionDeclSyntax) -> FixIt {
|
|
|
114
115
|
)
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
///
|
|
118
|
-
///
|
|
119
|
-
///
|
|
120
|
-
/// `
|
|
121
|
-
///
|
|
122
|
-
///
|
|
123
|
-
|
|
124
|
-
/// - a function **return** typed free-form is an error (it would need to encode);
|
|
125
|
-
/// - a **property** typed free-form is an error regardless of settability, because its getter always
|
|
126
|
-
/// encodes.
|
|
127
|
-
///
|
|
128
|
-
/// Types are matched by their written spelling on the type node, so the diagnostic points at the
|
|
129
|
-
/// offending type in the user's source.
|
|
130
|
-
private func diagnoseFreeFormTypes(
|
|
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(
|
|
131
125
|
in declaration: some DeclSyntaxProtocol,
|
|
132
126
|
in context: some MacroExpansionContext
|
|
133
127
|
) {
|
|
128
|
+
let parameters: FunctionParameterListSyntax
|
|
134
129
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Diagnostic(node: returnType, message: freeFormReturnError(for: returnType.trimmedDescription)))
|
|
140
|
-
}
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// A constructor decodes its arguments exactly like a function; it has no return value to encode, so
|
|
145
|
-
// only the argument warning applies.
|
|
146
|
-
if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
147
|
-
warnFreeFormArguments(initDecl.signature.parameterClause.parameters, in: context)
|
|
130
|
+
parameters = funcDecl.signature.parameterClause.parameters
|
|
131
|
+
} else if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
132
|
+
parameters = initDecl.signature.parameterClause.parameters
|
|
133
|
+
} else {
|
|
148
134
|
return
|
|
149
135
|
}
|
|
150
|
-
|
|
151
|
-
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
152
|
-
let type = varDecl.bindings.first?.typeAnnotation?.type,
|
|
153
|
-
isFreeFormBoundaryType(type.trimmedDescription) {
|
|
154
|
-
context.diagnose(
|
|
155
|
-
Diagnostic(node: type, message: freeFormPropertyError(for: type.trimmedDescription)))
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/// The tail of a free-form encode error: the typed wrapper of the same shape (`JavaScriptArray`,
|
|
160
|
-
/// `JavaScriptObject`) when there is one, otherwise the plain `JavaScriptValue` passthrough suggestion.
|
|
161
|
-
private func suggestedAlternative(for freeFormType: String) -> String {
|
|
162
|
-
if let reshaped = typedFreeFormReplacement(for: freeFormType), reshaped != "JavaScriptValue" {
|
|
163
|
-
return "Use '\(reshaped)', or 'JavaScriptValue' to pass a JS value through unchanged."
|
|
164
|
-
}
|
|
165
|
-
return "Use a concrete type, or 'JavaScriptValue' to pass a JS value through unchanged."
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/// Emits the steering warning for each free-form parameter in a list. Shared by the function and
|
|
169
|
-
/// constructor cases, which both decode their arguments through the same path.
|
|
170
|
-
private func warnFreeFormArguments(
|
|
171
|
-
_ parameters: FunctionParameterListSyntax,
|
|
172
|
-
in context: some MacroExpansionContext
|
|
173
|
-
) {
|
|
174
136
|
for parameter in parameters {
|
|
175
|
-
let
|
|
176
|
-
guard let suggested = typedFreeFormReplacement(for: type) else {
|
|
137
|
+
guard let specifier = SpecifiedType(parameter.type).unsupportedSpecifier else {
|
|
177
138
|
continue
|
|
178
139
|
}
|
|
179
140
|
context.diagnose(
|
|
180
141
|
Diagnostic(
|
|
181
142
|
node: parameter.type,
|
|
182
|
-
message:
|
|
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
|
+
)))
|
|
183
148
|
}
|
|
184
149
|
}
|
|
185
150
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
151
|
+
/// Emits the free-form (`Any`-bearing) diagnostics for a `@JS` declaration.
|
|
152
|
+
///
|
|
153
|
+
/// A type built from `Any`, arrays, `String`-keyed dictionaries and optionals converts through the
|
|
154
|
+
/// `JavaScriptValue` free-form methods in every position (see `FreeFormConversion`). That means:
|
|
155
|
+
/// - a free-form argument, return value or property needs no diagnostic: it decodes through
|
|
156
|
+
/// `decodeAny…` and encodes through `encodeAny…`;
|
|
157
|
+
/// - a type that holds `Any` in any other shape (`[Int: Any]`, `Set<[String: Any]>`) is an error in
|
|
158
|
+
/// every position, since the free-form methods never produce or accept it.
|
|
159
|
+
///
|
|
160
|
+
/// Types are matched on the type node, so the diagnostic points at the offending type in the user's
|
|
161
|
+
/// source. A closure type is left to `diagnoseClosureTypes`.
|
|
162
|
+
private func diagnoseFreeFormTypes(
|
|
163
|
+
in declaration: some DeclSyntaxProtocol,
|
|
164
|
+
in context: some MacroExpansionContext
|
|
165
|
+
) {
|
|
166
|
+
var types: [TypeSyntax] = []
|
|
167
|
+
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
168
|
+
types = funcDecl.signature.parameterClause.parameters.map { SpecifiedType($0.type).type }
|
|
169
|
+
if let returnType = funcDecl.signature.returnClause?.type {
|
|
170
|
+
types.append(returnType)
|
|
171
|
+
}
|
|
172
|
+
} else if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
173
|
+
// A constructor decodes its arguments exactly like a function and has no return value.
|
|
174
|
+
types = initDecl.signature.parameterClause.parameters.map { SpecifiedType($0.type).type }
|
|
175
|
+
} else if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
176
|
+
let type = varDecl.bindings.first?.typeAnnotation?.type {
|
|
177
|
+
types = [type]
|
|
178
|
+
}
|
|
196
179
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
180
|
+
for type in types
|
|
181
|
+
where mentionsFreeFormAny(type) && !isConvertibleFreeFormType(type) && !containsFunctionType(type) {
|
|
182
|
+
context.diagnose(
|
|
183
|
+
Diagnostic(node: type, message: unconvertibleFreeFormError(for: type.trimmedDescription)))
|
|
184
|
+
}
|
|
203
185
|
}
|
|
204
186
|
|
|
205
|
-
private func
|
|
187
|
+
private func unconvertibleFreeFormError(for type: String) -> JSDiagnosticMessage {
|
|
206
188
|
return JSDiagnosticMessage(
|
|
207
|
-
"A @JS
|
|
208
|
-
id: "js-free-form-
|
|
189
|
+
"A @JS member can't use the type '\(type)': a type holding 'Any' converts to and from JavaScript only when it's built from 'Any', arrays, String-keyed dictionaries and optionals. Use one of those shapes, such as '[String: Any]', or 'JavaScriptValue' to keep the JavaScript value unconverted.",
|
|
190
|
+
id: "js-free-form-unsupported",
|
|
209
191
|
severity: .error
|
|
210
192
|
)
|
|
211
193
|
}
|
|
@@ -254,9 +236,10 @@ private struct BoundaryMember {
|
|
|
254
236
|
/// function contributes its parameter types (decodable) and its return type when non-Void (encodable);
|
|
255
237
|
/// a property contributes its value type as encodable (the getter) and also as decodable when settable
|
|
256
238
|
/// (the setter). Composed types (`[Int]`, `String?`, …) are kept verbatim, their conditional
|
|
257
|
-
/// conformances transitively constraining the elements.
|
|
258
|
-
///
|
|
259
|
-
///
|
|
239
|
+
/// conformances transitively constraining the elements. A type that mentions `Any` is left out: it
|
|
240
|
+
/// converts through the free-form methods, or is diagnosed by `diagnoseFreeFormTypes`. Returns `nil`
|
|
241
|
+
/// for declaration kinds `@JS` doesn't read types from, a property whose type isn't spelled out (a
|
|
242
|
+
/// syntactic macro can't recover it), or one whose type mentions `Any`.
|
|
260
243
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
261
244
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
262
245
|
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
@@ -265,22 +248,32 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
265
248
|
var decodableTypes: [String] = []
|
|
266
249
|
var callbackArgumentTypes: [String] = []
|
|
267
250
|
for parameter in funcDecl.signature.parameterClause.parameters {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
251
|
+
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
252
|
+
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
253
|
+
let specified = SpecifiedType(parameter.type)
|
|
254
|
+
guard specified.unsupportedSpecifier == nil else {
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
guard let closure = JSClosureType(specified.type) else {
|
|
258
|
+
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
259
|
+
decodableTypes.append(specified.type.trimmedDescription)
|
|
271
260
|
}
|
|
272
261
|
continue
|
|
273
262
|
}
|
|
274
|
-
|
|
263
|
+
// A closure type that mentions `Any` is reported by `diagnoseClosureTypes` instead.
|
|
264
|
+
for parameter in closure.parameters
|
|
265
|
+
where !containsFunctionType(parameter.type) && !mentionsFreeFormAny(parameter.type) {
|
|
275
266
|
callbackArgumentTypes.append(parameter.type.trimmedDescription)
|
|
276
267
|
}
|
|
277
|
-
|
|
268
|
+
let closureReturnType = closure.functionType.returnClause.type
|
|
269
|
+
if let returnType = closure.returnType,
|
|
270
|
+
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType) {
|
|
278
271
|
decodableTypes.append(returnType)
|
|
279
272
|
}
|
|
280
273
|
}
|
|
281
274
|
let returnType = funcDecl.signature.returnClause?.type
|
|
282
275
|
let encodableType = returnType.flatMap {
|
|
283
|
-
isVoidType($0) || containsFunctionType($0) ? nil : $0.trimmedDescription
|
|
276
|
+
isVoidType($0) || containsFunctionType($0) || mentionsFreeFormAny($0) ? nil : $0.trimmedDescription
|
|
284
277
|
}
|
|
285
278
|
return BoundaryMember(
|
|
286
279
|
name: funcDecl.name.text,
|
|
@@ -297,7 +290,11 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
297
290
|
let type = binding.typeAnnotation?.type,
|
|
298
291
|
!containsFunctionType(type) {
|
|
299
292
|
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
300
|
-
// decoded on write, so its value type is asserted in both directions.
|
|
293
|
+
// decoded on write, so its value type is asserted in both directions. A type that mentions `Any`
|
|
294
|
+
// converts through the free-form methods instead, so it isn't asserted at all.
|
|
295
|
+
guard !mentionsFreeFormAny(type) else {
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
301
298
|
let typeText = type.trimmedDescription
|
|
302
299
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
303
300
|
let isSettable = isVar && bindingIsSettable(binding)
|