@expo/expo-modules-macros-plugin 0.0.8 → 0.1.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/publish.yml +116 -0
- package/.github/workflows/swift.yml +59 -0
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +12 -4
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +244 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +29 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +168 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +4 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +523 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +188 -0
- package/apple/build.js +129 -12
- package/package.json +1 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
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
|
+
- `from(object:appContext:)` — the fast path, reading each property straight off a
|
|
12
|
+
`JavaScriptObject`,
|
|
13
|
+
- `from(dictionary:appContext:)` — reading each property from a `[String: Any]` dictionary,
|
|
14
|
+
- `toDictionary(appContext:)` and `toObject(appContext:)` for the write side.
|
|
15
|
+
|
|
16
|
+
The type is auto-conformed to `Record` (a core protocol whose requirements are exactly this
|
|
17
|
+
surface); the synthesized methods override `Record`'s reflection-based defaults. Author-facing
|
|
18
|
+
shape — no conformance to spell out:
|
|
19
|
+
|
|
20
|
+
@Record
|
|
21
|
+
struct Options {
|
|
22
|
+
var name: String // required (non-optional, no default)
|
|
23
|
+
var count: Int = 0 // optional (has default)
|
|
24
|
+
var note: String? // nullable + optional
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
The JS key is always the property name. Requiredness is inferred from the declaration:
|
|
28
|
+
a default value makes a property optional (the default applies when the source omits it),
|
|
29
|
+
an optional type makes it nullable and optional, and a non-optional property without a
|
|
30
|
+
default is required (the factories throw when the source omits it).
|
|
31
|
+
|
|
32
|
+
Conversions go through the **public** dynamic-type API — `T.getDynamicType()` plus
|
|
33
|
+
`cast(jsValue:appContext:)` / `cast(_:appContext:)` / `convertToJS(_:appContext:)` —
|
|
34
|
+
so the synthesized code compiles inside user modules without any internal core symbols.
|
|
35
|
+
Every property type must therefore conform to `AnyArgument`.
|
|
36
|
+
|
|
37
|
+
For classes that inherit from another `@Record`-annotated class, the synthesized
|
|
38
|
+
methods chain to `super` so inherited properties are handled first.
|
|
39
|
+
*/
|
|
40
|
+
public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
41
|
+
public static func expansion(
|
|
42
|
+
of node: AttributeSyntax,
|
|
43
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
44
|
+
conformingTo protocols: [TypeSyntax],
|
|
45
|
+
in context: some MacroExpansionContext
|
|
46
|
+
) throws -> [DeclSyntax] {
|
|
47
|
+
let isClass = declaration.is(ClassDeclSyntax.self)
|
|
48
|
+
guard declaration.is(StructDeclSyntax.self) || isClass else {
|
|
49
|
+
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let properties = try recordProperties(of: declaration)
|
|
53
|
+
let inheritsRecord = isClass && classHasInheritance(declaration)
|
|
54
|
+
|
|
55
|
+
// The user may hand-write any of the initializers the macro would otherwise synthesize. Emitting
|
|
56
|
+
// a duplicate is a hard "invalid redeclaration" error, so skip anything already declared and let
|
|
57
|
+
// the author's version stand — the factories only need *an* `init(<properties>)` and `Record` only
|
|
58
|
+
// needs *an* `init()`, regardless of who wrote them.
|
|
59
|
+
let existingInitLabels = initializerParameterLabels(of: declaration)
|
|
60
|
+
|
|
61
|
+
var members: [DeclSyntax] = []
|
|
62
|
+
if !existingInitLabels.contains([]) {
|
|
63
|
+
if let defaultInit = defaultInit(properties: properties, isClass: isClass) {
|
|
64
|
+
members.append(defaultInit)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if !existingInitLabels.contains(properties.map { $0.name }) {
|
|
68
|
+
members.append(memberwiseInit(properties: properties))
|
|
69
|
+
}
|
|
70
|
+
members.append(fromJSObjectFactory(properties: properties))
|
|
71
|
+
members.append(fromDictionaryFactory(properties: properties))
|
|
72
|
+
members.append(toDictionaryMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
73
|
+
members.append(toObjectMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
74
|
+
return members
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
Auto-conforms the type to `Record` — the protocol whose requirements are exactly the members
|
|
79
|
+
synthesized above (`init()`, the two `from(_:)` factories, and the `toDictionary`/`toObject`
|
|
80
|
+
write side), which the synthesized methods satisfy by overriding `Record`'s reflection-based
|
|
81
|
+
defaults.
|
|
82
|
+
|
|
83
|
+
The conformance is skipped when the type already declares it, and for class subclasses
|
|
84
|
+
(which inherit it from a `@Record`-annotated parent) no extension is emitted at all.
|
|
85
|
+
*/
|
|
86
|
+
public static func expansion(
|
|
87
|
+
of node: AttributeSyntax,
|
|
88
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
89
|
+
providingExtensionsOf type: some TypeSyntaxProtocol,
|
|
90
|
+
conformingTo protocols: [TypeSyntax],
|
|
91
|
+
in context: some MacroExpansionContext
|
|
92
|
+
) throws -> [ExtensionDeclSyntax] {
|
|
93
|
+
if declaration.is(ClassDeclSyntax.self) && classHasInheritance(declaration) {
|
|
94
|
+
return []
|
|
95
|
+
}
|
|
96
|
+
guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
if inheritsProtocol(named: "Record", in: declaration) {
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let ext: DeclSyntax = """
|
|
104
|
+
extension \(type.trimmed): Record {}
|
|
105
|
+
"""
|
|
106
|
+
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
return [extDecl]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - Property model
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
A single record property discovered on the type, paired with everything the synthesized
|
|
117
|
+
conversions need: its property name (also the JS key), its written type, and how the
|
|
118
|
+
source may omit it.
|
|
119
|
+
*/
|
|
120
|
+
private struct RecordProperty {
|
|
121
|
+
let name: String
|
|
122
|
+
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build
|
|
123
|
+
/// the memberwise-init parameter and to look up the dynamic type via `T.getDynamicType()`.
|
|
124
|
+
let type: String
|
|
125
|
+
/// The default-value expression verbatim (`0`, `""`, `[]`), or `nil` when the property has none.
|
|
126
|
+
/// Inlined into the memberwise init and the factories' omitted-property branch so the synthesized
|
|
127
|
+
/// code never needs a throwaway `Self()` to recover defaults.
|
|
128
|
+
let defaultValue: String?
|
|
129
|
+
/// True when the property's type is optional (`T?` / `T!` / `Optional<T>`).
|
|
130
|
+
let isOptional: Bool
|
|
131
|
+
|
|
132
|
+
/// True when the property declares a default value (`var x: T = …`).
|
|
133
|
+
var hasDefault: Bool {
|
|
134
|
+
return defaultValue != nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Required properties must be present in the source; omission throws.
|
|
138
|
+
var isRequired: Bool {
|
|
139
|
+
return !hasDefault && !isOptional
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
Discovers the record's properties: every stored `var`/`let` binding that is not `static`,
|
|
145
|
+
`private`, `fileprivate`, `lazy`, or computed. Each property must declare an explicit type
|
|
146
|
+
annotation, since the synthesized conversions reference `Type.getDynamicType()`.
|
|
147
|
+
*/
|
|
148
|
+
private func recordProperties(
|
|
149
|
+
of declaration: some DeclGroupSyntax
|
|
150
|
+
) throws -> [RecordProperty] {
|
|
151
|
+
var properties: [RecordProperty] = []
|
|
152
|
+
|
|
153
|
+
for member in declaration.memberBlock.members {
|
|
154
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if isExcludedByModifier(varDecl.modifiers) {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
// `@Field` is the v1 property wrapper and has no meaning here — every stored property is a
|
|
161
|
+
// record property now. Left in place it would wrap the value in `Field<T>` (backing storage
|
|
162
|
+
// `_name`), so the synthesized conversions would be generated against the wrong type. Flag it
|
|
163
|
+
// explicitly rather than emit broken code.
|
|
164
|
+
if varDecl.attributes.firstAttribute(named: "Field") != nil {
|
|
165
|
+
throw MacroExpansionErrorMessage(
|
|
166
|
+
"@Field is no longer used — @Record treats every stored property as a record property. Remove the @Field attribute"
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
for binding in varDecl.bindings {
|
|
170
|
+
// Computed properties (and `{ get set }`) carry an accessor block — never properties.
|
|
171
|
+
if binding.accessorBlock != nil {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
// Prefer the explicit annotation. When it's omitted, recover the type from a literal default
|
|
178
|
+
// (Swift's own default-literal type) — covers the common `var name = "foo"` case. Anything
|
|
179
|
+
// whose type a syntactic macro can't determine (calls, collections, member access) still needs
|
|
180
|
+
// an annotation.
|
|
181
|
+
let inferredType = binding.typeAnnotation?.type.trimmedDescription
|
|
182
|
+
?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
|
|
183
|
+
guard let type = inferredType else {
|
|
184
|
+
throw MacroExpansionErrorMessage(
|
|
185
|
+
"@Record properties must declare an explicit type — '\(ident.identifier.text)' has none"
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
properties.append(
|
|
189
|
+
RecordProperty(
|
|
190
|
+
name: ident.identifier.text,
|
|
191
|
+
type: type,
|
|
192
|
+
defaultValue: binding.initializer?.value.trimmedDescription,
|
|
193
|
+
isOptional: binding.typeAnnotation.map { isOptionalType($0.type) } ?? false
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return properties
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: - Synthesized members
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
The parameterless `init()` required by the `Record` protocol. Nothing in the synthesized code uses
|
|
205
|
+
it — the factories construct through the memberwise init with defaults inlined — so it's purely a
|
|
206
|
+
conformance witness. Swift only synthesizes an implicit `init()` for a struct that declares no other
|
|
207
|
+
initializer, but the macro always emits a memberwise `init(…)`, which suppresses it, so for structs
|
|
208
|
+
the macro must supply `init()` itself.
|
|
209
|
+
|
|
210
|
+
When no property is required, the body is empty: every stored property initializes from its own
|
|
211
|
+
declaration. When a property *is* required there's no value to give it, so the body traps — this `init()`
|
|
212
|
+
is never reached (the `from(…)` factories don't call it), it exists only to satisfy the requirement.
|
|
213
|
+
|
|
214
|
+
Not emitted when there are no properties at all — there the memberwise init already *is* `init()`. Classes
|
|
215
|
+
are left alone: they inherit `init()` from their `@Record` superclass, and a base `@Record` class is
|
|
216
|
+
unsupported today.
|
|
217
|
+
*/
|
|
218
|
+
private func defaultInit(properties: [RecordProperty], isClass: Bool) -> DeclSyntax? {
|
|
219
|
+
if isClass || properties.isEmpty {
|
|
220
|
+
return nil
|
|
221
|
+
}
|
|
222
|
+
if properties.contains(where: { $0.isRequired }) {
|
|
223
|
+
return """
|
|
224
|
+
public init() {
|
|
225
|
+
fatalError("\\(Self.self) has required properties and cannot be created with init(); construct it through the @Record-synthesized from(dictionary:) or from(object:) factories")
|
|
226
|
+
}
|
|
227
|
+
"""
|
|
228
|
+
}
|
|
229
|
+
return """
|
|
230
|
+
public init() {
|
|
231
|
+
}
|
|
232
|
+
"""
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
Explicit memberwise initializer over the discovered properties, in declaration order. The factories
|
|
237
|
+
call this as the single construction point. Optional properties default to `nil` for ergonomics; all
|
|
238
|
+
other parameters are required at this level (the factories supply a value for every property, inlining
|
|
239
|
+
each declared default where the source omits it).
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
243
|
+
let params = properties.map { property -> String in
|
|
244
|
+
if property.isOptional {
|
|
245
|
+
return "\(property.name): \(property.type) = nil"
|
|
246
|
+
}
|
|
247
|
+
return "\(property.name): \(property.type)"
|
|
248
|
+
}
|
|
249
|
+
let assignments = properties.map { " self.\($0.name) = \($0.name)" }.joined(separator: "\n")
|
|
250
|
+
let signature = params.joined(separator: ", ")
|
|
251
|
+
|
|
252
|
+
return """
|
|
253
|
+
public init(\(raw: signature)) {
|
|
254
|
+
\(raw: assignments)
|
|
255
|
+
}
|
|
256
|
+
"""
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
`from(object:appContext:)` — reads each property off the `JavaScriptObject` into a local,
|
|
261
|
+
then constructs the record through the memberwise init. Required properties throw when
|
|
262
|
+
undefined; defaulted properties fall back to the property's declared default (read from a
|
|
263
|
+
throwaway `Self()`) when undefined; optional properties become `nil` when undefined/null.
|
|
264
|
+
*/
|
|
265
|
+
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
266
|
+
let body = factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties))
|
|
267
|
+
return """
|
|
268
|
+
@JavaScriptActor
|
|
269
|
+
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
270
|
+
\(raw: body)
|
|
271
|
+
}
|
|
272
|
+
"""
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
`from(dictionary:appContext:)` — same as the JS path but reads from `[String: Any]` and
|
|
277
|
+
uses the `Any?` cast overload; a missing key is `dictionary[key] == nil`.
|
|
278
|
+
*/
|
|
279
|
+
private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
280
|
+
let body = factoryBody(properties: properties, readLines: dictionaryReadLines(properties: properties))
|
|
281
|
+
return """
|
|
282
|
+
public static func from(dictionary: [String: Any], appContext: AppContext) throws -> Self {
|
|
283
|
+
\(raw: body)
|
|
284
|
+
}
|
|
285
|
+
"""
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
Builds a factory body: per-property reads into locals, then a single call to the synthesized
|
|
290
|
+
memberwise init. Each property's declared default expression is captured at parse time and inlined
|
|
291
|
+
directly into the omitted-property branch (see the read-line builders), so the factory needs neither
|
|
292
|
+
a throwaway `Self()` nor the record's `init()`.
|
|
293
|
+
*/
|
|
294
|
+
private func factoryBody(properties: [RecordProperty], readLines: [String]) -> String {
|
|
295
|
+
var lines: [String] = []
|
|
296
|
+
lines.append(contentsOf: readLines)
|
|
297
|
+
let initArgs = properties.map { "\($0.name): \($0.name)" }.joined(separator: ", ")
|
|
298
|
+
lines.append(" return Self(\(initArgs))")
|
|
299
|
+
return lines.joined(separator: "\n")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// Per-property read statements for the JS-object factory, each producing a `let <name>`.
|
|
303
|
+
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
304
|
+
var lines: [String] = []
|
|
305
|
+
for property in properties {
|
|
306
|
+
let valueVar = "\(property.name)JSValue"
|
|
307
|
+
let cast = "try \(property.type).getDynamicType().cast(jsValue: \(valueVar), appContext: appContext) as! \(property.type)"
|
|
308
|
+
lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
|
|
309
|
+
if property.isRequired {
|
|
310
|
+
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
311
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
|
|
312
|
+
lines.append(" }")
|
|
313
|
+
lines.append(" let \(property.name) = \(cast)")
|
|
314
|
+
} else if property.isOptional {
|
|
315
|
+
lines.append(" let \(property.name): \(property.type) = (\(valueVar).isUndefined() || \(valueVar).isNull()) ? nil : \(cast)")
|
|
316
|
+
} else {
|
|
317
|
+
lines.append(" let \(property.name) = \(valueVar).isUndefined() ? \(property.defaultValue!) : \(cast)")
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return lines
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// Per-property read statements for the dictionary factory, each producing a `let <name>`.
|
|
324
|
+
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
325
|
+
var lines: [String] = []
|
|
326
|
+
for property in properties {
|
|
327
|
+
let valueVar = "\(property.name)Value"
|
|
328
|
+
let cast = "try \(property.type).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(property.type)"
|
|
329
|
+
lines.append(" let \(valueVar) = dictionary[\"\(property.name)\"]")
|
|
330
|
+
if property.isRequired {
|
|
331
|
+
lines.append(" guard let \(valueVar) else {")
|
|
332
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
|
|
333
|
+
lines.append(" }")
|
|
334
|
+
lines.append(" let \(property.name) = \(cast)")
|
|
335
|
+
} else if property.isOptional {
|
|
336
|
+
lines.append(" let \(property.name): \(property.type) = (\(valueVar) == nil || \(valueVar)! is NSNull) ? nil : \(cast)")
|
|
337
|
+
} else {
|
|
338
|
+
lines.append(" let \(property.name) = \(valueVar) == nil ? \(property.defaultValue!) : \(cast)")
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return lines
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
`toDictionary(appContext:)` — converts each property back to a JS-compatible value via the
|
|
346
|
+
dynamic type and assembles a `[String: Any]`. Inherited properties are merged in first.
|
|
347
|
+
*/
|
|
348
|
+
private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
349
|
+
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
350
|
+
var lines: [String] = []
|
|
351
|
+
if inheritsRecord {
|
|
352
|
+
lines.append(" var dictionary = super.toDictionary(appContext: appContext)")
|
|
353
|
+
} else {
|
|
354
|
+
lines.append(" var dictionary: [String: Any] = [:]")
|
|
355
|
+
}
|
|
356
|
+
for property in properties {
|
|
357
|
+
lines.append(" dictionary[\"\(property.name)\"] = self.\(property.name)")
|
|
358
|
+
}
|
|
359
|
+
lines.append(" return dictionary")
|
|
360
|
+
let body = lines.joined(separator: "\n")
|
|
361
|
+
|
|
362
|
+
if inheritsRecord {
|
|
363
|
+
return """
|
|
364
|
+
public \(raw: overrideKeyword)func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
365
|
+
\(raw: body)
|
|
366
|
+
}
|
|
367
|
+
"""
|
|
368
|
+
}
|
|
369
|
+
return """
|
|
370
|
+
public func toDictionary(appContext: AppContext? = nil) -> [String: Any] {
|
|
371
|
+
\(raw: body)
|
|
372
|
+
}
|
|
373
|
+
"""
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
`toObject(appContext:)` — builds a `JavaScriptObject` directly, converting each property via
|
|
378
|
+
`convertToJS`. The fast write path mirroring `from(object:)`. Subclasses chain to `super` so
|
|
379
|
+
inherited properties are written first.
|
|
380
|
+
*/
|
|
381
|
+
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
382
|
+
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
383
|
+
var lines: [String] = []
|
|
384
|
+
if inheritsRecord {
|
|
385
|
+
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
386
|
+
} else {
|
|
387
|
+
lines.append(" let object = try appContext.runtime.createObject()")
|
|
388
|
+
}
|
|
389
|
+
for property in properties {
|
|
390
|
+
lines.append(" object.setProperty(\"\(property.name)\", value: try \(property.type).getDynamicType().convertToJS(self.\(property.name), appContext: appContext))")
|
|
391
|
+
}
|
|
392
|
+
lines.append(" return object")
|
|
393
|
+
let body = lines.joined(separator: "\n")
|
|
394
|
+
|
|
395
|
+
return """
|
|
396
|
+
@JavaScriptActor
|
|
397
|
+
public \(raw: overrideKeyword)func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
398
|
+
\(raw: body)
|
|
399
|
+
}
|
|
400
|
+
"""
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// MARK: - Helpers
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
The parameter-label list of every initializer the type already declares, used to avoid emitting a
|
|
407
|
+
duplicate of one the author hand-wrote. Each entry is the ordered external argument labels of one
|
|
408
|
+
`init` — e.g. `init(name: String, count: Int)` → `["name", "count"]`, and `init()` → `[]`. A
|
|
409
|
+
parameter written with `_` or with no external label contributes its internal name's absence as an
|
|
410
|
+
empty-string label, which simply won't match the macro's always-labeled signatures (a safe miss).
|
|
411
|
+
*/
|
|
412
|
+
private func initializerParameterLabels(of declaration: some DeclGroupSyntax) -> [[String]] {
|
|
413
|
+
var signatures: [[String]] = []
|
|
414
|
+
for member in declaration.memberBlock.members {
|
|
415
|
+
guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
let labels = initDecl.signature.parameterClause.parameters.map { parameter in
|
|
419
|
+
// `firstName` is the external label (or `_`); fall back to the internal name when omitted.
|
|
420
|
+
let label = parameter.firstName.text
|
|
421
|
+
return label == "_" ? "" : label
|
|
422
|
+
}
|
|
423
|
+
signatures.append(labels)
|
|
424
|
+
}
|
|
425
|
+
return signatures
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
True if the variable declaration carries a modifier that excludes it from being a property:
|
|
430
|
+
`static`, `class` (type-level storage), `private`, `fileprivate`, or `lazy`.
|
|
431
|
+
*/
|
|
432
|
+
private func isExcludedByModifier(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
433
|
+
for modifier in modifiers {
|
|
434
|
+
switch modifier.name.tokenKind {
|
|
435
|
+
case .keyword(.static), .keyword(.class), .keyword(.private), .keyword(.fileprivate), .keyword(.lazy):
|
|
436
|
+
return true
|
|
437
|
+
default:
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return false
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
The Swift default type of a literal expression — `String`, `Double`, `Int`, or `Bool` — or `nil`
|
|
446
|
+
when the expression isn't one of those literals. Used to recover a property's type when it has no
|
|
447
|
+
annotation but does have a literal default (`var name = "foo"` → `String`). This matches the type
|
|
448
|
+
Swift itself would infer for the same un-annotated declaration; expressions whose type a syntactic
|
|
449
|
+
macro can't know (function calls, collection literals, member access) return `nil`.
|
|
450
|
+
*/
|
|
451
|
+
private func inferredLiteralType(of expression: ExprSyntax) -> String? {
|
|
452
|
+
if expression.is(StringLiteralExprSyntax.self) {
|
|
453
|
+
return "String"
|
|
454
|
+
}
|
|
455
|
+
if expression.is(FloatLiteralExprSyntax.self) {
|
|
456
|
+
return "Double"
|
|
457
|
+
}
|
|
458
|
+
if expression.is(IntegerLiteralExprSyntax.self) {
|
|
459
|
+
return "Int"
|
|
460
|
+
}
|
|
461
|
+
if expression.is(BooleanLiteralExprSyntax.self) {
|
|
462
|
+
return "Bool"
|
|
463
|
+
}
|
|
464
|
+
return nil
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
True if the type syntax is optional: `T?`, `T!`, or the spelled-out `Optional<T>`.
|
|
469
|
+
*/
|
|
470
|
+
private func isOptionalType(_ type: TypeSyntax) -> Bool {
|
|
471
|
+
if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
|
|
472
|
+
return true
|
|
473
|
+
}
|
|
474
|
+
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == "Optional" {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
return false
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
True if the type's inheritance clause already lists a protocol with the given name.
|
|
482
|
+
Matches either the bare identifier (`Record`) or a qualified member access ending in
|
|
483
|
+
the name (`ExpoModulesCore.Record`).
|
|
484
|
+
*/
|
|
485
|
+
private func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
|
|
486
|
+
let inheritanceClause: InheritanceClauseSyntax?
|
|
487
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
488
|
+
inheritanceClause = structDecl.inheritanceClause
|
|
489
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
490
|
+
inheritanceClause = classDecl.inheritanceClause
|
|
491
|
+
} else {
|
|
492
|
+
return false
|
|
493
|
+
}
|
|
494
|
+
guard let inherited = inheritanceClause?.inheritedTypes else {
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
for entry in inherited {
|
|
498
|
+
let typeSyntax = entry.type
|
|
499
|
+
if let identifier = typeSyntax.as(IdentifierTypeSyntax.self),
|
|
500
|
+
identifier.name.text == name {
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
if let member = typeSyntax.as(MemberTypeSyntax.self),
|
|
504
|
+
member.name.text == name {
|
|
505
|
+
return true
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return false
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
True if the class declaration has any inheritance clause. Used as a heuristic for
|
|
513
|
+
whether the superclass also conforms to `Record` and provides the synthesized methods;
|
|
514
|
+
the macro emits `override` in this case.
|
|
515
|
+
*/
|
|
516
|
+
private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
|
|
517
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
518
|
+
let inherited = classDecl.inheritanceClause?.inheritedTypes,
|
|
519
|
+
!inherited.isEmpty else {
|
|
520
|
+
return false
|
|
521
|
+
}
|
|
522
|
+
return true
|
|
523
|
+
}
|