expo-modules-macros 0.17.0 → 0.19.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.
@@ -72,6 +72,9 @@ internal func isConcurrentJSMember(_ decl: DeclSyntaxProtocol) -> Bool {
72
72
  /// True if the type is written as an optional: `T?`, `T!`, or the explicit `Optional<T>`. Used to
73
73
  /// decide argument requiredness (an optional parameter may be omitted) and record-field nullability.
74
74
  internal func isOptionalType(_ type: TypeSyntax) -> Bool {
75
+ if let attributed = type.as(AttributedTypeSyntax.self) {
76
+ return isOptionalType(attributed.baseType)
77
+ }
75
78
  if type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
76
79
  return true
77
80
  }
@@ -81,6 +84,39 @@ internal func isOptionalType(_ type: TypeSyntax) -> Bool {
81
84
  return false
82
85
  }
83
86
 
87
+ /// A parameter's type split from the specifiers written in front of it (`borrowing`, `consuming`,
88
+ /// `sending`, `inout`, `isolated`, ...). The specifiers aren't part of the type: the decode call and
89
+ /// the conformance assertion need the bare type (`borrowing Point.decode(...)` isn't valid Swift), and
90
+ /// a decoded argument is a fresh owned value, so it can be passed under any ownership convention.
91
+ internal struct SpecifiedType {
92
+ /// The type without its specifiers.
93
+ let type: TypeSyntax
94
+ /// The specifiers written before the type, in source order.
95
+ let specifiers: [String]
96
+
97
+ init(_ type: TypeSyntax) {
98
+ if let attributed = type.as(AttributedTypeSyntax.self), !attributed.specifiers.isEmpty {
99
+ self.specifiers = attributed.specifiers.map(\.trimmedDescription)
100
+ self.type =
101
+ attributed.attributes.isEmpty
102
+ ? attributed.baseType : TypeSyntax(attributed.with(\.specifiers, []))
103
+ } else {
104
+ self.specifiers = []
105
+ self.type = type
106
+ }
107
+ }
108
+
109
+ /// The first specifier a decoded argument can't honor: `inout` (JavaScript can't observe a write
110
+ /// back) or `isolated` (the argument doesn't carry an isolation).
111
+ var unsupportedSpecifier: String? {
112
+ return specifiers.first { !ownershipSpecifiers.contains($0) }
113
+ }
114
+ }
115
+
116
+ /// The ownership specifiers a parameter may carry. They only change how the argument is passed, which
117
+ /// a fresh decoded value satisfies either way.
118
+ internal let ownershipSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
119
+
84
120
  /// The type wrapped by one optional layer (`T?`, `T!` or `Optional<T>`), or `nil` when the type isn't
85
121
  /// optional.
86
122
  internal func optionalWrappedType(_ type: TypeSyntax) -> TypeSyntax? {
@@ -330,17 +366,53 @@ internal func expressionType(_ type: String) -> String {
330
366
  }
331
367
 
332
368
  /// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
333
- /// as it appears after `try`. A free-form type (`Any`, `[Any]`, `[String: Any]`) can't conform to
334
- /// `JavaScriptDecodable`, so it decodes through the dedicated `JavaScriptValue.decodeAny…` entry point
335
- /// keyed to its shape; every other type decodes through its own static `decode`, spelled in expression
336
- /// position (`T!` rewritten to `T?`).
337
- internal func decodeCall(_ type: String, from valueExpression: String) -> String {
338
- if let method = freeFormDecodeMethod(for: type) {
339
- return "JavaScriptValue.\(method)(\(valueExpression), in: runtime)"
369
+ /// as it appears after `try`. A type that mentions `Any` can't conform to `JavaScriptDecodable`, so it
370
+ /// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names; every other
371
+ /// type decodes through its own static `decode`, spelled in expression position (`T!` rewritten to
372
+ /// `T?`).
373
+ internal func decodeCall(_ type: String, freeForm: FreeFormConversion?, from valueExpression: String) -> String {
374
+ if let freeForm {
375
+ return freeForm.decodeExpression(of: valueExpression, as: expressionType(type))
340
376
  }
341
377
  return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
342
378
  }
343
379
 
380
+ /// The encode expression for the native value `valueExpression` of a boundary type, as it appears
381
+ /// after `try`. The counterpart of `decodeCall`: a type that mentions `Any` encodes through the
382
+ /// `JavaScriptValue` free-form methods, and every other type through its own static `encode`.
383
+ internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String) -> String {
384
+ if let freeForm {
385
+ return freeForm.encodeExpression(of: valueExpression)
386
+ }
387
+ return "\(expressionType(type)).encode(\(valueExpression), in: runtime)"
388
+ }
389
+
390
+ // MARK: - Escaped names
391
+
392
+ /// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
393
+ /// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
394
+ /// name, a record key) is the bare name.
395
+ internal func withoutBackticks(_ name: String) -> String {
396
+ var name = Substring(name)
397
+ while name.first == "`" {
398
+ name.removeFirst()
399
+ }
400
+ while name.last == "`" {
401
+ name.removeLast()
402
+ }
403
+ return String(name)
404
+ }
405
+
406
+ /// A generated identifier built from a member's name, such as `countDescriptor` from `count`. The
407
+ /// affixes join the bare name, and an escaped name keeps the backticks around the whole result
408
+ /// (`` `class` `` gives `` `classDescriptor` ``), since the bare name may not be an identifier on
409
+ /// its own.
410
+ internal func derivedIdentifier(from name: String, prefix: String = "", suffix: String = "") -> String {
411
+ let bareName = withoutBackticks(name)
412
+ let identifier = prefix + bareName + suffix
413
+ return bareName == name ? identifier : "`\(identifier)`"
414
+ }
415
+
344
416
  // MARK: - @JS property collection
345
417
 
346
418
  /// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
@@ -365,8 +437,10 @@ internal func collectProperties(
365
437
  ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
366
438
  return JSProperty(
367
439
  swiftName: swiftName,
368
- jsName: jsNameOverride ?? swiftName,
440
+ jsName: jsNameOverride ?? withoutBackticks(swiftName),
369
441
  valueType: valueType,
442
+ // A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
443
+ freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
370
444
  isSettable: isVar && bindingIsSettable(binding)
371
445
  )
372
446
  }
@@ -95,7 +95,7 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
95
95
  members.append(defaultInit)
96
96
  }
97
97
  }
98
- if !existingInitLabels.contains(properties.map { $0.name }) {
98
+ if !existingInitLabels.contains(properties.map { $0.key }) {
99
99
  members.append(memberwiseInit(properties: properties))
100
100
  }
101
101
  members.append(fromJSObjectFactory(properties: properties))
@@ -143,62 +143,13 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
143
143
 
144
144
  // MARK: - Property model
145
145
 
146
- /**
147
- How a property whose type mentions `Any` converts on the JS-value paths. Such a type can't conform to
148
- the JS codable protocols, so the property converts through the `JavaScriptValue` free-form methods.
149
- */
150
- private enum FreeFormConversion {
151
- /// `Any`, `[Any]` or `[String: Any]`, wrapped in at most one optional: the dedicated methods for
152
- /// that shape, named by `decodeMethod` and `encodeMethod`. The optional is handled around them.
153
- case shape(decodeMethod: String, encodeMethod: String, isOptional: Bool, isBareAny: Bool)
154
- /// Any other type that mentions `Any`: the generic `decodeAny(_:as:in:)` and `encodeAny(_:in:)`.
155
- case generic
156
-
157
- /// Classifies a type that mentions `Any`; the caller checks `isConvertibleFreeFormType` first.
158
- init(type: TypeSyntax) {
159
- let wrapped = optionalWrappedType(type)
160
- switch freeFormShape(of: wrapped ?? type) {
161
- case .any:
162
- self = .shape(
163
- decodeMethod: "decodeAny",
164
- encodeMethod: "encodeAny",
165
- isOptional: wrapped != nil,
166
- isBareAny: true
167
- )
168
- case .array:
169
- self = .shape(
170
- decodeMethod: "decodeAnyArray",
171
- encodeMethod: "encodeAnyArray",
172
- isOptional: wrapped != nil,
173
- isBareAny: false
174
- )
175
- case .dictionary:
176
- self = .shape(
177
- decodeMethod: "decodeAnyDictionary",
178
- encodeMethod: "encodeAnyDictionary",
179
- isOptional: wrapped != nil,
180
- isBareAny: false
181
- )
182
- case nil:
183
- self = .generic
184
- }
185
- }
186
-
187
- /// True for `Any` itself, optional or not.
188
- var isBareAny: Bool {
189
- guard case .shape(_, _, _, let isBareAny) = self else {
190
- return false
191
- }
192
- return isBareAny
193
- }
194
- }
195
-
196
146
  /**
197
147
  A single record property discovered on the type, paired with everything the synthesized
198
- conversions need: its property name (also the JS key), its written type, and how the
199
- source may omit it.
148
+ conversions need: its property name, its written type, and how the source may omit it.
200
149
  */
201
150
  private struct RecordProperty {
151
+ /// The property name as written, backticks included for an escaped name (`` `default` ``), since
152
+ /// every identifier position in the generated code needs them.
202
153
  let name: String
203
154
  /// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
204
155
  /// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
@@ -222,6 +173,12 @@ private struct RecordProperty {
222
173
  var isRequired: Bool {
223
174
  return !hasDefault && !isOptional
224
175
  }
176
+
177
+ /// The key the property reads from and writes to, in both the JS object and the dictionary: the
178
+ /// name without escaping backticks.
179
+ var key: String {
180
+ return withoutBackticks(name)
181
+ }
225
182
  }
226
183
 
227
184
  /**
@@ -414,7 +371,7 @@ private func factoryBody(properties: [RecordProperty], readLines: [String]) -> S
414
371
  private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
415
372
  var lines: [String] = []
416
373
  for property in properties {
417
- let valueVar = "\(property.name)JSValue"
374
+ let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
418
375
  let exprType = expressionType(property.type)
419
376
  let decode: String
420
377
  switch property.freeForm {
@@ -426,10 +383,10 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
426
383
  case nil:
427
384
  decode = "try \(exprType).decode(\(valueVar), in: runtime)"
428
385
  }
429
- lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
386
+ lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
430
387
  if property.isRequired {
431
388
  lines.append(" guard !\(valueVar).isUndefined() else {")
432
- lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
389
+ lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
433
390
  lines.append(" }")
434
391
  lines.append(" let \(property.name) = \(decode)")
435
392
  } else if property.isOptional {
@@ -449,16 +406,16 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
449
406
  private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
450
407
  var lines: [String] = []
451
408
  for property in properties {
452
- let valueVar = "\(property.name)Value"
409
+ let valueVar = derivedIdentifier(from: property.name, suffix: "Value")
453
410
  let exprType = expressionType(property.type)
454
411
  let isBareAny = property.freeForm?.isBareAny == true
455
412
  let cast = isBareAny
456
413
  ? valueVar
457
414
  : "try \(exprType).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(exprType)"
458
- lines.append(" let \(valueVar) = dictionary[\"\(property.name)\"]")
415
+ lines.append(" let \(valueVar) = dictionary[\"\(property.key)\"]")
459
416
  if property.isRequired {
460
417
  lines.append(" guard let \(valueVar) else {")
461
- lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
418
+ lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
462
419
  lines.append(" }")
463
420
  lines.append(" let \(property.name) = \(cast)")
464
421
  } else if property.isOptional {
@@ -483,7 +440,7 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
483
440
  lines.append(" var dictionary: [String: Any] = [:]")
484
441
  }
485
442
  for property in properties {
486
- lines.append(" dictionary[\"\(property.name)\"] = self.\(property.name)")
443
+ lines.append(" dictionary[\"\(property.key)\"] = self.\(property.name)")
487
444
  }
488
445
  lines.append(" return dictionary")
489
446
  let body = lines.joined(separator: "\n")
@@ -537,7 +494,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
537
494
  case nil:
538
495
  encode = "try \(exprType).encode(self.\(property.name), in: runtime)"
539
496
  }
540
- lines.append(" object.setProperty(\"\(property.name)\", value: \(encode))")
497
+ lines.append(" object.setProperty(\"\(property.key)\", value: \(encode))")
541
498
  }
542
499
  lines.append(" return object")
543
500
  let body = lines.joined(separator: "\n")
@@ -567,7 +524,7 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
567
524
  }
568
525
  let labels = initDecl.signature.parameterClause.parameters.map { parameter in
569
526
  // `firstName` is the external label (or `_`); fall back to the internal name when omitted.
570
- let label = parameter.firstName.text
527
+ let label = withoutBackticks(parameter.firstName.text)
571
528
  return label == "_" ? "" : label
572
529
  }
573
530
  signatures.append(labels)
@@ -39,7 +39,7 @@ public struct SharedObjectMacro: MemberMacro {
39
39
  }
40
40
 
41
41
  let typeName = classDecl.name.text
42
- let jsName = jsNameArgument(of: node) ?? typeName
42
+ let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
43
43
 
44
44
  // `@JS func`s/`var`s and the `@JS init` are bound directly into the shared object's JS object by
45
45
  // the synthesized `_decorateSharedObject(prototype:)` / `_decorateSharedObject(constructor:)` / `_constructSharedObject`
@@ -111,8 +111,9 @@ internal func directionalConformanceAssertion(
111
111
  let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
112
112
  let call = "\(name)(\(arguments.joined(separator: ", ")))"
113
113
  let staticKeyword = isStatic ? "static " : ""
114
+ let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
114
115
  return """
115
- private \(raw: staticKeyword)func _assertTypesConformance_\(raw: name)() {
116
+ private \(raw: staticKeyword)func \(raw: peerName)() {
116
117
  \(raw: helper)
117
118
  \(raw: call)
118
119
  }
@@ -142,10 +143,11 @@ internal func typeConformanceAssertions(
142
143
  }
143
144
 
144
145
  /// The type to assert for a boundary type as written, or `nil` when nothing needs asserting: an
145
- /// exactly-free-form type (handled by the binding's `decodeAny…` reroute) and a known-conforming
146
- /// primitive both return `nil`; everything else is unwrapped of trailing optionals and asserted. An
147
- /// *optional* free-form (`[String: Any]?`) isn't rerouted and doesn't conform, so it's kept and
148
- /// asserted for a clean diagnostic. Shared with `@Event`.
146
+ /// exactly-free-form type (converted through the `JavaScriptValue` free-form methods) and a
147
+ /// known-conforming primitive both return `nil`; everything else is unwrapped of trailing optionals
148
+ /// and asserted. Shared with `@Event`, where an *optional* free-form type (`[String: Any]?`) is kept
149
+ /// and asserted for a clean diagnostic. `@JS` leaves out every type that mentions `Any` before it gets
150
+ /// here.
149
151
  internal func assertableBoundaryType(_ type: String) -> String? {
150
152
  guard !isFreeFormBoundaryType(type) else {
151
153
  return nil
@@ -251,18 +251,6 @@ private struct ViewProp {
251
251
  }
252
252
  }
253
253
 
254
- /// `name` without the backticks that escape it: `` `default` `` becomes `default`.
255
- private func withoutBackticks(_ name: String) -> String {
256
- var name = Substring(name)
257
- while name.first == "`" {
258
- name.removeFirst()
259
- }
260
- while name.last == "`" {
261
- name.removeLast()
262
- }
263
- return String(name)
264
- }
265
-
266
254
  private struct ViewPropsModel {
267
255
  /// Value props, in declaration order. The order is the bit order, so it is part of the ABI
268
256
  /// between a compiled view and the runtime that fills the mask.
@@ -158,7 +158,8 @@ struct ExportedEvent: Encodable, Equatable {
158
158
 
159
159
  /// A `@ExpoModule` type and its `@JS` surface.
160
160
  struct ExportedModule: Encodable, Equatable {
161
- /// The Swift class name.
161
+ /// The Swift class name. A module is always declared at file scope, since autolinking registers no
162
+ /// other.
162
163
  let name: String
163
164
 
164
165
  /// The JS module name: `@ExpoModule("Foo")` override, else the class name.
@@ -178,7 +179,9 @@ struct ExportedModule: Encodable, Equatable {
178
179
 
179
180
  /// One entry of `@ExpoModule(classes: [...])`.
180
181
  struct ExportedClass: Encodable, Equatable {
181
- /// The Swift name as spelled in the argument, e.g. `Database` or `Outer.Statement`.
182
+ /// The `name` of the matching `@SharedObject`, which is qualified when the class is nested
183
+ /// (`Outer.Statement`). The Swift name as spelled in the argument when the scan found no
184
+ /// `@SharedObject` with this name.
182
185
  let name: String
183
186
 
184
187
  /// The name of the module member JS reads the class from: the matching `@SharedObject`'s `jsName`.
@@ -189,10 +192,11 @@ struct ExportedClass: Encodable, Equatable {
189
192
 
190
193
  /// A `@SharedObject` type: a JS class with an optional `@JS init` constructor plus its `@JS` members.
191
194
  struct ExportedSharedObject: Encodable, Equatable {
192
- /// The Swift class name.
195
+ /// The Swift class name, qualified with the types it is nested in (`Outer.Player`).
193
196
  let name: String
194
197
 
195
- /// The JS class name: `@SharedObject("Foo")` override, else the class name.
198
+ /// The JS class name: `@SharedObject("Foo")` override, else the class name without the enclosing
199
+ /// types.
196
200
  let jsName: String
197
201
 
198
202
  /// The `@JS init` parameters, or `nil` when there's none. A shared object has at most one.
@@ -207,7 +211,8 @@ struct ExportedSharedObject: Encodable, Equatable {
207
211
 
208
212
  /// A `@Record` type and its properties (data only: no functions, accessors, or constructor).
209
213
  struct ExportedRecord: Encodable, Equatable {
210
- /// The Swift type name (struct or class).
214
+ /// The Swift type name (struct or class), qualified with the types it is nested in
215
+ /// (`Outer.Options`).
211
216
  let name: String
212
217
 
213
218
  let properties: [ExportedRecordProperty]
@@ -247,8 +252,9 @@ struct ExportedEnumCase: Encodable, Equatable {
247
252
  /// Detected by conformance, not by a macro attribute, because core converts it through its
248
253
  /// `RawRepresentable`/`Enumerable` conformance (`Coding/…+Enumerable`) with no macro involved.
249
254
  struct ExportedEnum: Encodable, Equatable {
250
- /// The Swift enum name. No `jsName`: an enum carries no macro to spell an override on, and it
251
- /// reaches JS as its raw values, not under a bound name.
255
+ /// The Swift enum name, qualified with the types it is nested in (`Options.Encoding`). No `jsName`:
256
+ /// an enum carries no macro to spell an override on, and it reaches JS as its raw values, not under
257
+ /// a bound name.
252
258
  let name: String
253
259
 
254
260
  /// The raw value type as written (`String`, `Int`, …), or `nil` for a bare `Enumerable` conformance
@@ -296,8 +302,9 @@ struct ExportedUnionMember: Encodable, Equatable {
296
302
  /// `Double`, two compatible records) the earlier case wins. Reordering them describes a different
297
303
  /// union than the one the module runs.
298
304
  struct ExportedUnion: Encodable, Equatable {
299
- /// The Swift enum name. No `jsName`: `@Union` takes no arguments, and a union reaches JS as its
300
- /// payload types rather than under a bound name.
305
+ /// The Swift enum name, qualified with the types it is nested in (`Options.Value`). No `jsName`:
306
+ /// `@Union` takes no arguments, and a union reaches JS as its payload types rather than under a bound
307
+ /// name.
301
308
  let name: String
302
309
 
303
310
  let members: [ExportedUnionMember]
@@ -320,8 +327,9 @@ struct ExportedSurface: Encodable, Equatable {
320
327
  /// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
321
328
  /// and change for different reasons. Version 2 added `events` to modules and shared objects; version 3
322
329
  /// added `enums`; version 4 added `unions`; version 5 resolves refs, adding `refKind` and correcting
323
- /// an enum ref's `typeof`; version 6 added `classes` to modules.
324
- let scanExportsSchemaVersion = 6
330
+ /// an enum ref's `typeof`; version 6 added `classes` to modules; version 7 reports nested types under
331
+ /// qualified names and gives a resolved ref the name of its declaration.
332
+ let scanExportsSchemaVersion = 7
325
333
 
326
334
  /// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
327
335
  /// `ScanModulesResult` (different consumer: TS generation vs. autolinking).