expo-modules-macros 0.18.0 → 0.19.1
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 +4 -3
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +15 -7
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +101 -52
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +42 -86
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +19 -7
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +0 -50
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +10 -8
- 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 +99 -27
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +3 -1
- package/build/types.d.ts +18 -3
- package/build/types.js +1 -1
- package/package.json +3 -3
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -93,7 +93,8 @@ internal func argumentDecodeStatements(
|
|
|
93
93
|
// A specifier such as `borrowing` belongs to the parameter, not to the type that decodes.
|
|
94
94
|
let type = SpecifiedType(type).type
|
|
95
95
|
guard let closure = JSClosureType(type) else {
|
|
96
|
-
|
|
96
|
+
let decode = decodeCall(type.trimmedDescription, freeForm: freeFormConversion(of: type), from: valueExpression)
|
|
97
|
+
return ["let \(name) = try \(decode)"]
|
|
97
98
|
}
|
|
98
99
|
return closureArgumentStatements(closure, into: name, from: valueExpression)
|
|
99
100
|
}
|
|
@@ -279,7 +280,7 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
// The closure's parameters are encoded and its result is decoded, so neither can be another
|
|
282
|
-
// closure or a
|
|
283
|
+
// closure or a type that mentions `Any`, in any shape.
|
|
283
284
|
let returnType = functionType.returnClause.type
|
|
284
285
|
for type in closure.parameters.map(\.type) + [returnType] {
|
|
285
286
|
if let nested = firstFunctionType(in: type) {
|
|
@@ -290,7 +291,7 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
290
291
|
"A @JS closure argument can't take or return another closure.",
|
|
291
292
|
id: "js-closure-nested"
|
|
292
293
|
)))
|
|
293
|
-
} else if
|
|
294
|
+
} else if mentionsFreeFormAny(type) {
|
|
294
295
|
context.diagnose(
|
|
295
296
|
Diagnostic(
|
|
296
297
|
node: type,
|
|
@@ -19,6 +19,8 @@ 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
|
|
|
@@ -29,6 +31,7 @@ internal struct JSFunction {
|
|
|
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
|
|
@@ -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()"))
|
|
@@ -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.
|
|
@@ -148,99 +148,46 @@ private func diagnoseParameterSpecifiers(
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/// Emits the free-form (`Any
|
|
151
|
+
/// Emits the free-form (`Any`-bearing) diagnostics for a `@JS` declaration.
|
|
152
152
|
///
|
|
153
|
-
/// A
|
|
154
|
-
/// `JavaScriptValue
|
|
155
|
-
///
|
|
156
|
-
///
|
|
157
|
-
///
|
|
158
|
-
///
|
|
159
|
-
/// - a **property** typed free-form is an error regardless of settability, because its getter always
|
|
160
|
-
/// encodes.
|
|
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.
|
|
161
159
|
///
|
|
162
|
-
/// Types are matched
|
|
163
|
-
///
|
|
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`.
|
|
164
162
|
private func diagnoseFreeFormTypes(
|
|
165
163
|
in declaration: some DeclSyntaxProtocol,
|
|
166
164
|
in context: some MacroExpansionContext
|
|
167
165
|
) {
|
|
166
|
+
var types: [TypeSyntax] = []
|
|
168
167
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
169
|
-
|
|
170
|
-
if let returnType = funcDecl.signature.returnClause?.type
|
|
171
|
-
|
|
172
|
-
context.diagnose(
|
|
173
|
-
Diagnostic(node: returnType, message: freeFormReturnError(for: returnType.trimmedDescription)))
|
|
168
|
+
types = funcDecl.signature.parameterClause.parameters.map { SpecifiedType($0.type).type }
|
|
169
|
+
if let returnType = funcDecl.signature.returnClause?.type {
|
|
170
|
+
types.append(returnType)
|
|
174
171
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
warnFreeFormArguments(initDecl.signature.parameterClause.parameters, in: context)
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
186
|
-
let type = varDecl.bindings.first?.typeAnnotation?.type,
|
|
187
|
-
isFreeFormBoundaryType(type.trimmedDescription) {
|
|
188
|
-
context.diagnose(
|
|
189
|
-
Diagnostic(node: type, message: freeFormPropertyError(for: type.trimmedDescription)))
|
|
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]
|
|
190
178
|
}
|
|
191
|
-
}
|
|
192
179
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
private func suggestedAlternative(for freeFormType: String) -> String {
|
|
196
|
-
if let reshaped = typedFreeFormReplacement(for: freeFormType), reshaped != "JavaScriptValue" {
|
|
197
|
-
return "Use '\(reshaped)', or 'JavaScriptValue' to pass a JS value through unchanged."
|
|
198
|
-
}
|
|
199
|
-
return "Use a concrete type, or 'JavaScriptValue' to pass a JS value through unchanged."
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/// Emits the steering warning for each free-form parameter in a list. Shared by the function and
|
|
203
|
-
/// constructor cases, which both decode their arguments through the same path.
|
|
204
|
-
private func warnFreeFormArguments(
|
|
205
|
-
_ parameters: FunctionParameterListSyntax,
|
|
206
|
-
in context: some MacroExpansionContext
|
|
207
|
-
) {
|
|
208
|
-
for parameter in parameters {
|
|
209
|
-
// Matched on the bare type: `borrowing [String: Any]` decodes as free-form too.
|
|
210
|
-
let type = SpecifiedType(parameter.type).type.trimmedDescription
|
|
211
|
-
guard let suggested = typedFreeFormReplacement(for: type) else {
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
180
|
+
for type in types
|
|
181
|
+
where mentionsFreeFormAny(type) && !isConvertibleFreeFormType(type) && !containsFunctionType(type) {
|
|
214
182
|
context.diagnose(
|
|
215
|
-
Diagnostic(
|
|
216
|
-
node: parameter.type,
|
|
217
|
-
message: freeFormArgumentWarning(for: type, suggesting: suggested)))
|
|
183
|
+
Diagnostic(node: type, message: unconvertibleFreeFormError(for: type.trimmedDescription)))
|
|
218
184
|
}
|
|
219
185
|
}
|
|
220
186
|
|
|
221
|
-
private func
|
|
222
|
-
for freeFormType: String,
|
|
223
|
-
suggesting suggestedType: String
|
|
224
|
-
) -> JSDiagnosticMessage {
|
|
187
|
+
private func unconvertibleFreeFormError(for type: String) -> JSDiagnosticMessage {
|
|
225
188
|
return JSDiagnosticMessage(
|
|
226
|
-
"
|
|
227
|
-
id: "js-free-form-
|
|
228
|
-
severity: .warning
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private func freeFormReturnError(for freeFormType: String) -> JSDiagnosticMessage {
|
|
233
|
-
return JSDiagnosticMessage(
|
|
234
|
-
"A @JS function can't return the free-form '\(freeFormType)': there's no way to encode an untyped value back to JavaScript. \(suggestedAlternative(for: freeFormType))",
|
|
235
|
-
id: "js-free-form-return",
|
|
236
|
-
severity: .error
|
|
237
|
-
)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private func freeFormPropertyError(for freeFormType: String) -> JSDiagnosticMessage {
|
|
241
|
-
return JSDiagnosticMessage(
|
|
242
|
-
"A @JS property can't have the free-form '\(freeFormType)': its getter would have to encode an untyped value back to JavaScript, which isn't supported. \(suggestedAlternative(for: freeFormType))",
|
|
243
|
-
id: "js-free-form-property",
|
|
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",
|
|
244
191
|
severity: .error
|
|
245
192
|
)
|
|
246
193
|
}
|
|
@@ -289,9 +236,10 @@ private struct BoundaryMember {
|
|
|
289
236
|
/// function contributes its parameter types (decodable) and its return type when non-Void (encodable);
|
|
290
237
|
/// a property contributes its value type as encodable (the getter) and also as decodable when settable
|
|
291
238
|
/// (the setter). Composed types (`[Int]`, `String?`, …) are kept verbatim, their conditional
|
|
292
|
-
/// conformances transitively constraining the elements.
|
|
293
|
-
///
|
|
294
|
-
///
|
|
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`.
|
|
295
243
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
296
244
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
297
245
|
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
@@ -307,21 +255,25 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
307
255
|
continue
|
|
308
256
|
}
|
|
309
257
|
guard let closure = JSClosureType(specified.type) else {
|
|
310
|
-
if !containsFunctionType(specified.type) {
|
|
258
|
+
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
311
259
|
decodableTypes.append(specified.type.trimmedDescription)
|
|
312
260
|
}
|
|
313
261
|
continue
|
|
314
262
|
}
|
|
315
|
-
|
|
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) {
|
|
316
266
|
callbackArgumentTypes.append(parameter.type.trimmedDescription)
|
|
317
267
|
}
|
|
318
|
-
|
|
268
|
+
let closureReturnType = closure.functionType.returnClause.type
|
|
269
|
+
if let returnType = closure.returnType,
|
|
270
|
+
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType) {
|
|
319
271
|
decodableTypes.append(returnType)
|
|
320
272
|
}
|
|
321
273
|
}
|
|
322
274
|
let returnType = funcDecl.signature.returnClause?.type
|
|
323
275
|
let encodableType = returnType.flatMap {
|
|
324
|
-
isVoidType($0) || containsFunctionType($0) ? nil : $0.trimmedDescription
|
|
276
|
+
isVoidType($0) || containsFunctionType($0) || mentionsFreeFormAny($0) ? nil : $0.trimmedDescription
|
|
325
277
|
}
|
|
326
278
|
return BoundaryMember(
|
|
327
279
|
name: funcDecl.name.text,
|
|
@@ -338,7 +290,11 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
338
290
|
let type = binding.typeAnnotation?.type,
|
|
339
291
|
!containsFunctionType(type) {
|
|
340
292
|
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
341
|
-
// 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
|
+
}
|
|
342
298
|
let typeText = type.trimmedDescription
|
|
343
299
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
344
300
|
let isSettable = isVar && bindingIsSettable(binding)
|
|
@@ -366,17 +366,27 @@ internal func expressionType(_ type: String) -> String {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
/// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
|
|
369
|
-
/// as it appears after `try`. A
|
|
370
|
-
///
|
|
371
|
-
///
|
|
372
|
-
///
|
|
373
|
-
internal func decodeCall(_ type: String, from valueExpression: String) -> String {
|
|
374
|
-
if let
|
|
375
|
-
return
|
|
369
|
+
/// as it appears after `try`. A type that mentions `Any` can't conform to `JavaScriptDecodable`, so it
|
|
370
|
+
/// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names; every other
|
|
371
|
+
/// type decodes through its own static `decode`, spelled in expression position (`T!` rewritten to
|
|
372
|
+
/// `T?`).
|
|
373
|
+
internal func decodeCall(_ type: String, freeForm: FreeFormConversion?, from valueExpression: String) -> String {
|
|
374
|
+
if let freeForm {
|
|
375
|
+
return freeForm.decodeExpression(of: valueExpression, as: expressionType(type))
|
|
376
376
|
}
|
|
377
377
|
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
/// The encode expression for the native value `valueExpression` of a boundary type, as it appears
|
|
381
|
+
/// after `try`. The counterpart of `decodeCall`: a type that mentions `Any` encodes through the
|
|
382
|
+
/// `JavaScriptValue` free-form methods, and every other type through its own static `encode`.
|
|
383
|
+
internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String) -> String {
|
|
384
|
+
if let freeForm {
|
|
385
|
+
return freeForm.encodeExpression(of: valueExpression)
|
|
386
|
+
}
|
|
387
|
+
return "\(expressionType(type)).encode(\(valueExpression), in: runtime)"
|
|
388
|
+
}
|
|
389
|
+
|
|
380
390
|
// MARK: - Escaped names
|
|
381
391
|
|
|
382
392
|
/// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
|
|
@@ -429,6 +439,8 @@ internal func collectProperties(
|
|
|
429
439
|
swiftName: swiftName,
|
|
430
440
|
jsName: jsNameOverride ?? withoutBackticks(swiftName),
|
|
431
441
|
valueType: valueType,
|
|
442
|
+
// A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
|
|
443
|
+
freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
|
|
432
444
|
isSettable: isVar && bindingIsSettable(binding)
|
|
433
445
|
)
|
|
434
446
|
}
|
|
@@ -143,56 +143,6 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
143
143
|
|
|
144
144
|
// MARK: - Property model
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
How a property whose type mentions `Any` converts on the JS-value paths. Such a type can't conform to
|
|
148
|
-
the JS codable protocols, so the property converts through the `JavaScriptValue` free-form methods.
|
|
149
|
-
*/
|
|
150
|
-
private enum FreeFormConversion {
|
|
151
|
-
/// `Any`, `[Any]` or `[String: Any]`, wrapped in at most one optional: the dedicated methods for
|
|
152
|
-
/// that shape, named by `decodeMethod` and `encodeMethod`. The optional is handled around them.
|
|
153
|
-
case shape(decodeMethod: String, encodeMethod: String, isOptional: Bool, isBareAny: Bool)
|
|
154
|
-
/// Any other type that mentions `Any`: the generic `decodeAny(_:as:in:)` and `encodeAny(_:in:)`.
|
|
155
|
-
case generic
|
|
156
|
-
|
|
157
|
-
/// Classifies a type that mentions `Any`; the caller checks `isConvertibleFreeFormType` first.
|
|
158
|
-
init(type: TypeSyntax) {
|
|
159
|
-
let wrapped = optionalWrappedType(type)
|
|
160
|
-
switch freeFormShape(of: wrapped ?? type) {
|
|
161
|
-
case .any:
|
|
162
|
-
self = .shape(
|
|
163
|
-
decodeMethod: "decodeAny",
|
|
164
|
-
encodeMethod: "encodeAny",
|
|
165
|
-
isOptional: wrapped != nil,
|
|
166
|
-
isBareAny: true
|
|
167
|
-
)
|
|
168
|
-
case .array:
|
|
169
|
-
self = .shape(
|
|
170
|
-
decodeMethod: "decodeAnyArray",
|
|
171
|
-
encodeMethod: "encodeAnyArray",
|
|
172
|
-
isOptional: wrapped != nil,
|
|
173
|
-
isBareAny: false
|
|
174
|
-
)
|
|
175
|
-
case .dictionary:
|
|
176
|
-
self = .shape(
|
|
177
|
-
decodeMethod: "decodeAnyDictionary",
|
|
178
|
-
encodeMethod: "encodeAnyDictionary",
|
|
179
|
-
isOptional: wrapped != nil,
|
|
180
|
-
isBareAny: false
|
|
181
|
-
)
|
|
182
|
-
case nil:
|
|
183
|
-
self = .generic
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/// True for `Any` itself, optional or not.
|
|
188
|
-
var isBareAny: Bool {
|
|
189
|
-
guard case .shape(_, _, _, let isBareAny) = self else {
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
return isBareAny
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
146
|
/**
|
|
197
147
|
A single record property discovered on the type, paired with everything the synthesized
|
|
198
148
|
conversions need: its property name, its written type, and how the source may omit it.
|
|
@@ -88,22 +88,23 @@ internal func directionalConformanceAssertion(
|
|
|
88
88
|
// Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
|
|
89
89
|
// constrained `JavaScriptDecodable` per argument, one `E0…` slot constrained `JavaScriptEncodable`
|
|
90
90
|
// per closure-argument parameter, and a single `Return` slot constrained `JavaScriptEncodable` for
|
|
91
|
-
// the return/getter value (there's only ever one, so it isn't indexed).
|
|
91
|
+
// the return/getter value (there's only ever one, so it isn't indexed). Every slot suppresses
|
|
92
|
+
// `Copyable`, so a non-copyable boundary type such as `JavaScriptValue` can fill it.
|
|
92
93
|
var parameters: [String] = []
|
|
93
94
|
var typeParameters: [String] = []
|
|
94
95
|
var arguments: [String] = []
|
|
95
96
|
for (index, type) in decodables.enumerated() {
|
|
96
|
-
parameters.append("A\(index): \(javaScriptDecodableProtocolName)")
|
|
97
|
+
parameters.append("A\(index): \(javaScriptDecodableProtocolName) & ~Copyable")
|
|
97
98
|
typeParameters.append("_: A\(index).Type")
|
|
98
99
|
arguments.append("\(type).self")
|
|
99
100
|
}
|
|
100
101
|
for (index, type) in callbackArguments.enumerated() {
|
|
101
|
-
parameters.append("E\(index): \(javaScriptEncodableProtocolName)")
|
|
102
|
+
parameters.append("E\(index): \(javaScriptEncodableProtocolName) & ~Copyable")
|
|
102
103
|
typeParameters.append("_: E\(index).Type")
|
|
103
104
|
arguments.append("\(type).self")
|
|
104
105
|
}
|
|
105
106
|
if let encodable {
|
|
106
|
-
parameters.append("Return: \(javaScriptEncodableProtocolName)")
|
|
107
|
+
parameters.append("Return: \(javaScriptEncodableProtocolName) & ~Copyable")
|
|
107
108
|
typeParameters.append("_: Return.Type")
|
|
108
109
|
arguments.append("\(encodable).self")
|
|
109
110
|
}
|
|
@@ -143,10 +144,11 @@ internal func typeConformanceAssertions(
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/// The type to assert for a boundary type as written, or `nil` when nothing needs asserting: an
|
|
146
|
-
/// exactly-free-form type (
|
|
147
|
-
/// primitive both return `nil`; everything else is unwrapped of trailing optionals
|
|
148
|
-
/// *optional* free-form (`[String: Any]?`)
|
|
149
|
-
/// asserted for a clean diagnostic.
|
|
147
|
+
/// exactly-free-form type (converted through the `JavaScriptValue` free-form methods) and a
|
|
148
|
+
/// known-conforming primitive both return `nil`; everything else is unwrapped of trailing optionals
|
|
149
|
+
/// and asserted. Shared with `@Event`, where an *optional* free-form type (`[String: Any]?`) is kept
|
|
150
|
+
/// and asserted for a clean diagnostic. `@JS` leaves out every type that mentions `Any` before it gets
|
|
151
|
+
/// here.
|
|
150
152
|
internal func assertableBoundaryType(_ type: String) -> String? {
|
|
151
153
|
guard !isFreeFormBoundaryType(type) else {
|
|
152
154
|
return nil
|