@expo/expo-modules-macros-plugin 0.0.9 → 0.2.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.
@@ -3,34 +3,39 @@ import SwiftSyntaxBuilder
3
3
  import SwiftSyntaxMacros
4
4
 
5
5
  /**
6
- Member macro applied to a `Record` type. Scans the body for `@Field`-annotated
7
- stored properties and synthesizes `_recordFields(of:)`, returning each field
8
- paired with its compile-time key. This replaces the runtime `Mirror` walk in
9
- `fieldsOf(_:)` and lets the framework drop the lazy keying that today protects
10
- the field's options with a `Mutex`.
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:
11
9
 
12
- @Record
13
- struct Options: Record {
14
- @Field var name: String = ""
15
- @Field(.required) var count: Int = 0
16
- @Field(.keyed("custom_key")) var flag: Bool = false
17
- }
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.
18
15
 
19
- Expansion:
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:
20
19
 
21
- public static func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
22
- var fields: [RecordFieldDescriptor] = []
23
- fields.append(RecordFieldDescriptor(key: "name", isRequired: false, field: instance._name))
24
- fields.append(RecordFieldDescriptor(key: "count", isRequired: true, field: instance._count))
25
- fields.append(RecordFieldDescriptor(key: "custom_key", isRequired: false, field: instance._flag))
26
- return fields
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
27
25
  }
28
26
 
29
- `isRequired` is computed from the `@Field` attribute arguments at compile time so the
30
- framework can avoid a `Mutex.withLock` per field per call.
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
31
 
32
- For classes that inherit from another `@Record`-annotated class, the macro
33
- prepends `super._recordFields(of: instance)` so inherited fields appear first.
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.
34
39
  */
35
40
  public struct RecordMacro: MemberMacro, ExtensionMacro {
36
41
  public static func expansion(
@@ -44,64 +49,39 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
44
49
  throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
45
50
  }
46
51
 
47
- var entries: [(propertyName: String, key: String, isRequired: Bool)] = []
48
-
49
- for member in declaration.memberBlock.members {
50
- guard let varDecl = member.decl.as(VariableDeclSyntax.self),
51
- let attribute = varDecl.attributes.firstAttribute(named: "Field") else {
52
- continue
53
- }
54
- for binding in varDecl.bindings {
55
- guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
56
- continue
57
- }
58
- let propertyName = ident.identifier.text
59
- let key = explicitKeyArgument(of: attribute) ?? propertyName
60
- let isRequired = hasRequiredArgument(of: attribute)
61
- entries.append((propertyName, key, isRequired))
62
- }
63
- }
64
-
52
+ let properties = try recordProperties(of: declaration)
65
53
  let inheritsRecord = isClass && classHasInheritance(declaration)
66
- let overrideKeyword = inheritsRecord ? "override " : ""
67
-
68
- var lines: [String] = []
69
- if inheritsRecord {
70
- lines.append(" var fields: [RecordFieldDescriptor] = super._recordFields(of: instance)")
71
- } else {
72
- lines.append(" var fields: [RecordFieldDescriptor] = []")
73
- }
74
- for entry in entries {
75
- let requiredLiteral = entry.isRequired ? "true" : "false"
76
- lines.append(" fields.append(RecordFieldDescriptor(key: \"\(entry.key)\", isRequired: \(requiredLiteral), field: instance._\(entry.propertyName)))")
77
- }
78
- lines.append(" return fields")
79
54
 
80
- let body = lines.joined(separator: "\n")
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)
81
60
 
82
- let method: DeclSyntax = """
83
- public \(raw: overrideKeyword)class func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
84
- \(raw: body)
61
+ var members: [DeclSyntax] = []
62
+ if !existingInitLabels.contains([]) {
63
+ if let defaultInit = defaultInit(properties: properties, isClass: isClass) {
64
+ members.append(defaultInit)
85
65
  }
86
- """
87
-
88
- let staticMethod: DeclSyntax = """
89
- public static func _recordFields(of instance: Self) -> [RecordFieldDescriptor] {
90
- \(raw: body)
91
- }
92
- """
93
-
94
- return [isClass ? method : staticMethod]
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
95
75
  }
96
76
 
97
77
  /**
98
- Adds `Record` and `_RecordFieldsProvider` conformances. Each is only added when the type
99
- doesn't already declare it Swift rejects redundant conformances, so the macro must check
100
- the inheritance clause syntactically.
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.
101
82
 
102
- For class subclasses (which inherit conformance from a `@Record`-annotated parent), no
103
- extension is emitted at all. The user can write either `@Record struct Options { ... }`
104
- or `@Record struct Options: Record { ... }`; both work.
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.
105
85
  */
