expo-modules-macros 0.20.0 → 0.22.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/.github/workflows/linux.yml +132 -0
- package/.github/workflows/publish.yml +160 -11
- package/.github/workflows/swift-format.yml +76 -0
- package/.github/workflows/windows.yml +3 -28
- package/AGENTS.md +19 -9
- package/README.md +27 -8
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +3 -3
- package/apple/Package.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +117 -25
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +55 -45
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +180 -44
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +84 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +165 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +358 -234
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +48 -32
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
- package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +15 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +127 -45
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/index.d.ts +4 -3
- package/build/index.js +33 -18
- package/build/types.d.ts +7 -1
- package/package.json +7 -3
|
@@ -3,51 +3,59 @@ import SwiftSyntax
|
|
|
3
3
|
import SwiftSyntaxBuilder
|
|
4
4
|
import SwiftSyntaxMacros
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- `from(dictionary:appContext:)` —
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
The type is auto-conformed to `Record` (a core protocol whose requirements are
|
|
18
|
-
|
|
19
|
-
shape — no conformance to
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
6
|
+
/// Member macro applied to a record type. Treats **every stored property** that is not
|
|
7
|
+
/// `static`, `private`, `fileprivate`, `lazy` or computed as a property — no `@Field` wrapper
|
|
8
|
+
/// needed — and synthesizes the conversion surface from the property's static type:
|
|
9
|
+
///
|
|
10
|
+
/// - an explicit memberwise `init` (so the static factories have a single construction point),
|
|
11
|
+
/// - `decode(_:in:)` and `encode(_:in:)` — the fast paths, reading each property straight off a JS
|
|
12
|
+
/// object and writing it straight into one,
|
|
13
|
+
/// - `from(object:appContext:)` and `toObject(appContext:)`, which forward to `decode` and `encode`,
|
|
14
|
+
/// - `from(dictionary:appContext:)` and `toDictionary(appContext:)` — converting each property from and
|
|
15
|
+
/// to a `[String: Any]` dictionary.
|
|
16
|
+
///
|
|
17
|
+
/// The type is auto-conformed to `Record` (a core protocol whose requirements are this surface); the
|
|
18
|
+
/// synthesized methods override `Record`'s reflection-based defaults, and the synthesized `decode` and
|
|
19
|
+
/// `encode` replace its default `JavaScriptCodable` witnesses. Author-facing shape — no conformance to
|
|
20
|
+
/// spell out:
|
|
21
|
+
///
|
|
22
|
+
/// @Record
|
|
23
|
+
/// struct Options {
|
|
24
|
+
/// var name: String // required (non-optional, no default)
|
|
25
|
+
/// var count: Int = 0 // optional (has default)
|
|
26
|
+
/// var note: String? // nullable + optional
|
|
27
|
+
/// }
|
|
28
|
+
///
|
|
29
|
+
/// The JS key is always the property name. Requiredness is inferred from the declaration:
|
|
30
|
+
/// a default value makes a property optional (the default applies when the source omits it),
|
|
31
|
+
/// an optional type makes it nullable and optional, and a non-optional property without a
|
|
32
|
+
/// default is required (the factories throw when the source omits it).
|
|
33
|
+
///
|
|
34
|
+
/// `decode(_:in:)` and `encode(_:in:)` name each property with a `JavaScriptPropNameID` that each runtime
|
|
35
|
+
/// creates once. The names are held by the nested `_PropNames` class, which each runtime caches under one
|
|
36
|
+
/// key. A record that is generic or nested in a generic type uses string names, since a generic type can't
|
|
37
|
+
/// have static stored properties. The macro can't tell that an extension extends a generic type, so a
|
|
38
|
+
/// record declared in one doesn't compile.
|
|
39
|
+
///
|
|
40
|
+
/// The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
41
|
+
/// (`decode(_:in:)` / `encode(_:in:)`); the native-`Any` dictionary paths still go through the
|
|
42
|
+
/// public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
|
|
43
|
+
/// (`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
|
|
44
|
+
/// values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
45
|
+
/// user modules without any internal core symbols. Every property type must therefore conform to both
|
|
46
|
+
/// `AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`.
|
|
47
|
+
///
|
|
48
|
+
/// A property whose type mentions `Any` (`Any?`, `[String: Any]?`, `[String: [Any]]`, …) can't conform to
|
|
49
|
+
/// the JS codable protocols. On the JS-value paths it converts through the `JavaScriptValue` free-form
|
|
50
|
+
/// methods instead: `decodeAny`/`decodeAnyArray`/`decodeAnyDictionary` and the matching `encodeAny…`
|
|
51
|
+
/// for `Any`, `[Any]` and `[String: Any]` (optional or not), and the generic `decodeAny(_:as:in:)` /
|
|
52
|
+
/// `encodeAny(_:in:)` for any other type. Such a property only needs `AnyArgument` for the dictionary
|
|
53
|
+
/// paths, and a bare `Any` needs nothing, since the dictionary paths pass it through unchanged.
|
|
54
|
+
///
|
|
55
|
+
/// A record class must be `final` and have no inheritance clause. The synthesized factories construct
|
|
56
|
+
/// `Self` through the memberwise init, which only a final class can do without a `required` init, and
|
|
57
|
+
/// the macro can't tell a superclass from a protocol in the clause, so protocol conformances go in an
|
|
58
|
+
/// extension.
|
|
51
59
|
public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
52
60
|
public static func expansion(
|
|
53
61
|
of node: AttributeSyntax,
|
|
@@ -55,13 +63,21 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
55
63
|
conformingTo protocols: [TypeSyntax],
|
|
56
64
|
in context: some MacroExpansionContext
|
|
57
65
|
) throws -> [DeclSyntax] {
|
|
58
|
-
let
|
|
59
|
-
|
|
66
|
+
let typeName: String
|
|
67
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
68
|
+
typeName = structDecl.name.text
|
|
69
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
70
|
+
typeName = classDecl.name.text
|
|
71
|
+
} else {
|
|
60
72
|
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
61
73
|
}
|
|
74
|
+
let classDiagnostics = unsupportedClassDiagnostics(of: declaration)
|
|
75
|
+
guard classDiagnostics.isEmpty else {
|
|
76
|
+
throw DiagnosticsError(diagnostics: classDiagnostics)
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
let properties = try recordProperties(of: declaration)
|
|
64
|
-
let
|
|
80
|
+
let cachesPropNames = !properties.isEmpty && !isInGenericContext(declaration, context: context)
|
|
65
81
|
|
|
66
82
|
// The user may hand-write any of the initializers the macro would otherwise synthesize. Emitting
|
|
67
83
|
// a duplicate is a hard "invalid redeclaration" error, so skip anything already declared and let
|
|
@@ -92,29 +108,37 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
if !existingInitLabels.contains([]) {
|
|
95
|
-
if let defaultInit = defaultInit(properties: properties
|
|
111
|
+
if let defaultInit = defaultInit(properties: properties) {
|
|
96
112
|
members.append(defaultInit)
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
100
116
|
members.append(memberwiseInit(properties: properties))
|
|
101
117
|
}
|
|
102
|
-
members.append(fromJSObjectFactory(
|
|
118
|
+
members.append(fromJSObjectFactory())
|
|
103
119
|
members.append(fromDictionaryFactory(properties: properties))
|
|
104
|
-
members.append(toDictionaryMethod(properties: properties
|
|
105
|
-
members.append(toObjectMethod(
|
|
120
|
+
members.append(toDictionaryMethod(properties: properties))
|
|
121
|
+
members.append(toObjectMethod())
|
|
122
|
+
if cachesPropNames {
|
|
123
|
+
members.append(propNamesClass(properties: properties))
|
|
124
|
+
}
|
|
125
|
+
members.append(
|
|
126
|
+
contentsOf: javaScriptCodableWitnesses(
|
|
127
|
+
properties: properties,
|
|
128
|
+
typeName: typeName,
|
|
129
|
+
cachesPropNames: cachesPropNames
|
|
130
|
+
)
|
|
131
|
+
)
|
|
106
132
|
return members
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(which inherit it from a `@Record`-annotated parent) no extension is emitted at all.
|
|
117
|
-
*/
|
|
135
|
+
/// Auto-conforms the type to `Record` — the protocol whose requirements are exactly the members
|
|
136
|
+
/// synthesized above (`init()`, the two `from(_:)` factories, and the `toDictionary`/`toObject`
|
|
137
|
+
/// write side), which the synthesized methods satisfy by overriding `Record`'s reflection-based
|
|
138
|
+
/// defaults.
|
|
139
|
+
///
|
|
140
|
+
/// The conformance is skipped when the type already declares it, and for a class the member role
|
|
141
|
+
/// rejects, which would otherwise get conformance errors for the members it didn't synthesize.
|
|
118
142
|
public static func expansion(
|
|
119
143
|
of node: AttributeSyntax,
|
|
120
144
|
attachedTo declaration: some DeclGroupSyntax,
|
|
@@ -122,10 +146,10 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
122
146
|
conformingTo protocols: [TypeSyntax],
|
|
123
147
|
in context: some MacroExpansionContext
|
|
124
148
|
) throws -> [ExtensionDeclSyntax] {
|
|
125
|
-
|
|
149
|
+
guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
|
|
126
150
|
return []
|
|
127
151
|
}
|
|
128
|
-
guard
|
|
152
|
+
guard unsupportedClassDiagnostics(of: declaration).isEmpty else {
|
|
129
153
|
return []
|
|
130
154
|
}
|
|
131
155
|
if inheritsProtocol(named: "Record", in: declaration) {
|
|
@@ -144,10 +168,8 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
144
168
|
|
|
145
169
|
// MARK: - Property model
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
conversions need: its property name, its written type, and how the source may omit it.
|
|
150
|
-
*/
|
|
171
|
+
/// A single record property discovered on the type, paired with everything the synthesized
|
|
172
|
+
/// conversions need: its property name, its written type, and how the source may omit it.
|
|
151
173
|
private struct RecordProperty {
|
|
152
174
|
/// The property name as written, backticks included for an escaped name (`` `default` ``), since
|
|
153
175
|
/// every identifier position in the generated code needs them.
|
|
@@ -182,11 +204,9 @@ private struct RecordProperty {
|
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
annotation, since the synthesized conversions name the type (e.g. `Type.decode(…)`).
|
|
189
|
-
*/
|
|
207
|
+
/// Discovers the record's properties: every stored `var`/`let` binding that is not `static`,
|
|
208
|
+
/// `private`, `fileprivate`, `lazy`, or computed. Each property must declare an explicit type
|
|
209
|
+
/// annotation, since the synthesized conversions name the type (e.g. `Type.decode(…)`).
|
|
190
210
|
private func recordProperties(
|
|
191
211
|
of declaration: some DeclGroupSyntax
|
|
192
212
|
) throws -> [RecordProperty] {
|
|
@@ -220,7 +240,8 @@ private func recordProperties(
|
|
|
220
240
|
// (Swift's own default-literal type) — covers the common `var name = "foo"` case. Anything
|
|
221
241
|
// whose type a syntactic macro can't determine (calls, collections, member access) still needs
|
|
222
242
|
// an annotation.
|
|
223
|
-
let inferredType =
|
|
243
|
+
let inferredType =
|
|
244
|
+
binding.typeAnnotation?.type.trimmedDescription
|
|
224
245
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
225
246
|
guard let type = inferredType else {
|
|
226
247
|
throw MacroExpansionErrorMessage(
|
|
@@ -239,7 +260,8 @@ private func recordProperties(
|
|
|
239
260
|
message: RecordDiagnosticMessage(
|
|
240
261
|
"@Record property '\(ident.identifier.text)' can't hold a closure: a record property is also encoded to JavaScript, and a native closure can't be encoded",
|
|
241
262
|
id: "record-closure-property"
|
|
242
|
-
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
243
265
|
])
|
|
244
266
|
}
|
|
245
267
|
let freeForm = try annotatedType.flatMap { annotatedType -> FreeFormConversion? in
|
|
@@ -269,23 +291,19 @@ private func recordProperties(
|
|
|
269
291
|
|
|
270
292
|
// MARK: - Synthesized members
|
|
271
293
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
When
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
unsupported today.
|
|
286
|
-
*/
|
|
287
|
-
private func defaultInit(properties: [RecordProperty], isClass: Bool) -> DeclSyntax? {
|
|
288
|
-
if isClass || properties.isEmpty {
|
|
294
|
+
/// The parameterless `init()` required by the `Record` protocol. Nothing in the synthesized code uses
|
|
295
|
+
/// it — the factories construct through the memberwise init with defaults inlined — so it's purely a
|
|
296
|
+
/// conformance witness. Swift only synthesizes an implicit `init()` for a type that declares no other
|
|
297
|
+
/// initializer, but the macro always emits a memberwise `init(…)`, which suppresses it, so the macro
|
|
298
|
+
/// must supply `init()` itself.
|
|
299
|
+
///
|
|
300
|
+
/// When no property is required, the body is empty: every stored property initializes from its own
|
|
301
|
+
/// declaration. When a property *is* required there's no value to give it, so the body traps — this `init()`
|
|
302
|
+
/// is never reached (the `from(…)` factories don't call it), it exists only to satisfy the requirement.
|
|
303
|
+
///
|
|
304
|
+
/// Not emitted when there are no properties at all — there the memberwise init already *is* `init()`.
|
|
305
|
+
private func defaultInit(properties: [RecordProperty]) -> DeclSyntax? {
|
|
306
|
+
if properties.isEmpty {
|
|
289
307
|
return nil
|
|
290
308
|
}
|
|
291
309
|
if properties.contains(where: { $0.isRequired }) {
|
|
@@ -325,36 +343,24 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
325
343
|
"""
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
|
|
329
|
-
`
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`JavaScriptDecodable.decode`, so the factory binds the `runtime` from the app context once and
|
|
334
|
-
threads it to every read. A free-form property decodes through the `JavaScriptValue` free-form
|
|
335
|
-
methods, which take the runtime too.
|
|
336
|
-
*/
|
|
337
|
-
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
338
|
-
var lines: [String] = []
|
|
339
|
-
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
340
|
-
// otherwise leave it unused.
|
|
341
|
-
if !properties.isEmpty {
|
|
342
|
-
lines.append(" let runtime = try appContext.runtime")
|
|
343
|
-
}
|
|
344
|
-
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
345
|
-
let body = lines.joined(separator: "\n")
|
|
346
|
+
/// `from(object:appContext:)`, which forwards to the record's own unowned `decode(_:in:)` witness (see
|
|
347
|
+
/// `javaScriptCodableWitnesses`), where the property reads are.
|
|
348
|
+
private func fromJSObjectFactory() -> DeclSyntax {
|
|
349
|
+
// The record declares only the unowned `decode`, so the call lends the value unowned. With an owning
|
|
350
|
+
// value it would reach an owning `decode` default, and `Record`'s converts through this factory.
|
|
346
351
|
return """
|
|
347
352
|
@JavaScriptActor
|
|
348
353
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
349
|
-
|
|
354
|
+
let runtime = try appContext.runtime
|
|
355
|
+
return try object.asValue().withUnownedValue(in: runtime) { unownedValue in
|
|
356
|
+
return try decode(unownedValue, in: runtime)
|
|
357
|
+
}
|
|
350
358
|
}
|
|
351
359
|
"""
|
|
352
360
|
}
|
|
353
361
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
uses the `Any?` cast overload; a missing key is `dictionary[key] == nil`.
|
|
357
|
-
*/
|
|
362
|
+
/// `from(dictionary:appContext:)` — same as the JS path but reads from `[String: Any]` and
|
|
363
|
+
/// uses the `Any?` cast overload; a missing key is `dictionary[key] == nil`.
|
|
358
364
|
private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
359
365
|
let body = factoryBody(properties: properties, readLines: dictionaryReadLines(properties: properties))
|
|
360
366
|
return """
|
|
@@ -364,172 +370,239 @@ private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
364
370
|
"""
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
a throwaway `Self()` nor the record's `init()`.
|
|
372
|
-
*/
|
|
373
|
+
/// Builds a factory body: per-property reads into locals, then a single call to the synthesized
|
|
374
|
+
/// memberwise init. Each property's declared default expression is captured at parse time and inlined
|
|
375
|
+
/// directly into the omitted-property branch (see the read-line builders), so the factory needs neither
|
|
376
|
+
/// a throwaway `Self()` nor the record's `init()`.
|
|
373
377
|
private func factoryBody(properties: [RecordProperty], readLines: [String]) -> String {
|
|
374
378
|
var lines: [String] = []
|
|
375
379
|
lines.append(contentsOf: readLines)
|
|
376
|
-
let initArgs = properties.map { "\($
|
|
380
|
+
let initArgs = properties.enumerated().map { "\($1.name): \(fieldLocal($0))" }.joined(separator: ", ")
|
|
377
381
|
lines.append(" return Self(\(initArgs))")
|
|
378
382
|
return lines.joined(separator: "\n")
|
|
379
383
|
}
|
|
380
384
|
|
|
381
|
-
///
|
|
382
|
-
/// the
|
|
383
|
-
///
|
|
384
|
-
|
|
385
|
-
|
|
385
|
+
/// The local that holds the value of the property at `index` in a factory. It's numbered rather than
|
|
386
|
+
/// named after the property, so no property can collide with it or with the factory's other parameters
|
|
387
|
+
/// and locals (`object`, `runtime`, `dictionary`, `appContext`).
|
|
388
|
+
private func fieldLocal(_ index: Int) -> String {
|
|
389
|
+
return "field\(index)"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Per-property read statements, each producing a `let field<index>` from the property of the JS object
|
|
393
|
+
/// named `object`. The property is lent to a closure as a `JavaScriptUnownedValue` and decoded there the
|
|
394
|
+
/// same way as a `@JS` argument (see `decodeCall`), so the read doesn't allocate a `JavaScriptValue`.
|
|
395
|
+
/// Only the generic free-form decode copies the value into one.
|
|
396
|
+
private func jsObjectReadLines(properties: [RecordProperty], cachesPropNames: Bool) -> [String] {
|
|
386
397
|
var lines: [String] = []
|
|
387
|
-
for property in properties {
|
|
398
|
+
for (index, property) in properties.enumerated() {
|
|
388
399
|
let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
|
|
389
|
-
let
|
|
390
|
-
let
|
|
391
|
-
|
|
392
|
-
case .shape(let decodeMethod, _, let isOptional, _):
|
|
393
|
-
let call = "try JavaScriptValue.\(decodeMethod)(\(valueVar), in: runtime)"
|
|
394
|
-
decode = isOptional ? "\(valueVar).isUndefined() || \(valueVar).isNull() ? nil : \(call)" : call
|
|
395
|
-
case .generic:
|
|
396
|
-
decode = "try JavaScriptValue.decodeAny(\(valueVar), as: \(exprType).self, in: runtime)"
|
|
397
|
-
case nil:
|
|
398
|
-
decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
399
|
-
}
|
|
400
|
-
lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
|
|
400
|
+
let decode = "try \(decodeCall(property.type, freeForm: property.freeForm, from: valueVar))"
|
|
401
|
+
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
402
|
+
lines.append(" let \(fieldLocal(index)) = try object.withUnownedProperty(\(propName)) { \(valueVar) in")
|
|
401
403
|
if property.isRequired {
|
|
402
|
-
lines.append("
|
|
403
|
-
lines.append("
|
|
404
|
-
lines.append("
|
|
405
|
-
lines.append("
|
|
404
|
+
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
405
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
406
|
+
lines.append(" }")
|
|
407
|
+
lines.append(" return \(decode)")
|
|
406
408
|
} else if property.isOptional {
|
|
407
|
-
// `Optional.decode` (and the free-form
|
|
408
|
-
// the read is a plain decode.
|
|
409
|
-
lines.append("
|
|
409
|
+
// `Optional.decode` (and the free-form decodes of optional shapes) already map `undefined` and
|
|
410
|
+
// `null` to `nil`, so the read is a plain decode.
|
|
411
|
+
lines.append(" return \(decode)")
|
|
410
412
|
} else {
|
|
411
|
-
lines.append("
|
|
413
|
+
lines.append(" return \(valueVar).isUndefined() ? \(property.defaultValue!) : \(decode)")
|
|
412
414
|
}
|
|
415
|
+
lines.append(" }")
|
|
413
416
|
}
|
|
414
417
|
return lines
|
|
415
418
|
}
|
|
416
419
|
|
|
417
|
-
/// Per-property read statements for the dictionary factory, each producing a `let <
|
|
418
|
-
/// property has no dynamic type to cast through, and
|
|
419
|
-
/// read unchanged.
|
|
420
|
+
/// Per-property read statements for the dictionary factory, each producing a `let field<index>` from the
|
|
421
|
+
/// raw dictionary value in `entry<index>`. A bare `Any` property has no dynamic type to cast through, and
|
|
422
|
+
/// the dictionary value already is an `Any`, so it's read unchanged.
|
|
420
423
|
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
421
424
|
var lines: [String] = []
|
|
422
|
-
for property in properties {
|
|
423
|
-
let
|
|
425
|
+
for (index, property) in properties.enumerated() {
|
|
426
|
+
let field = fieldLocal(index)
|
|
427
|
+
let entry = "entry\(index)"
|
|
424
428
|
let exprType = expressionType(property.type)
|
|
425
429
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
426
|
-
let cast =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
+
let cast =
|
|
431
|
+
isBareAny
|
|
432
|
+
? entry
|
|
433
|
+
: "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
434
|
+
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
430
435
|
if property.isRequired {
|
|
431
|
-
lines.append(" guard let \(
|
|
436
|
+
lines.append(" guard let \(entry) else {")
|
|
432
437
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
433
438
|
lines.append(" }")
|
|
434
|
-
lines.append(" let \(
|
|
439
|
+
lines.append(" let \(field) = \(cast)")
|
|
435
440
|
} else if property.isOptional {
|
|
436
|
-
lines.append(" let \(
|
|
441
|
+
lines.append(" let \(field): \(property.type) = (\(entry) == nil || \(entry)! is NSNull) ? nil : \(cast)")
|
|
437
442
|
} else {
|
|
438
|
-
lines.append(" let \(
|
|
443
|
+
lines.append(" let \(field) = \(entry) == nil ? \(property.defaultValue!) : \(isBareAny ? entry + "!" : cast)")
|
|
439
444
|
}
|
|
440
445
|
}
|
|
441
446
|
return lines
|
|
442
447
|
}
|
|
443
448
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
449
|
-
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
450
|
-
var lines: [String] = []
|
|
451
|
-
if inheritsRecord {
|
|
452
|
-
lines.append(" var dictionary = super.toDictionary(appContext: appContext)")
|
|
453
|
-
} else {
|
|
454
|
-
lines.append(" var dictionary: [String: Any] = [:]")
|
|
455
|
-
}
|
|
449
|
+
/// `toDictionary(appContext:)` — converts each property back to a JS-compatible value via the
|
|
450
|
+
/// dynamic type and assembles a `[String: Any]`.
|
|
451
|
+
private func toDictionaryMethod(properties: [RecordProperty]) -> DeclSyntax {
|
|
452
|
+
var lines = [" var dictionary: [String: Any] = [:]"]
|
|
456
453
|
for property in properties {
|
|
457
454
|
lines.append(" dictionary[\"\(property.key)\"] = self.\(property.name)")
|
|
458
455
|
}
|
|
459
456
|
lines.append(" return dictionary")
|
|
460
|
-
let body = lines.joined(separator: "\n")
|
|
461
|
-
|
|
462
|
-
if inheritsRecord {
|
|
463
|
-
return """
|
|
464
|
-
public \(raw: overrideKeyword)func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
465
|
-
\(raw: body)
|
|
466
|
-
}
|
|
467
|
-
"""
|
|
468
|
-
}
|
|
469
457
|
return """
|
|
470
458
|
public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
471
|
-
\(raw:
|
|
459
|
+
\(raw: lines.joined(separator: "\n"))
|
|
472
460
|
}
|
|
473
461
|
"""
|
|
474
462
|
}
|
|
475
463
|
|
|
476
|
-
|
|
477
|
-
`
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
464
|
+
/// `toObject(appContext:)`, which forwards to the record's own `encode(_:in:)` witness (see
|
|
465
|
+
/// `javaScriptCodableWitnesses`), where the property writes are.
|
|
466
|
+
private func toObjectMethod() -> DeclSyntax {
|
|
467
|
+
return """
|
|
468
|
+
@JavaScriptActor
|
|
469
|
+
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
470
|
+
return try Self.encode(self, in: appContext.runtime).asObject()
|
|
471
|
+
}
|
|
472
|
+
"""
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/// The record's own `JavaScriptDecodable` and `JavaScriptEncodable` witnesses, which convert in the
|
|
476
|
+
/// runtime they're given. Without them the record would convert through `Record`'s default witnesses,
|
|
477
|
+
/// which recover the app context from the runtime and convert in the app context's runtime.
|
|
478
|
+
///
|
|
479
|
+
/// `decode` reads each property off the JS object into a local, then constructs the record through the
|
|
480
|
+
/// memberwise init. Required properties throw when undefined; defaulted properties fall back to the
|
|
481
|
+
/// property's declared default (inlined) when undefined; optional properties become `nil` when
|
|
482
|
+
/// undefined/null. It's the unowned overload: an owning value decodes through `JavaScriptDecodable`'s
|
|
483
|
+
/// default, which forwards to it. `encode` builds the JS object, writing each property. Its parameter
|
|
484
|
+
/// names the type, because a class can't take `Self` as a parameter type.
|
|
485
|
+
private func javaScriptCodableWitnesses(
|
|
486
|
+
properties: [RecordProperty],
|
|
487
|
+
typeName: String,
|
|
488
|
+
cachesPropNames: Bool
|
|
489
|
+
) -> [DeclSyntax] {
|
|
490
|
+
// An empty record still decodes only from an object, but has no property to read off it.
|
|
491
|
+
let objectLine =
|
|
492
|
+
properties.isEmpty
|
|
493
|
+
? " _ = try value.asObject(in: runtime)"
|
|
494
|
+
: " let object = try value.asObject(in: runtime)"
|
|
495
|
+
let lookupLines = cachesPropNames ? propNamesLookupLines() : []
|
|
496
|
+
let readLines = lookupLines + jsObjectReadLines(properties: properties, cachesPropNames: cachesPropNames)
|
|
497
|
+
let decodeBody = objectLine + "\n" + factoryBody(properties: properties, readLines: readLines)
|
|
498
|
+
let encodeLines =
|
|
499
|
+
[" let object = runtime.createObject()"]
|
|
500
|
+
+ lookupLines
|
|
501
|
+
+ objectWriteLines(properties: properties, of: "value", cachesPropNames: cachesPropNames)
|
|
502
|
+
+ [" return object.asValue()"]
|
|
503
|
+
return [
|
|
504
|
+
"""
|
|
505
|
+
@JavaScriptActor
|
|
506
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self {
|
|
507
|
+
\(raw: decodeBody)
|
|
508
|
+
}
|
|
509
|
+
""",
|
|
510
|
+
"""
|
|
511
|
+
@JavaScriptActor
|
|
512
|
+
public static func encode(_ value: \(raw: typeName), in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
|
|
513
|
+
\(raw: encodeLines.joined(separator: "\n"))
|
|
514
|
+
}
|
|
515
|
+
""",
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// The statements that write each property of `record` (`self` or the `encode` parameter) into the
|
|
520
|
+
/// `object` local, encoding it with `JavaScriptEncodable.encode` or, for a free-form property, the
|
|
521
|
+
/// matching `JavaScriptValue` free-form method.
|
|
522
|
+
private func objectWriteLines(properties: [RecordProperty], of record: String, cachesPropNames: Bool) -> [String] {
|
|
523
|
+
return properties.enumerated().map { index, property in
|
|
524
|
+
let value = "\(record).\(property.name)"
|
|
499
525
|
let encode: String
|
|
500
526
|
switch property.freeForm {
|
|
501
527
|
case .shape(_, let encodeMethod, let isOptional, _):
|
|
502
|
-
encode =
|
|
503
|
-
|
|
504
|
-
:
|
|
528
|
+
encode =
|
|
529
|
+
isOptional
|
|
530
|
+
? "\(value) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(\(value)!, in: runtime)"
|
|
531
|
+
: "try JavaScriptValue.\(encodeMethod)(\(value), in: runtime)"
|
|
505
532
|
case .generic:
|
|
506
533
|
// The explicit `as Any` keeps an optional property from warning about its implicit coercion.
|
|
507
|
-
encode = "try JavaScriptValue.encodeAny(
|
|
534
|
+
encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
|
|
508
535
|
case nil:
|
|
509
|
-
encode = "try \(
|
|
536
|
+
encode = "try \(expressionType(property.type)).encode(\(value), in: runtime)"
|
|
510
537
|
}
|
|
511
|
-
|
|
538
|
+
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
539
|
+
return " object.setProperty(\(propName), value: \(encode))"
|
|
512
540
|
}
|
|
513
|
-
|
|
514
|
-
let body = lines.joined(separator: "\n")
|
|
541
|
+
}
|
|
515
542
|
|
|
543
|
+
/// The name of the nested class that holds the property names (see `propNamesClass`).
|
|
544
|
+
private let propNamesTypeName = "_PropNames"
|
|
545
|
+
|
|
546
|
+
/// A nested class that holds the `JavaScriptPropNameID` of each property, `_name<index>` for the property
|
|
547
|
+
/// at `index`, and the runtime cache key for its instances. Each runtime creates one instance, so the
|
|
548
|
+
/// whole record takes one cache key.
|
|
549
|
+
private func propNamesClass(properties: [RecordProperty]) -> DeclSyntax {
|
|
550
|
+
let storedProperties = properties.indices.map { index in
|
|
551
|
+
" let \(propNameMember(index)): JavaScriptPropNameID"
|
|
552
|
+
}
|
|
553
|
+
let assignments = properties.enumerated().map { index, property in
|
|
554
|
+
" self.\(propNameMember(index)) = JavaScriptPropNameID(runtime, string: \"\(property.key)\")"
|
|
555
|
+
}
|
|
516
556
|
return """
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
557
|
+
private final class \(raw: propNamesTypeName) {
|
|
558
|
+
static let _key = JavaScriptRuntime.Cache.Key<\(raw: propNamesTypeName)>()
|
|
559
|
+
|
|
560
|
+
\(raw: storedProperties.joined(separator: "\n"))
|
|
561
|
+
|
|
562
|
+
init(_ runtime: borrowing JavaScriptRuntime) {
|
|
563
|
+
\(raw: assignments.joined(separator: "\n"))
|
|
564
|
+
}
|
|
520
565
|
}
|
|
521
566
|
"""
|
|
522
567
|
}
|
|
523
568
|
|
|
569
|
+
private func propNameMember(_ index: Int) -> String {
|
|
570
|
+
return "_name\(index)"
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// The statements that get the record's property names from the runtime cache into `_propNames`, at the
|
|
574
|
+
/// start of `decode` and `encode`.
|
|
575
|
+
private func propNamesLookupLines() -> [String] {
|
|
576
|
+
return [
|
|
577
|
+
" let _propNames = runtime.cached(\(propNamesTypeName)._key) {",
|
|
578
|
+
" return \(propNamesTypeName)(runtime)",
|
|
579
|
+
" }",
|
|
580
|
+
]
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// The first argument of the `withUnownedProperty` and `setProperty` calls for the property at `index`:
|
|
584
|
+
/// its name from `_propNames`, or the key as a string literal when the record can't hold the cache key.
|
|
585
|
+
private func propNameArgument(for property: RecordProperty, at index: Int, cached: Bool) -> String {
|
|
586
|
+
return cached ? "_propNames.\(propNameMember(index))" : "\"\(property.key)\""
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/// True when the record or a type that encloses it is generic. Swift doesn't support static stored
|
|
590
|
+
/// properties in generic types, so the record can't hold the cache key.
|
|
591
|
+
private func isInGenericContext(_ declaration: some DeclGroupSyntax, context: some MacroExpansionContext) -> Bool {
|
|
592
|
+
// The lexical context of a member macro usually starts with the type it's attached to, but the
|
|
593
|
+
// declaration is checked on its own too.
|
|
594
|
+
return ([Syntax(declaration)] + context.lexicalContext).contains { node in
|
|
595
|
+
return node.asProtocol(WithGenericParametersSyntax.self)?.genericParameterClause != nil
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
524
599
|
// MARK: - Helpers
|
|
525
600
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
empty-string label, which simply won't match the macro's always-labeled signatures (a safe miss).
|
|
532
|
-
*/
|
|
601
|
+
/// The parameter-label list of every initializer the type already declares, used to avoid emitting a
|
|
602
|
+
/// duplicate of one the author hand-wrote. Each entry is the ordered external argument labels of one
|
|
603
|
+
/// `init` — e.g. `init(name: String, count: Int)` → `["name", "count"]`, and `init()` → `[]`. A
|
|
604
|
+
/// parameter written with `_` or with no external label contributes its internal name's absence as an
|
|
605
|
+
/// empty-string label, which simply won't match the macro's always-labeled signatures (a safe miss).
|
|
533
606
|
private func initializerParameterLabels(of declaration: some DeclGroupSyntax) -> [[String]] {
|
|
534
607
|
var signatures: [[String]] = []
|
|
535
608
|
for member in declaration.memberBlock.members {
|
|
@@ -546,18 +619,59 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
|
|
|
546
619
|
return signatures
|
|
547
620
|
}
|
|
548
621
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
622
|
+
/// The errors for a class the macro can't synthesize a record for: one that isn't `final`, or one
|
|
623
|
+
/// with an inheritance clause. Empty for a struct and for a supported class.
|
|
624
|
+
private func unsupportedClassDiagnostics(of declaration: some DeclGroupSyntax) -> [Diagnostic] {
|
|
625
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
626
|
+
return []
|
|
627
|
+
}
|
|
628
|
+
let name = classDecl.name.text
|
|
629
|
+
var diagnostics: [Diagnostic] = []
|
|
630
|
+
if !classDecl.modifiers.contains(where: { $0.name.tokenKind == .keyword(.final) }) {
|
|
631
|
+
// An `open` class can't also be `final`, so changing its access level is left to the author.
|
|
632
|
+
let isOpen = classDecl.modifiers.contains(where: { $0.name.tokenKind == .keyword(.open) })
|
|
633
|
+
diagnostics.append(
|
|
634
|
+
Diagnostic(
|
|
635
|
+
node: classDecl.classKeyword,
|
|
636
|
+
message: RecordDiagnosticMessage(
|
|
637
|
+
"@Record class '\(name)' must be final, because the macro doesn't support subclasses of a record",
|
|
638
|
+
id: "record-class-not-final"
|
|
639
|
+
),
|
|
640
|
+
fixIts: isOpen ? [] : [markFinalFixIt(for: classDecl)]
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
if let inheritanceClause = classDecl.inheritanceClause {
|
|
645
|
+
diagnostics.append(
|
|
646
|
+
Diagnostic(
|
|
647
|
+
node: inheritanceClause,
|
|
648
|
+
message: RecordDiagnosticMessage(
|
|
649
|
+
"@Record class '\(name)' can't have an inheritance clause: a record class can't have a superclass, and the macro can't tell a superclass from a protocol. Declare protocol conformances in an extension",
|
|
650
|
+
id: "record-class-inheritance"
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
)
|
|
559
654
|
}
|
|
560
|
-
return
|
|
655
|
+
return diagnostics
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// The fix-it for a record class that isn't `final`: add the modifier right before `class`.
|
|
659
|
+
private func markFinalFixIt(for classDecl: ClassDeclSyntax) -> FixIt {
|
|
660
|
+
// The new modifier takes over the leading trivia of `class` (the line break after the attribute
|
|
661
|
+
// when no other modifier comes first), so it lands where `class` started.
|
|
662
|
+
let classKeyword = classDecl.classKeyword
|
|
663
|
+
var modifiers = classDecl.modifiers
|
|
664
|
+
modifiers.append(
|
|
665
|
+
DeclModifierSyntax(name: .keyword(.final, leadingTrivia: classKeyword.leadingTrivia, trailingTrivia: .space))
|
|
666
|
+
)
|
|
667
|
+
let newDecl =
|
|
668
|
+
classDecl
|
|
669
|
+
.with(\.modifiers, modifiers)
|
|
670
|
+
.with(\.classKeyword, classKeyword.with(\.leadingTrivia, []))
|
|
671
|
+
return FixIt(
|
|
672
|
+
message: RecordFixItMessage("Mark the class 'final'", id: "record-class-insert-final"),
|
|
673
|
+
changes: [.replace(oldNode: Syntax(classDecl), newNode: Syntax(newDecl))]
|
|
674
|
+
)
|
|
561
675
|
}
|
|
562
676
|
|
|
563
677
|
private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
@@ -570,3 +684,13 @@ private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
|
570
684
|
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
571
685
|
}
|
|
572
686
|
}
|
|
687
|
+
|
|
688
|
+
private struct RecordFixItMessage: FixItMessage {
|
|
689
|
+
let message: String
|
|
690
|
+
let fixItID: MessageID
|
|
691
|
+
|
|
692
|
+
init(_ message: String, id: String) {
|
|
693
|
+
self.message = message
|
|
694
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
695
|
+
}
|
|
696
|
+
}
|