expo-modules-macros 0.21.0 → 0.23.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 +8 -5
- package/.github/workflows/publish.yml +26 -11
- package/.github/workflows/swift-format.yml +79 -0
- package/.github/workflows/swift.yml +7 -4
- package/.github/workflows/windows.yml +8 -5
- package/AGENTS.md +16 -9
- package/README.md +20 -5
- 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 +144 -30
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
- 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 +184 -48
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
- 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 +20 -4
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/index.d.ts +1 -1
- package/build/index.js +3 -3
- package/build/types.d.ts +32 -2
- package/build/types.js +1 -1
- package/package.json +8 -5
|
@@ -3,53 +3,59 @@ import SwiftSyntax
|
|
|
3
3
|
import SwiftSyntaxBuilder
|
|
4
4
|
import SwiftSyntaxMacros
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- `from(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
53
59
|
public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
54
60
|
public static func expansion(
|
|
55
61
|
of node: AttributeSyntax,
|
|
@@ -65,10 +71,13 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
65
71
|
} else {
|
|
66
72
|
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
67
73
|
}
|
|
68
|
-
let
|
|
74
|
+
let classDiagnostics = unsupportedClassDiagnostics(of: declaration)
|
|
75
|
+
guard classDiagnostics.isEmpty else {
|
|
76
|
+
throw DiagnosticsError(diagnostics: classDiagnostics)
|
|
77
|
+
}
|
|
69
78
|
|
|
70
79
|
let properties = try recordProperties(of: declaration)
|
|
71
|
-
let
|
|
80
|
+
let cachesPropNames = !properties.isEmpty && !isInGenericContext(declaration, context: context)
|
|
72
81
|
|
|
73
82
|
// The user may hand-write any of the initializers the macro would otherwise synthesize. Emitting
|
|
74
83
|
// a duplicate is a hard "invalid redeclaration" error, so skip anything already declared and let
|
|
@@ -99,32 +108,37 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
if !existingInitLabels.contains([]) {
|
|
102
|
-
if let defaultInit = defaultInit(properties: properties
|
|
111
|
+
if let defaultInit = defaultInit(properties: properties) {
|
|
103
112
|
members.append(defaultInit)
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
107
116
|
members.append(memberwiseInit(properties: properties))
|
|
108
117
|
}
|
|
109
|
-
members.append(fromJSObjectFactory(
|
|
118
|
+
members.append(fromJSObjectFactory())
|
|
110
119
|
members.append(fromDictionaryFactory(properties: properties))
|
|
111
|
-
members.append(toDictionaryMethod(properties: properties
|
|
112
|
-
members.append(toObjectMethod(
|
|
113
|
-
if
|
|
114
|
-
members.append(
|
|
120
|
+
members.append(toDictionaryMethod(properties: properties))
|
|
121
|
+
members.append(toObjectMethod())
|
|
122
|
+
if cachesPropNames {
|
|
123
|
+
members.append(propNamesClass(properties: properties))
|
|
115
124
|
}
|
|
125
|
+
members.append(
|
|
126
|
+
contentsOf: javaScriptCodableWitnesses(
|
|
127
|
+
properties: properties,
|
|
128
|
+
typeName: typeName,
|
|
129
|
+
cachesPropNames: cachesPropNames
|
|
130
|
+
)
|
|
131
|
+
)
|
|
116
132
|
return members
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(which inherit it from a `@Record`-annotated parent) no extension is emitted at all.
|
|
127
|
-
*/
|
|
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.
|
|
128
142
|
public static func expansion(
|
|
129
143
|
of node: AttributeSyntax,
|
|
130
144
|
attachedTo declaration: some DeclGroupSyntax,
|
|
@@ -132,10 +146,10 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
132
146
|
conformingTo protocols: [TypeSyntax],
|
|
133
147
|
in context: some MacroExpansionContext
|
|
134
148
|
) throws -> [ExtensionDeclSyntax] {
|
|
135
|
-
|
|
149
|
+
guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
|
|
136
150
|
return []
|
|
137
151
|
}
|
|
138
|
-
guard
|
|
152
|
+
guard unsupportedClassDiagnostics(of: declaration).isEmpty else {
|
|
139
153
|
return []
|
|
140
154
|
}
|
|
141
155
|
if inheritsProtocol(named: "Record", in: declaration) {
|
|
@@ -154,10 +168,8 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
154
168
|
|
|
155
169
|
// MARK: - Property model
|
|
156
170
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
conversions need: its property name, its written type, and how the source may omit it.
|
|
160
|
-
*/
|
|
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.
|
|
161
173
|
private struct RecordProperty {
|
|
162
174
|
/// The property name as written, backticks included for an escaped name (`` `default` ``), since
|
|
163
175
|
/// every identifier position in the generated code needs them.
|
|
@@ -192,11 +204,9 @@ private struct RecordProperty {
|
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
annotation, since the synthesized conversions name the type (e.g. `Type.decode(…)`).
|
|
199
|
-
*/
|
|
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(…)`).
|
|
200
210
|
private func recordProperties(
|
|
201
211
|
of declaration: some DeclGroupSyntax
|
|
202
212
|
) throws -> [RecordProperty] {
|
|
@@ -230,7 +240,8 @@ private func recordProperties(
|
|
|
230
240
|
// (Swift's own default-literal type) — covers the common `var name = "foo"` case. Anything
|
|
231
241
|
// whose type a syntactic macro can't determine (calls, collections, member access) still needs
|
|
232
242
|
// an annotation.
|
|
233
|
-
let inferredType =
|
|
243
|
+
let inferredType =
|
|
244
|
+
binding.typeAnnotation?.type.trimmedDescription
|
|
234
245
|
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
235
246
|
guard let type = inferredType else {
|
|
236
247
|
throw MacroExpansionErrorMessage(
|
|
@@ -249,7 +260,8 @@ private func recordProperties(
|
|
|
249
260
|
message: RecordDiagnosticMessage(
|
|
250
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",
|
|
251
262
|
id: "record-closure-property"
|
|
252
|
-
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
253
265
|
])
|
|
254
266
|
}
|
|
255
267
|
let freeForm = try annotatedType.flatMap { annotatedType -> FreeFormConversion? in
|
|
@@ -279,23 +291,19 @@ private func recordProperties(
|
|
|
279
291
|
|
|
280
292
|
// MARK: - Synthesized members
|
|
281
293
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
When
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
unsupported today.
|
|
296
|
-
*/
|
|
297
|
-
private func defaultInit(properties: [RecordProperty], isClass: Bool) -> DeclSyntax? {
|
|
298
|
-
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 {
|
|
299
307
|
return nil
|
|
300
308
|
}
|
|
301
309
|
if properties.contains(where: { $0.isRequired }) {
|
|
@@ -335,44 +343,24 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
335
343
|
"""
|
|
336
344
|
}
|
|
337
345
|
|
|
338
|
-
|
|
339
|
-
`
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
private func fromJSObjectFactory(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
344
|
-
guard inheritsRecord else {
|
|
345
|
-
// The record declares only the unowned `decode`, so the call lends the value unowned. With an owning
|
|
346
|
-
// value it would reach an owning `decode` default, and `Record`'s converts through this factory.
|
|
347
|
-
return """
|
|
348
|
-
@JavaScriptActor
|
|
349
|
-
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
350
|
-
let runtime = try appContext.runtime
|
|
351
|
-
return try object.asValue().withUnownedValue(in: runtime) { unownedValue in
|
|
352
|
-
return try decode(unownedValue, in: runtime)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
"""
|
|
356
|
-
}
|
|
357
|
-
var lines: [String] = []
|
|
358
|
-
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
359
|
-
// otherwise leave it unused.
|
|
360
|
-
if !properties.isEmpty {
|
|
361
|
-
lines.append(" let runtime = try appContext.runtime")
|
|
362
|
-
}
|
|
363
|
-
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
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.
|
|
364
351
|
return """
|
|
365
352
|
@JavaScriptActor
|
|
366
353
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
367
|
-
|
|
354
|
+
let runtime = try appContext.runtime
|
|
355
|
+
return try object.asValue().withUnownedValue(in: runtime) { unownedValue in
|
|
356
|
+
return try decode(unownedValue, in: runtime)
|
|
357
|
+
}
|
|
368
358
|
}
|
|
369
359
|
"""
|
|
370
360
|
}
|
|
371
361
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
uses the `Any?` cast overload; a missing key is `dictionary[key] == nil`.
|
|
375
|
-
*/
|
|
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`.
|
|
376
364
|
private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
377
365
|
let body = factoryBody(properties: properties, readLines: dictionaryReadLines(properties: properties))
|
|
378
366
|
return """
|
|
@@ -382,12 +370,10 @@ private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
382
370
|
"""
|
|
383
371
|
}
|
|
384
372
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
a throwaway `Self()` nor the record's `init()`.
|
|
390
|
-
*/
|
|
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()`.
|
|
391
377
|
private func factoryBody(properties: [RecordProperty], readLines: [String]) -> String {
|
|
392
378
|
var lines: [String] = []
|
|
393
379
|
lines.append(contentsOf: readLines)
|
|
@@ -407,12 +393,13 @@ private func fieldLocal(_ index: Int) -> String {
|
|
|
407
393
|
/// named `object`. The property is lent to a closure as a `JavaScriptUnownedValue` and decoded there the
|
|
408
394
|
/// same way as a `@JS` argument (see `decodeCall`), so the read doesn't allocate a `JavaScriptValue`.
|
|
409
395
|
/// Only the generic free-form decode copies the value into one.
|
|
410
|
-
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
396
|
+
private func jsObjectReadLines(properties: [RecordProperty], cachesPropNames: Bool) -> [String] {
|
|
411
397
|
var lines: [String] = []
|
|
412
398
|
for (index, property) in properties.enumerated() {
|
|
413
399
|
let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
|
|
414
400
|
let decode = "try \(decodeCall(property.type, freeForm: property.freeForm, from: valueVar))"
|
|
415
|
-
|
|
401
|
+
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
402
|
+
lines.append(" let \(fieldLocal(index)) = try object.withUnownedProperty(\(propName)) { \(valueVar) in")
|
|
416
403
|
if property.isRequired {
|
|
417
404
|
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
418
405
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
@@ -440,7 +427,8 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
440
427
|
let entry = "entry\(index)"
|
|
441
428
|
let exprType = expressionType(property.type)
|
|
442
429
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
443
|
-
let cast =
|
|
430
|
+
let cast =
|
|
431
|
+
isBareAny
|
|
444
432
|
? entry
|
|
445
433
|
: "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
446
434
|
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
@@ -458,91 +446,60 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
458
446
|
return lines
|
|
459
447
|
}
|
|
460
448
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
466
|
-
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
467
|
-
var lines: [String] = []
|
|
468
|
-
if inheritsRecord {
|
|
469
|
-
lines.append(" var dictionary = super.toDictionary(appContext: appContext)")
|
|
470
|
-
} else {
|
|
471
|
-
lines.append(" var dictionary: [String: Any] = [:]")
|
|
472
|
-
}
|
|
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] = [:]"]
|
|
473
453
|
for property in properties {
|
|
474
454
|
lines.append(" dictionary[\"\(property.key)\"] = self.\(property.name)")
|
|
475
455
|
}
|
|
476
456
|
lines.append(" return dictionary")
|
|
477
|
-
let body = lines.joined(separator: "\n")
|
|
478
|
-
|
|
479
|
-
if inheritsRecord {
|
|
480
|
-
return """
|
|
481
|
-
public \(raw: overrideKeyword)func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
482
|
-
\(raw: body)
|
|
483
|
-
}
|
|
484
|
-
"""
|
|
485
|
-
}
|
|
486
457
|
return """
|
|
487
458
|
public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
488
|
-
\(raw:
|
|
459
|
+
\(raw: lines.joined(separator: "\n"))
|
|
489
460
|
}
|
|
490
461
|
"""
|
|
491
462
|
}
|
|
492
463
|
|
|
493
|
-
|
|
494
|
-
`
|
|
495
|
-
|
|
496
|
-
overrides it to chain to `super`, so inherited properties are written first, and writes its own
|
|
497
|
-
properties in the app context's runtime.
|
|
498
|
-
*/
|
|
499
|
-
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
500
|
-
guard inheritsRecord else {
|
|
501
|
-
return """
|
|
502
|
-
@JavaScriptActor
|
|
503
|
-
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
504
|
-
return try Self.encode(self, in: appContext.runtime).asObject()
|
|
505
|
-
}
|
|
506
|
-
"""
|
|
507
|
-
}
|
|
508
|
-
var lines: [String] = []
|
|
509
|
-
// The runtime is only needed when there's a property to encode (the object itself comes from
|
|
510
|
-
// `super`). Binding it when unused would warn.
|
|
511
|
-
if !properties.isEmpty {
|
|
512
|
-
lines.append(" let runtime = try appContext.runtime")
|
|
513
|
-
}
|
|
514
|
-
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
515
|
-
lines.append(contentsOf: objectWriteLines(properties: properties, of: "self"))
|
|
516
|
-
lines.append(" return object")
|
|
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 {
|
|
517
467
|
return """
|
|
518
468
|
@JavaScriptActor
|
|
519
|
-
public
|
|
520
|
-
|
|
469
|
+
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
470
|
+
return try Self.encode(self, in: appContext.runtime).asObject()
|
|
521
471
|
}
|
|
522
472
|
"""
|
|
523
473
|
}
|
|
524
474
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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] {
|
|
538
490
|
// An empty record still decodes only from an object, but has no property to read off it.
|
|
539
|
-
let objectLine =
|
|
491
|
+
let objectLine =
|
|
492
|
+
properties.isEmpty
|
|
540
493
|
? " _ = try value.asObject(in: runtime)"
|
|
541
494
|
: " let object = try value.asObject(in: runtime)"
|
|
542
|
-
let
|
|
543
|
-
let
|
|
544
|
-
|
|
545
|
-
|
|
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.takeValue()"]
|
|
546
503
|
return [
|
|
547
504
|
"""
|
|
548
505
|
@JavaScriptActor
|
|
@@ -562,13 +519,14 @@ private func javaScriptCodableWitnesses(properties: [RecordProperty], typeName:
|
|
|
562
519
|
/// The statements that write each property of `record` (`self` or the `encode` parameter) into the
|
|
563
520
|
/// `object` local, encoding it with `JavaScriptEncodable.encode` or, for a free-form property, the
|
|
564
521
|
/// matching `JavaScriptValue` free-form method.
|
|
565
|
-
private func objectWriteLines(properties: [RecordProperty], of record: String) -> [String] {
|
|
566
|
-
return properties.map { property in
|
|
522
|
+
private func objectWriteLines(properties: [RecordProperty], of record: String, cachesPropNames: Bool) -> [String] {
|
|
523
|
+
return properties.enumerated().map { index, property in
|
|
567
524
|
let value = "\(record).\(property.name)"
|
|
568
525
|
let encode: String
|
|
569
526
|
switch property.freeForm {
|
|
570
527
|
case .shape(_, let encodeMethod, let isOptional, _):
|
|
571
|
-
encode =
|
|
528
|
+
encode =
|
|
529
|
+
isOptional
|
|
572
530
|
? "\(value) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(\(value)!, in: runtime)"
|
|
573
531
|
: "try JavaScriptValue.\(encodeMethod)(\(value), in: runtime)"
|
|
574
532
|
case .generic:
|
|
@@ -577,19 +535,74 @@ private func objectWriteLines(properties: [RecordProperty], of record: String) -
|
|
|
577
535
|
case nil:
|
|
578
536
|
encode = "try \(expressionType(property.type)).encode(\(value), in: runtime)"
|
|
579
537
|
}
|
|
580
|
-
|
|
538
|
+
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
539
|
+
return " object.setProperty(\(propName), value: \(encode))"
|
|
540
|
+
}
|
|
541
|
+
}
|
|
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
|
+
}
|
|
556
|
+
return """
|
|
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
|
+
}
|
|
565
|
+
}
|
|
566
|
+
"""
|
|
567
|
+
}
|
|
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
|
|
581
596
|
}
|
|
582
597
|
}
|
|
583
598
|
|
|
584
599
|
// MARK: - Helpers
|
|
585
600
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
empty-string label, which simply won't match the macro's always-labeled signatures (a safe miss).
|
|
592
|
-
*/
|
|
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).
|
|
593
606
|
private func initializerParameterLabels(of declaration: some DeclGroupSyntax) -> [[String]] {
|
|
594
607
|
var signatures: [[String]] = []
|
|
595
608
|
for member in declaration.memberBlock.members {
|
|
@@ -606,18 +619,59 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
|
|
|
606
619
|
return signatures
|
|
607
620
|
}
|
|
608
621
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
+
)
|
|
619
654
|
}
|
|
620
|
-
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
|
+
)
|
|
621
675
|
}
|
|
622
676
|
|
|
623
677
|
private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
@@ -630,3 +684,13 @@ private struct RecordDiagnosticMessage: DiagnosticMessage {
|
|
|
630
684
|
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
631
685
|
}
|
|
632
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
|
+
}
|