expo-modules-macros 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +28 -4
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +3 -2
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +23 -10
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +13 -1
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +71 -54
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +11 -4
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +16 -5
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +13 -1
- package/apple/Sources/ExpoModulesMacros/TupleTypes.swift +234 -0
- package/apple/Sources/ExpoModulesMacros/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +104 -41
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +16 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +9 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesScanner/Exports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +47 -4
- package/build/types.d.ts +32 -1
- package/build/types.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// Recognition of boundary types that hold a tuple, and the conversions the macros emit for them. A
|
|
4
|
+
/// tuple can't conform to a protocol, so neither it nor an array, optional or dictionary of tuples can
|
|
5
|
+
/// convert through the usual `T.decode` / `T.encode` path. Such a type converts through the
|
|
6
|
+
/// `JavaScriptValue` tuple and container methods instead, which take one closure per element, so the
|
|
7
|
+
/// conversion follows the structure of the type: a tuple, optional, array or `String`-keyed dictionary
|
|
8
|
+
/// that holds a tuple converts through the matching method, and every other type in it through its own
|
|
9
|
+
/// `decode` / `encode`.
|
|
10
|
+
///
|
|
11
|
+
/// An unlabeled tuple maps to a JavaScript array with one value per element, and a labeled tuple to an
|
|
12
|
+
/// object with one property per label. Recognized are tuples of two or more elements where either every
|
|
13
|
+
/// element is labeled or none is. `TypeConformanceAssertion` asserts the types that convert through
|
|
14
|
+
/// their own `decode` / `encode` (see `conformingTypes`) in place of the whole type.
|
|
15
|
+
internal enum TupleConversion {
|
|
16
|
+
/// A type that holds no tuple, which converts through its own `decode` / `encode`.
|
|
17
|
+
case conforming(type: String)
|
|
18
|
+
/// A tuple: the conversion of each element, with one label per element for a labeled tuple.
|
|
19
|
+
case tuple(elements: [TupleConversion], labels: [String]?)
|
|
20
|
+
/// An optional of a type that holds a tuple.
|
|
21
|
+
indirect case optional(wrapped: TupleConversion)
|
|
22
|
+
/// An array of a type that holds a tuple.
|
|
23
|
+
indirect case array(element: TupleConversion)
|
|
24
|
+
/// A `String`-keyed dictionary whose values hold a tuple.
|
|
25
|
+
indirect case dictionary(value: TupleConversion)
|
|
26
|
+
|
|
27
|
+
/// The decode expression, as it appears after `try`, for a value read from `valueExpression` (a
|
|
28
|
+
/// `JavaScriptUnownedValue`). A tuple whose elements all conform passes the element types; any other
|
|
29
|
+
/// tuple and every container pass closures, which take the value and the runtime as parameters that
|
|
30
|
+
/// shadow the outer ones, so a closure captures nothing.
|
|
31
|
+
func decodeExpression(of valueExpression: String) -> String {
|
|
32
|
+
switch self {
|
|
33
|
+
case .conforming(let type):
|
|
34
|
+
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
35
|
+
case .tuple(let elements, let labels):
|
|
36
|
+
if let types = conformingElementTypes(elements) {
|
|
37
|
+
let metatypes = types.map { "\(expressionType($0)).self" }.joined(separator: ", ")
|
|
38
|
+
let arguments = [valueExpression] + labelsArgument(labels) + ["as: \(metatypes)", "in: runtime"]
|
|
39
|
+
return "JavaScriptValue.decodeTuple(\(arguments.joined(separator: ", ")))"
|
|
40
|
+
}
|
|
41
|
+
let closures = elements.map { elementClosure(returning: $0.decodeExpression(of: "value")) }
|
|
42
|
+
let arguments = [valueExpression] + labelsArgument(labels) + ["in: runtime"] + closures
|
|
43
|
+
return "JavaScriptValue.decodeTuple(\(arguments.joined(separator: ", ")))"
|
|
44
|
+
case .optional(let wrapped):
|
|
45
|
+
return containerDecode("decodeOptional", label: "wrapped", inner: wrapped, of: valueExpression)
|
|
46
|
+
case .array(let element):
|
|
47
|
+
return containerDecode("decodeArray", label: "element", inner: element, of: valueExpression)
|
|
48
|
+
case .dictionary(let value):
|
|
49
|
+
return containerDecode("decodeDictionary", label: "value", inner: value, of: valueExpression)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// The encode expression, as it appears after `try`, for the value `valueExpression` in the runtime
|
|
54
|
+
/// named `runtime`. A tuple whose elements all conform needs no closures. The tuple method takes an
|
|
55
|
+
/// unlabeled tuple, so a labeled one is coerced to its unlabeled type first.
|
|
56
|
+
func encodeExpression(of valueExpression: String, in runtime: String = "runtime") -> String {
|
|
57
|
+
switch self {
|
|
58
|
+
case .conforming(let type):
|
|
59
|
+
return "\(expressionType(type)).encode(\(valueExpression), in: \(runtime))"
|
|
60
|
+
case .tuple(let elements, let labels):
|
|
61
|
+
let value = labels == nil ? valueExpression : "\(valueExpression) as \(unlabeledType)"
|
|
62
|
+
let closures =
|
|
63
|
+
conformingElementTypes(elements) == nil
|
|
64
|
+
? elements.map { elementClosure(returning: $0.encodeExpression(of: "value")) } : []
|
|
65
|
+
let arguments = [value] + labelsArgument(labels) + ["in: \(runtime)"] + closures
|
|
66
|
+
return "JavaScriptValue.encodeTuple(\(arguments.joined(separator: ", ")))"
|
|
67
|
+
case .optional(let wrapped):
|
|
68
|
+
return containerEncode("encodeOptional", label: "wrapped", inner: wrapped, of: valueExpression, in: runtime)
|
|
69
|
+
case .array(let element):
|
|
70
|
+
return containerEncode("encodeArray", label: "element", inner: element, of: valueExpression, in: runtime)
|
|
71
|
+
case .dictionary(let value):
|
|
72
|
+
return containerEncode("encodeDictionary", label: "value", inner: value, of: valueExpression, in: runtime)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The JavaScript value kinds the decode accepts, as an expression of type `JavaScriptValueKinds`, for
|
|
77
|
+
/// a `@Union` that picks a payload by kind. Mirrors the kinds of the matching `JavaScriptCodable`
|
|
78
|
+
/// conformances: an array decodes a non-array value as a single element.
|
|
79
|
+
var decodableKindsExpression: String {
|
|
80
|
+
switch self {
|
|
81
|
+
case .conforming(let type):
|
|
82
|
+
return "\(expressionType(type)).decodableKinds"
|
|
83
|
+
case .tuple:
|
|
84
|
+
return "JavaScriptValueKinds.object"
|
|
85
|
+
case .optional(let wrapped):
|
|
86
|
+
return "\(wrapped.decodableKindsExpression).union([.null, .undefined])"
|
|
87
|
+
case .array(let element):
|
|
88
|
+
return "\(element.decodableKindsExpression).union(JavaScriptValueKinds.object)"
|
|
89
|
+
case .dictionary:
|
|
90
|
+
return "JavaScriptValueKinds.object"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// The types in the structure that convert through their own `decode` / `encode`, in order, as
|
|
95
|
+
/// written.
|
|
96
|
+
var conformingTypes: [String] {
|
|
97
|
+
switch self {
|
|
98
|
+
case .conforming(let type):
|
|
99
|
+
return [type]
|
|
100
|
+
case .tuple(let elements, _):
|
|
101
|
+
return elements.flatMap(\.conformingTypes)
|
|
102
|
+
case .optional(let inner), .array(let inner), .dictionary(let inner):
|
|
103
|
+
return inner.conformingTypes
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The type spelled in expression position, with tuple labels kept for nested tuples.
|
|
108
|
+
private var spelledType: String {
|
|
109
|
+
switch self {
|
|
110
|
+
case .conforming(let type):
|
|
111
|
+
return expressionType(type)
|
|
112
|
+
case .tuple(let elements, let labels):
|
|
113
|
+
let spelled = elements.enumerated().map { index, element in
|
|
114
|
+
labels.map { "\($0[index]): \(element.spelledType)" } ?? element.spelledType
|
|
115
|
+
}
|
|
116
|
+
return "(\(spelled.joined(separator: ", ")))"
|
|
117
|
+
case .optional(let wrapped):
|
|
118
|
+
return "\(wrapped.spelledType)?"
|
|
119
|
+
case .array(let element):
|
|
120
|
+
return "[\(element.spelledType)]"
|
|
121
|
+
case .dictionary(let value):
|
|
122
|
+
return "[String: \(value.spelledType)]"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// The type of a tuple with its own labels dropped. Labels of nested tuples are kept, since the
|
|
127
|
+
/// nested conversion handles them.
|
|
128
|
+
private var unlabeledType: String {
|
|
129
|
+
guard case .tuple(let elements, _) = self else {
|
|
130
|
+
return spelledType
|
|
131
|
+
}
|
|
132
|
+
return "(\(elements.map(\.spelledType).joined(separator: ", ")))"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func containerDecode(
|
|
136
|
+
_ method: String,
|
|
137
|
+
label: String,
|
|
138
|
+
inner: TupleConversion,
|
|
139
|
+
of valueExpression: String
|
|
140
|
+
) -> String {
|
|
141
|
+
return
|
|
142
|
+
"JavaScriptValue.\(method)(\(valueExpression), in: runtime, \(label): \(elementClosure(returning: inner.decodeExpression(of: "value"))))"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func containerEncode(
|
|
146
|
+
_ method: String,
|
|
147
|
+
label: String,
|
|
148
|
+
inner: TupleConversion,
|
|
149
|
+
of valueExpression: String,
|
|
150
|
+
in runtime: String
|
|
151
|
+
) -> String {
|
|
152
|
+
return
|
|
153
|
+
"JavaScriptValue.\(method)(\(valueExpression), in: \(runtime), \(label): \(elementClosure(returning: inner.encodeExpression(of: "value"))))"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// The types of a tuple's elements when every element converts through its own `decode` /
|
|
157
|
+
/// `encode`, so the tuple can use the overloads that take no closures; `nil` otherwise.
|
|
158
|
+
private func conformingElementTypes(_ elements: [TupleConversion]) -> [String]? {
|
|
159
|
+
var types: [String] = []
|
|
160
|
+
for element in elements {
|
|
161
|
+
guard case .conforming(let type) = element else {
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
types.append(type)
|
|
165
|
+
}
|
|
166
|
+
return types
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func labelsArgument(_ labels: [String]?) -> [String] {
|
|
170
|
+
guard let labels else {
|
|
171
|
+
return []
|
|
172
|
+
}
|
|
173
|
+
return ["labels: [\(labels.map { "\"\($0)\"" }.joined(separator: ", "))]"]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The conversion for a boundary type that holds a tuple, or `nil` when the type holds no tuple the
|
|
178
|
+
/// tuple methods convert (a single parenthesized type, a tuple that labels only some elements, or one
|
|
179
|
+
/// with a variadic element converts as it's written, and fails its conformance assertion).
|
|
180
|
+
internal func tupleConversion(of type: TypeSyntax) -> TupleConversion? {
|
|
181
|
+
let conversion = structure(of: type)
|
|
182
|
+
guard case .conforming = conversion else {
|
|
183
|
+
return conversion
|
|
184
|
+
}
|
|
185
|
+
return nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// The conversion for a boundary type written as a string. See `tupleConversion(of:)`.
|
|
189
|
+
internal func tupleConversion(of type: String) -> TupleConversion? {
|
|
190
|
+
return tupleConversion(of: TypeSyntax(stringLiteral: type))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// The conversion of `type`, which is `.conforming` when the type holds no recognized tuple.
|
|
194
|
+
private func structure(of type: TypeSyntax) -> TupleConversion {
|
|
195
|
+
let conforming = TupleConversion.conforming(type: type.trimmedDescription)
|
|
196
|
+
if let tuple = type.as(TupleTypeSyntax.self) {
|
|
197
|
+
// `(T)` is the same type as `T`.
|
|
198
|
+
if tuple.elements.count == 1, let only = tuple.elements.first, only.firstName == nil {
|
|
199
|
+
return structure(of: only.type)
|
|
200
|
+
}
|
|
201
|
+
guard tuple.elements.count >= 2,
|
|
202
|
+
tuple.elements.allSatisfy({ $0.ellipsis == nil && $0.inoutKeyword == nil })
|
|
203
|
+
else {
|
|
204
|
+
return conforming
|
|
205
|
+
}
|
|
206
|
+
let labels = tuple.elements.compactMap { $0.firstName?.text }
|
|
207
|
+
guard labels.isEmpty || labels.count == tuple.elements.count else {
|
|
208
|
+
return conforming
|
|
209
|
+
}
|
|
210
|
+
return .tuple(elements: tuple.elements.map { structure(of: $0.type) }, labels: labels.isEmpty ? nil : labels)
|
|
211
|
+
}
|
|
212
|
+
let inner: TupleConversion
|
|
213
|
+
let wrap: (TupleConversion) -> TupleConversion
|
|
214
|
+
if let wrapped = optionalWrappedType(type) {
|
|
215
|
+
(inner, wrap) = (structure(of: wrapped), TupleConversion.optional)
|
|
216
|
+
} else if let element = arrayElementType(type) {
|
|
217
|
+
(inner, wrap) = (structure(of: element), TupleConversion.array)
|
|
218
|
+
} else if let value = stringKeyedDictionaryValueType(type) {
|
|
219
|
+
(inner, wrap) = (structure(of: value), TupleConversion.dictionary)
|
|
220
|
+
} else {
|
|
221
|
+
return conforming
|
|
222
|
+
}
|
|
223
|
+
// A container that holds no tuple conforms as a whole.
|
|
224
|
+
if case .conforming = inner {
|
|
225
|
+
return conforming
|
|
226
|
+
}
|
|
227
|
+
return wrap(inner)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// An element closure that returns `expression`, as it appears after `try`. It takes the value and
|
|
231
|
+
/// the runtime as parameters, which shadow the outer ones, so it captures nothing.
|
|
232
|
+
internal func elementClosure(returning expression: String) -> String {
|
|
233
|
+
return "{ value, runtime in return try \(expression) }"
|
|
234
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// The type aliases declared in the body of a class whose aliased type holds a tuple (see
|
|
4
|
+
/// `TupleConversion`), resolved to that type. A macro sees only the declaration it's attached to, so
|
|
5
|
+
/// `@ExpoModule` and `@SharedObject` collect the aliases from their class body, and resolve them in
|
|
6
|
+
/// the `@JS` members they bind before reading the members' types: an alias of a tuple can't convert
|
|
7
|
+
/// through `decode` / `encode` under its own name, because the tuple can't conform.
|
|
8
|
+
///
|
|
9
|
+
/// An alias declared anywhere else (at file level, in an extension or in another type) isn't visible
|
|
10
|
+
/// to the macros, so it converts under its own name and fails its conformance assertion. A generic
|
|
11
|
+
/// alias isn't resolved either.
|
|
12
|
+
internal struct TupleTypeAliases {
|
|
13
|
+
/// The resolved type of each alias, by name.
|
|
14
|
+
private let resolved: [String: TypeSyntax]
|
|
15
|
+
|
|
16
|
+
init(in memberBlock: MemberBlockSyntax) {
|
|
17
|
+
var declared: [String: TypeSyntax] = [:]
|
|
18
|
+
for member in memberBlock.members {
|
|
19
|
+
guard let alias = member.decl.as(TypeAliasDeclSyntax.self), alias.genericParameterClause == nil else {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
declared[alias.name.text] = alias.initializer.value
|
|
23
|
+
}
|
|
24
|
+
// Aliases may refer to each other, so each one is resolved through the others. An alias that refers
|
|
25
|
+
// back to itself is invalid Swift, and is left as written after `declared.count` rounds.
|
|
26
|
+
var resolved = declared
|
|
27
|
+
for _ in 0..<declared.count {
|
|
28
|
+
let rewriter = AliasRewriter(aliases: resolved)
|
|
29
|
+
resolved = resolved.mapValues { rewriter.rewrite($0).cast(TypeSyntax.self) }
|
|
30
|
+
}
|
|
31
|
+
self.resolved = resolved.filter { tupleConversion(of: $0.value) != nil }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// True when the class body declares no alias that holds a tuple.
|
|
35
|
+
var isEmpty: Bool {
|
|
36
|
+
return resolved.isEmpty
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// `node` with each reference to an alias that holds a tuple replaced by the aliased type.
|
|
40
|
+
func resolving<Node: SyntaxProtocol>(_ node: Node) -> Node {
|
|
41
|
+
guard !resolved.isEmpty else {
|
|
42
|
+
return node
|
|
43
|
+
}
|
|
44
|
+
return AliasRewriter(aliases: resolved).rewrite(node).cast(Node.self)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Replaces each type reference to an alias by the aliased type: a bare name (`Pair`) or one qualified
|
|
49
|
+
/// with `Self` (`Self.Pair`). A name with generic arguments isn't an alias reference, since the
|
|
50
|
+
/// resolved aliases aren't generic.
|
|
51
|
+
private final class AliasRewriter: SyntaxRewriter {
|
|
52
|
+
let aliases: [String: TypeSyntax]
|
|
53
|
+
|
|
54
|
+
init(aliases: [String: TypeSyntax]) {
|
|
55
|
+
self.aliases = aliases
|
|
56
|
+
super.init(viewMode: .sourceAccurate)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax {
|
|
60
|
+
guard node.genericArgumentClause == nil, let aliased = aliases[node.name.text] else {
|
|
61
|
+
return TypeSyntax(node)
|
|
62
|
+
}
|
|
63
|
+
return aliased.with(\.leadingTrivia, node.leadingTrivia).with(\.trailingTrivia, node.trailingTrivia)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override func visit(_ node: MemberTypeSyntax) -> TypeSyntax {
|
|
67
|
+
guard node.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Self",
|
|
68
|
+
node.genericArgumentClause == nil,
|
|
69
|
+
let aliased = aliases[node.name.text]
|
|
70
|
+
else {
|
|
71
|
+
return super.visit(node)
|
|
72
|
+
}
|
|
73
|
+
return aliased.with(\.leadingTrivia, node.leadingTrivia).with(\.trailingTrivia, node.trailingTrivia)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -40,57 +40,78 @@ internal struct ConformanceAssertion {
|
|
|
40
40
|
let types: [String]
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/// One type crossing the boundary for a member, as a parameter of the member's assertion helper: the
|
|
44
|
+
/// type as written, the protocol it must conform to for the direction it travels, and the argument label
|
|
45
|
+
/// it's passed under.
|
|
46
|
+
internal struct AssertedType {
|
|
47
|
+
/// The argument label, or `nil` for an unlabeled parameter.
|
|
48
|
+
let label: String?
|
|
49
|
+
/// The type as written.
|
|
50
|
+
let type: String
|
|
51
|
+
/// `JavaScriptDecodable` for a type decoded from JavaScript, `JavaScriptEncodable` for one encoded to it.
|
|
52
|
+
let constraint: String
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/// A directional conformance-assertion peer for a `@JS` member: a never-called `private func` whose
|
|
44
|
-
/// body asserts each argument type is `JavaScriptDecodable` and the return
|
|
56
|
+
/// body asserts that each argument type is `JavaScriptDecodable` and the return or getter type is
|
|
45
57
|
/// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
|
|
46
|
-
/// encode on the way out). The
|
|
47
|
-
/// parameter per asserted type, called once with every type's metatype
|
|
48
|
-
///
|
|
49
|
-
///
|
|
58
|
+
/// encode on the way out). The body is a local helper named after the member, with one opaque metatype
|
|
59
|
+
/// parameter per asserted type, called once with every type's metatype (see
|
|
60
|
+
/// `memberConformanceAssertion`). Naming the helper after the member and its labels puts the member in
|
|
61
|
+
/// the compiler's diagnostic, and the per-parameter constraint reports the offending type against the
|
|
62
|
+
/// protocol for its direction.
|
|
50
63
|
///
|
|
51
|
-
/// `
|
|
52
|
-
///
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/// them, returns `nil` so the caller emits no peer. `isStatic` mirrors a `static`/`class` member so the
|
|
56
|
-
/// peer sits in the right metatype context.
|
|
57
|
-
internal func directionalConformanceAssertion(
|
|
58
|
-
name: String,
|
|
59
|
-
decodableTypes: [String],
|
|
60
|
-
encodableType: String?,
|
|
61
|
-
callbackArgumentTypes: [String] = [],
|
|
62
|
-
isStatic: Bool
|
|
63
|
-
) -> DeclSyntax? {
|
|
64
|
-
let decodables = distinctAssertableTypes(decodableTypes, constraint: javaScriptDecodableProtocolName)
|
|
65
|
-
let callbackArguments = distinctAssertableTypes(callbackArgumentTypes, constraint: javaScriptEncodableProtocolName)
|
|
66
|
-
let encodable = encodableType.flatMap { assertableBoundaryType($0, constraint: javaScriptEncodableProtocolName) }
|
|
67
|
-
guard !decodables.isEmpty || !callbackArguments.isEmpty || encodable != nil else {
|
|
64
|
+
/// Returns `nil` when every type is known to conform, so the caller emits no peer. `isStatic` mirrors a
|
|
65
|
+
/// `static`/`class` member so the peer sits in the right metatype context.
|
|
66
|
+
internal func directionalConformanceAssertion(name: String, types: [AssertedType], isStatic: Bool) -> DeclSyntax? {
|
|
67
|
+
guard let assertion = memberConformanceAssertion(name: name, types: types) else {
|
|
68
68
|
return nil
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
// Arguments are constrained `JavaScriptDecodable`, and closure-argument parameters and the
|
|
72
|
-
// return/getter value `JavaScriptEncodable`. Every parameter suppresses `Copyable`, so a
|
|
73
|
-
// non-copyable boundary type such as `JavaScriptValue` can fill it.
|
|
74
|
-
let encodables = callbackArguments + [encodable].compactMap { $0 }
|
|
75
|
-
let decodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptDecodableProtocolName) & ~Copyable")
|
|
76
|
-
let encodableParameter = opaqueMetatypeParameter(constraint: "\(javaScriptEncodableProtocolName) & ~Copyable")
|
|
77
|
-
let parameters =
|
|
78
|
-
Array(repeating: decodableParameter, count: decodables.count)
|
|
79
|
-
+ Array(repeating: encodableParameter, count: encodables.count)
|
|
80
|
-
let arguments = (decodables + encodables).map { "\($0).self" }
|
|
81
|
-
|
|
82
|
-
let helper = "func \(name)(\(parameters.joined(separator: ", "))) {}"
|
|
83
|
-
let call = "\(name)(\(arguments.joined(separator: ", ")))"
|
|
84
70
|
let staticKeyword = isStatic ? "static " : ""
|
|
85
71
|
let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
|
|
86
72
|
return """
|
|
87
73
|
private \(raw: staticKeyword)func \(raw: peerName)() {
|
|
88
|
-
\(raw: helper)
|
|
89
|
-
\(raw: call)
|
|
74
|
+
\(raw: assertion.helper)
|
|
75
|
+
\(raw: assertion.call)
|
|
90
76
|
}
|
|
91
77
|
"""
|
|
92
78
|
}
|
|
93
79
|
|
|
80
|
+
/// The local helper of a member's conformance assertion and its call, or `nil` when every type is known
|
|
81
|
+
/// to conform. The helper is named after the member and takes the labels of the types, so the
|
|
82
|
+
/// diagnostic reads like the member's signature, such as `local function 'move(_:by:returning:)'
|
|
83
|
+
/// requires that 'Point' conform to 'JavaScriptEncodable'`. A type that holds a tuple contributes the
|
|
84
|
+
/// types in it (see `distinctAssertableTypes`), under its label. Every parameter suppresses `Copyable`,
|
|
85
|
+
/// so a non-copyable boundary type such as `JavaScriptValue` can fill it.
|
|
86
|
+
internal func memberConformanceAssertion(name: String, types: [AssertedType]) -> MemberConformanceAssertion? {
|
|
87
|
+
var seen: Set<String> = []
|
|
88
|
+
var parameters: [AssertedType] = []
|
|
89
|
+
for asserted in types {
|
|
90
|
+
for type in distinctAssertableTypes([asserted.type], constraint: asserted.constraint) {
|
|
91
|
+
let key = "\(asserted.label ?? "_") \(type) \(asserted.constraint)"
|
|
92
|
+
if seen.insert(key).inserted {
|
|
93
|
+
parameters.append(AssertedType(label: asserted.label, type: type, constraint: asserted.constraint))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
guard !parameters.isEmpty else {
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
let declared = parameters.map { parameter in
|
|
101
|
+
let metatype = "(some \(parameter.constraint) & ~Copyable).Type"
|
|
102
|
+
return parameter.label.map { "\($0) _: \(metatype)" } ?? "_: \(metatype)"
|
|
103
|
+
}
|
|
104
|
+
let passed = parameters.map { parameter in
|
|
105
|
+
return parameter.label.map { "\($0): \(parameter.type).self" } ?? "\(parameter.type).self"
|
|
106
|
+
}
|
|
107
|
+
let labels = parameters.map { "\($0.label ?? "_"):" }.joined()
|
|
108
|
+
return MemberConformanceAssertion(
|
|
109
|
+
signature: "\(name)(\(labels))",
|
|
110
|
+
helper: "func \(name)(\(declared.joined(separator: ", "))) {}",
|
|
111
|
+
call: "\(name)(\(passed.joined(separator: ", ")))"
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
/// One conformance-assertion peer covering several members' assertions at once — used by `@Record`,
|
|
95
116
|
/// which folds every property into a single `_assertTypesConformance()` rather than emitting a peer
|
|
96
117
|
/// per property. Each assertion keeps its own named nested helper, so the per-member naming in the
|
|
@@ -182,11 +203,16 @@ internal func opaqueMetatypeParameter(constraint: String) -> String {
|
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
/// The distinct types to assert against `constraint` from a list, mapping each through
|
|
185
|
-
/// `assertableBoundaryType` (which drops what needs no assertion) and deduping in first-seen order.
|
|
186
|
-
|
|
206
|
+
/// `assertableBoundaryType` (which drops what needs no assertion) and deduping in first-seen order. A
|
|
207
|
+
/// type that holds a tuple converts element by element (see `TupleConversion`), so the types in it that
|
|
208
|
+
/// convert through their own `decode` / `encode` are asserted in its place.
|
|
209
|
+
internal func distinctAssertableTypes(_ types: [String], constraint: String) -> [String] {
|
|
187
210
|
var seen: Set<String> = []
|
|
188
211
|
var distinct: [String] = []
|
|
189
|
-
let assertable = types.
|
|
212
|
+
let assertable = types.flatMap { type in
|
|
213
|
+
let elementTypes = tupleConversion(of: type)?.conformingTypes ?? [type]
|
|
214
|
+
return elementTypes.compactMap { assertableBoundaryType($0, constraint: constraint) }
|
|
215
|
+
}
|
|
190
216
|
for type in assertable where seen.insert(type).inserted {
|
|
191
217
|
distinct.append(type)
|
|
192
218
|
}
|
|
@@ -207,3 +233,40 @@ private func unwrappedOptional(_ type: String) -> String {
|
|
|
207
233
|
}
|
|
208
234
|
return String(result)
|
|
209
235
|
}
|
|
236
|
+
|
|
237
|
+
/// The conformance assertion of one member: a local helper function named after the member, which takes
|
|
238
|
+
/// the metatype of each asserted type, and its call.
|
|
239
|
+
internal struct MemberConformanceAssertion {
|
|
240
|
+
/// The helper's full name with its argument labels, such as `load(_:)`.
|
|
241
|
+
let signature: String
|
|
242
|
+
/// The helper's declaration.
|
|
243
|
+
let helper: String
|
|
244
|
+
/// The call of the helper with each asserted type's metatype.
|
|
245
|
+
let call: String
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// One assertion member covering the `@JS` members of an `@ExpoModule` or `@SharedObject` class, which
|
|
249
|
+
/// the class macro emits in place of the `@JS` peers: each member's helper and its call, in member order.
|
|
250
|
+
/// The helpers are local functions, so two helpers with the same name and labels (members that differ
|
|
251
|
+
/// only by argument types) would redeclare the same function: such a helper sits in its own `do` block.
|
|
252
|
+
/// Returns `nil` when no member has a type to assert.
|
|
253
|
+
internal func classConformanceAssertions(_ assertions: [MemberConformanceAssertion]) -> DeclSyntax? {
|
|
254
|
+
guard !assertions.isEmpty else {
|
|
255
|
+
return nil
|
|
256
|
+
}
|
|
257
|
+
var signatureCounts: [String: Int] = [:]
|
|
258
|
+
for assertion in assertions {
|
|
259
|
+
signatureCounts[assertion.signature, default: 0] += 1
|
|
260
|
+
}
|
|
261
|
+
let lines = assertions.flatMap { assertion in
|
|
262
|
+
guard signatureCounts[assertion.signature, default: 0] > 1 else {
|
|
263
|
+
return [assertion.helper, assertion.call]
|
|
264
|
+
}
|
|
265
|
+
return ["do {", " \(assertion.helper)", " \(assertion.call)", "}"]
|
|
266
|
+
}
|
|
267
|
+
return """
|
|
268
|
+
private func _assertTypesConformance() {
|
|
269
|
+
\(raw: lines.joined(separator: "\n"))
|
|
270
|
+
}
|
|
271
|
+
"""
|
|
272
|
+
}
|
|
@@ -270,9 +270,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
270
270
|
private func decodeMethod(union: UnionType) -> DeclSyntax {
|
|
271
271
|
var lines: [String] = [" let kind = JavaScriptValueKinds(of: value)"]
|
|
272
272
|
for unionCase in union.cases {
|
|
273
|
-
let
|
|
273
|
+
let conversion = payloadConversion(unionCase)
|
|
274
274
|
lines.append(
|
|
275
|
-
" if \(
|
|
275
|
+
" if \(conversion.decodableKindsExpression).contains(kind), let payload = try? \(conversion.decodeExpression(of: "value")) {"
|
|
276
276
|
)
|
|
277
277
|
lines.append(" return \(unionCase.construction)")
|
|
278
278
|
lines.append(" }")
|
|
@@ -293,7 +293,7 @@ private func decodeMethod(union: UnionType) -> DeclSyntax {
|
|
|
293
293
|
/// `decodableKinds`: the union of the payloads' kinds, so a union nested in another union, or wrapped
|
|
294
294
|
/// in an optional, is skipped as precisely as its payloads are.
|
|
295
295
|
private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
|
|
296
|
-
let kinds = union.cases.map {
|
|
296
|
+
let kinds = union.cases.map { payloadConversion($0).decodableKindsExpression }
|
|
297
297
|
// An option set's array literal is the union of its elements, which reads like the case list. The
|
|
298
298
|
// optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
|
|
299
299
|
let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
|
|
@@ -311,7 +311,7 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
|
|
|
311
311
|
var lines: [String] = [" switch value {"]
|
|
312
312
|
for unionCase in union.cases {
|
|
313
313
|
lines.append(" case \(unionCase.pattern):")
|
|
314
|
-
lines.append(" return try \(
|
|
314
|
+
lines.append(" return try \(payloadConversion(unionCase).encodeExpression(of: "payload"))")
|
|
315
315
|
}
|
|
316
316
|
lines.append(" }")
|
|
317
317
|
let body = lines.joined(separator: "\n")
|
|
@@ -331,7 +331,12 @@ private func encodeIntoSlotMethod(union: UnionType) -> DeclSyntax {
|
|
|
331
331
|
var lines: [String] = [" switch value {"]
|
|
332
332
|
for unionCase in union.cases {
|
|
333
333
|
lines.append(" case \(unionCase.pattern):")
|
|
334
|
-
|
|
334
|
+
if let tuple = tupleConversion(of: unionCase.payloadType) {
|
|
335
|
+
// A payload that holds a tuple has no `encode(_:into:in:)`, so its encoded value is moved in.
|
|
336
|
+
lines.append(" slot.emplace(try \(tuple.encodeExpression(of: "payload")))")
|
|
337
|
+
} else {
|
|
338
|
+
lines.append(" try \(expressionType(unionCase.payloadType)).encode(payload, into: slot, in: runtime)")
|
|
339
|
+
}
|
|
335
340
|
}
|
|
336
341
|
lines.append(" }")
|
|
337
342
|
let body = lines.joined(separator: "\n")
|
|
@@ -439,3 +444,9 @@ private struct UnionFixItMessage: FixItMessage {
|
|
|
439
444
|
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
440
445
|
}
|
|
441
446
|
}
|
|
447
|
+
|
|
448
|
+
/// How a case's payload converts: through the tuple methods when it holds a tuple (see
|
|
449
|
+
/// `TupleConversion`), and through its own `decode` / `encode` otherwise.
|
|
450
|
+
private func payloadConversion(_ unionCase: UnionCase) -> TupleConversion {
|
|
451
|
+
return tupleConversion(of: unionCase.payloadType) ?? .conforming(type: unionCase.payloadType)
|
|
452
|
+
}
|
|
@@ -463,8 +463,9 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
463
463
|
/// qualified names and gives a resolved ref the name of its declaration; version 8 reports
|
|
464
464
|
/// declarations in `#if` blocks, adds `platforms` (with deprecations), `accessLevel`, and
|
|
465
465
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
466
|
-
/// `sharedRef` type kind and the `sharedRef` of a shared object
|
|
467
|
-
|
|
466
|
+
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
467
|
+
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes.
|
|
468
|
+
let scanExportsSchemaVersion = 10
|
|
468
469
|
|
|
469
470
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
470
471
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -136,6 +136,15 @@ extension TypeNode {
|
|
|
136
136
|
)
|
|
137
137
|
case .promise(let value):
|
|
138
138
|
return .promise(value: value.resolvingRefs(using: index, in: scope))
|
|
139
|
+
case .tuple(let elements, let name):
|
|
140
|
+
return .tuple(elements: elements.map { $0.resolvingRefs(using: index, in: scope) }, name: name)
|
|
141
|
+
case .object(let properties, let name):
|
|
142
|
+
return .object(
|
|
143
|
+
properties: properties.map {
|
|
144
|
+
TypeNodeProperty(name: $0.name, type: $0.type.resolvingRefs(using: index, in: scope))
|
|
145
|
+
},
|
|
146
|
+
name: name
|
|
147
|
+
)
|
|
139
148
|
case .function(let parameters, let returns, let isAsync, let isThrowing):
|
|
140
149
|
return .function(
|
|
141
150
|
parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
|
|
@@ -142,13 +142,14 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
142
142
|
guard scope.isEmpty else {
|
|
143
143
|
return
|
|
144
144
|
}
|
|
145
|
+
let aliases = TupleTypeAliases(in: members, scope: qualified)
|
|
145
146
|
let (functions, properties, events, _) = collectJSMembers(members)
|
|
146
147
|
modules.append(
|
|
147
148
|
ExportedModule(
|
|
148
149
|
name: qualified,
|
|
149
150
|
jsName: stringArgument(of: attribute) ?? name,
|
|
150
|
-
functions: functions,
|
|
151
|
-
properties: properties,
|
|
151
|
+
functions: functions.map(aliases.resolving),
|
|
152
|
+
properties: properties.map(aliases.resolving),
|
|
152
153
|
events: events,
|
|
153
154
|
// `jsName` is filled in by `ExportedSurface.resolvingRefs()`, since the `@SharedObject` can
|
|
154
155
|
// be declared in another file.
|
|
@@ -164,7 +165,13 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
if let attribute = attributes.firstAttribute(named: DetectedMacro.sharedObject.rawValue) {
|
|
167
|
-
let (
|
|
168
|
+
let aliases = TupleTypeAliases(in: members, scope: qualified)
|
|
169
|
+
var (functions, properties, events, initializer) = collectJSMembers(members)
|
|
170
|
+
functions = functions.map(aliases.resolving)
|
|
171
|
+
properties = properties.map(aliases.resolving)
|
|
172
|
+
if let parameters = initializer?.parameters {
|
|
173
|
+
initializer?.parameters = parameters.map(aliases.resolving)
|
|
174
|
+
}
|
|
168
175
|
sharedObjects.append(
|
|
169
176
|
ExportedSharedObject(
|
|
170
177
|
name: qualified,
|