106
86
  public static func expansion(
107
87
  of node: AttributeSyntax,
@@ -116,20 +96,12 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
116
96
  guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
117
97
  return []
118
98
  }
119
-
120
- var conformances: [String] = []
121
- if !inheritsProtocol(named: "Record", in: declaration) {
122
- conformances.append("Record")
123
- }
124
- if !inheritsProtocol(named: "_RecordFieldsProvider", in: declaration) {
125
- conformances.append("_RecordFieldsProvider")
126
- }
127
- if conformances.isEmpty {
99
+ if inheritsProtocol(named: "Record", in: declaration) {
128
100
  return []
129
101
  }
130
102
 
131
103
  let ext: DeclSyntax = """
132
- extension \(type.trimmed): \(raw: conformances.joined(separator: ", ")) {}
104
+ extension \(type.trimmed): Record {}
133
105
  """
134
106
  guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
135
107
  return []
@@ -138,12 +110,377 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
138
110
  }
139
111
  }
140
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
+
141
480
  /**
142
481
  True if the type's inheritance clause already lists a protocol with the given name.
143
482
  Matches either the bare identifier (`Record`) or a qualified member access ending in
144
- the name (`ExpoModulesCore.Record`). Typealiases can't be resolved syntactically, so a
145
- user who refers to `Record` via a typealias will get a redundant-conformance error from
146
- the macro-emitted extension and should drop the alias for the inheritance clause.
483
+ the name (`ExpoModulesCore.Record`).
147
484
  */
148
485
  private func inheritsProtocol(named name: String, in declaration: some DeclGroupSyntax) -> Bool {
149
486
  let inheritanceClause: InheritanceClauseSyntax?
@@ -173,9 +510,8 @@ private func inheritsProtocol(named name: String, in declaration: some DeclGroup
173
510
 
174
511
  /**
175
512
  True if the class declaration has any inheritance clause. Used as a heuristic for
176
- whether the superclass also conforms to `Record` and provides `_recordFields(of:)`.
177
- The macro emits an `override` in this case; if the superclass is not a `Record`,
178
- the user gets a clear compiler error pointing at the missing override target.
513
+ whether the superclass also conforms to `Record` and provides the synthesized methods;
514
+ the macro emits `override` in this case.
179
515
  */
180
516
  private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
181
517
  guard let classDecl = declaration.as(ClassDeclSyntax.self),
@@ -185,50 +521,3 @@ private func classHasInheritance(_ declaration: some DeclGroupSyntax) -> Bool {
185
521
  }
186
522
  return true
187
523
  }
188
-
189
- /**
190
- True if the `@Field` attribute has `.required` as one of its arguments. The check is purely
191
- syntactic — it matches the literal member-access expression `.required`. Variants like
192
- `FieldOption.required` or aliased identifiers won't be detected; the framework still falls
193
- back to the field's runtime `isRequired` for correctness in those cases (see `Record.swift`).
194
- */
195
- private func hasRequiredArgument(of attribute: AttributeSyntax) -> Bool {
196
- guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
197
- return false
198
- }
199
- for arg in args {
200
- if let member = arg.expression.as(MemberAccessExprSyntax.self),
201
- member.declName.baseName.text == "required" {
202
- return true
203
- }
204
- }
205
- return false
206
- }
207
-
208
- /**
209
- Extracts the dictionary key from a `@Field` attribute. Recognizes both forms:
210
- a bare string literal (`@Field("custom_key")`, relying on
211
- `FieldOption: ExpressibleByStringLiteral`) and the explicit factory call
212
- (`@Field(.keyed("custom_key"))`). Returns nil if neither is present; other
213
- `FieldOption` cases (`.required`, etc.) are ignored.
214
- */
215
- private func explicitKeyArgument(of attribute: AttributeSyntax) -> String? {
216
- guard let args = attribute.arguments?.as(LabeledExprListSyntax.self) else {
217
- return nil
218
- }
219
- for arg in args {
220
- if let str = arg.expression.as(StringLiteralExprSyntax.self),
221
- let segment = str.segments.first?.as(StringSegmentSyntax.self) {
222
- return segment.content.text
223
- }
224
- if let call = arg.expression.as(FunctionCallExprSyntax.self),
225
- let member = call.calledExpression.as(MemberAccessExprSyntax.self),
226
- member.declName.baseName.text == "keyed",
227
- let firstArg = call.arguments.first,
228
- let str = firstArg.expression.as(StringLiteralExprSyntax.self),
229
- let segment = str.segments.first?.as(StringSegmentSyntax.self) {
230
- return segment.content.text
231
- }
232
- }
233
- return nil
234
- }