expo-modules-macros 0.17.0 → 0.18.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.
Binary file
@@ -68,29 +68,20 @@ internal struct JSClosureType {
68
68
  let isVariadic: Bool
69
69
 
70
70
  init(_ element: TupleTypeElementSyntax) {
71
- if let attributed = element.type.as(AttributedTypeSyntax.self), !attributed.specifiers.isEmpty {
72
- self.specifiers = attributed.specifiers.map(\.trimmedDescription)
73
- self.type =
74
- attributed.attributes.isEmpty
75
- ? attributed.baseType : TypeSyntax(attributed.with(\.specifiers, []))
76
- } else {
77
- self.specifiers = []
78
- self.type = element.type
79
- }
71
+ let specified = SpecifiedType(element.type)
72
+ self.specifiers = specified.specifiers
73
+ self.type = specified.type
80
74
  self.isVariadic = element.ellipsis != nil
81
75
  }
82
76
 
83
- /// The first specifier the wrapper can't support, such as `inout` or `isolated`.
77
+ /// The first specifier the wrapper can't support, such as `inout` or `isolated`. The wrapper passes
78
+ /// each parameter on to the callback, so an ownership convention doesn't change what it does.
84
79
  var unsupportedSpecifier: String? {
85
- return specifiers.first { !supportedParameterSpecifiers.contains($0) }
80
+ return specifiers.first { !ownershipSpecifiers.contains($0) }
86
81
  }
87
82
  }
88
83
  }
89
84
 
90
- /// The ownership specifiers a closure parameter may carry. The wrapper passes each parameter on to the
91
- /// callback, so the ownership convention doesn't change what it does.
92
- private let supportedParameterSpecifiers: Set<String> = ["borrowing", "consuming", "sending", "__shared", "__owned"]
93
-
94
85
  /// The statements that bind `name` to the native value of a `@JS` argument read from
95
86
  /// `valueExpression` (a `JavaScriptUnownedValue`). A closure gets a wrapper around the JS function,
96
87
  /// and every other type decodes through `decodeCall`.
@@ -99,6 +90,8 @@ internal func argumentDecodeStatements(
99
90
  into name: String,
100
91
  from valueExpression: String
101
92
  ) -> [String] {
93
+ // A specifier such as `borrowing` belongs to the parameter, not to the type that decodes.
94
+ let type = SpecifiedType(type).type
102
95
  guard let closure = JSClosureType(type) else {
103
96
  return ["let \(name) = try \(decodeCall(type.trimmedDescription, from: valueExpression))"]
104
97
  }
@@ -24,7 +24,7 @@ internal struct JSFunction {
24
24
 
25
25
  init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
26
26
  self.swiftName = funcDecl.name.text
27
- self.jsName = jsNameArgument(of: attribute) ?? funcDecl.name.text
27
+ self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
28
28
  self.parameters = Array(funcDecl.signature.parameterClause.parameters)
29
29
 
30
30
  let declaredReturnType = funcDecl.signature.returnClause?.type
@@ -309,7 +309,7 @@ internal struct JSProperty {
309
309
  /// function bindings: a module captures `self` strong, a shared object captures nothing of the
310
310
  /// instance. Getter and setter are gated independently.
311
311
  func decorateStatements(object: String, receiver: Receiver) -> String {
312
- let descriptorName = "\(swiftName)Descriptor"
312
+ let descriptorName = derivedIdentifier(from: swiftName, suffix: "Descriptor")
313
313
  let callee = receiver.callee
314
314
  // A shared object's accessors unwrap the JS `this` into `_self` before reading/writing; a module
315
315
  // reads `self` directly. The unwrap leads each accessor body. Property accessors are synchronous, so
@@ -91,9 +91,10 @@ extension EventMacro: PeerMacro {
91
91
  ? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
92
92
  : "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
93
93
  let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
94
+ let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
94
95
  return [
95
96
  """
96
- private func _assertTypesConformance_\(raw: name)() {
97
+ private func \(raw: peerName)() {
97
98
  \(raw: helper)
98
99
  \(raw: call)
99
100
  }
@@ -172,7 +173,7 @@ private func validatedEvent(
172
173
  let swiftName = identifier.identifier.text
173
174
  return EventMember(
174
175
  swiftName: swiftName,
175
- jsName: jsNameArgument(of: node) ?? defaultEventName(for: swiftName),
176
+ jsName: jsNameArgument(of: node) ?? defaultEventName(for: withoutBackticks(swiftName)),
176
177
  payloadType: functionType.parameters.first?.type.trimmedDescription,
177
178
  isSync: boolArgument(of: node, label: "sync") == true
178
179
  )
@@ -45,7 +45,7 @@ public struct ExpoModuleMacro: MemberMacro {
45
45
  // class name) and emitted as a non-optional `_jsName` static below — so the
46
46
  // macro no longer emits a `Name(…)` DSL entry. Core reads the static for the module name,
47
47
  // ahead of its type-name fallback (which then only applies to non-macro DSL modules).
48
- let moduleName = jsNameArgument(of: node) ?? classDecl.name.text
48
+ let moduleName = jsNameArgument(of: node) ?? withoutBackticks(classDecl.name.text)
49
49
  var entries: [String] = []
50
50
 
51
51
  // `@JS func`s (sync and async) and `@JS var`s are bound directly into the JS object by the
@@ -31,6 +31,7 @@ public struct JSMacro: PeerMacro {
31
31
  providingPeersOf declaration: some DeclSyntaxProtocol,
32
32
  in context: some MacroExpansionContext
33
33
  ) throws -> [DeclSyntax] {
34
+ diagnoseParameterSpecifiers(in: declaration, in: context)
34
35
  diagnoseFreeFormTypes(in: declaration, in: context)
35
36
  diagnoseClosureTypes(in: declaration, in: context)
36
37
  diagnoseConcurrentOption(of: node, on: declaration, in: context)
@@ -114,6 +115,39 @@ private func insertAsyncFixIt(for funcDecl: FunctionDeclSyntax) -> FixIt {
114
115
  )
115
116
  }
116
117
 
118
+ /// Reports a function or constructor parameter whose specifier a decoded argument can't honor: an
119
+ /// `inout` parameter expects its writes to reach the caller, and an `isolated` one expects an actor,
120
+ /// while a `@JS` argument is a fresh value decoded from JavaScript. Ownership specifiers (`borrowing`,
121
+ /// `consuming`, `sending`) are fine: the binding passes the decoded value under whichever convention
122
+ /// the parameter asks for. The specifier is left off the decode and the assertion (see
123
+ /// `SpecifiedType`); a parameter reported here is also left out of the assertion.
124
+ private func diagnoseParameterSpecifiers(
125
+ in declaration: some DeclSyntaxProtocol,
126
+ in context: some MacroExpansionContext
127
+ ) {
128
+ let parameters: FunctionParameterListSyntax
129
+ if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
130
+ parameters = funcDecl.signature.parameterClause.parameters
131
+ } else if let initDecl = declaration.as(InitializerDeclSyntax.self) {
132
+ parameters = initDecl.signature.parameterClause.parameters
133
+ } else {
134
+ return
135
+ }
136
+ for parameter in parameters {
137
+ guard let specifier = SpecifiedType(parameter.type).unsupportedSpecifier else {
138
+ continue
139
+ }
140
+ context.diagnose(
141
+ Diagnostic(
142
+ node: parameter.type,
143
+ message: JSDiagnosticMessage(
144
+ "A @JS parameter can't be '\(specifier)': its argument is a value decoded from JavaScript, which can't be written back or carry an isolation.",
145
+ id: "js-parameter-specifier",
146
+ severity: .error
147
+ )))
148
+ }
149
+ }
150
+
117
151
  /// Emits the free-form (`Any` / `[Any]` / `[String: Any]`) diagnostics for a `@JS` declaration.
118
152
  ///
119
153
  /// A free-form type is only supported crossing the boundary as an **argument**, decoded through
@@ -172,7 +206,8 @@ private func warnFreeFormArguments(
172
206
  in context: some MacroExpansionContext
173
207
  ) {
174
208
  for parameter in parameters {
175
- let type = parameter.type.trimmedDescription
209
+ // Matched on the bare type: `borrowing [String: Any]` decodes as free-form too.
210
+ let type = SpecifiedType(parameter.type).type.trimmedDescription
176
211
  guard let suggested = typedFreeFormReplacement(for: type) else {
177
212
  continue
178
213
  }
@@ -265,9 +300,15 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
265
300
  var decodableTypes: [String] = []
266
301
  var callbackArgumentTypes: [String] = []
267
302
  for parameter in funcDecl.signature.parameterClause.parameters {
268
- guard let closure = JSClosureType(parameter.type) else {
269
- if !containsFunctionType(parameter.type) {
270
- decodableTypes.append(parameter.type.trimmedDescription)
303
+ // The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
304
+ // is reported by `diagnoseParameterSpecifiers` and not asserted.
305
+ let specified = SpecifiedType(parameter.type)
306
+ guard specified.unsupportedSpecifier == nil else {
307
+ continue
308
+ }
309
+ guard let closure = JSClosureType(specified.type) else {
310
+ if !containsFunctionType(specified.type) {
311
+ decodableTypes.append(specified.type.trimmedDescription)
271
312
  }
272
313
  continue
273
314
  }
@@ -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? {
@@ -341,6 +377,32 @@ internal func decodeCall(_ type: String, from valueExpression: String) -> String
341
377
  return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
342
378
  }
343
379
 
380
+ // MARK: - Escaped names
381
+
382
+ /// `name` without the backticks that escape it: `` `default` `` becomes `default`. Swift needs the
383
+ /// backticks in every identifier position, but a name JavaScript sees (a JS member name, an event
384
+ /// name, a record key) is the bare name.
385
+ internal func withoutBackticks(_ name: String) -> String {
386
+ var name = Substring(name)
387
+ while name.first == "`" {
388
+ name.removeFirst()
389
+ }
390
+ while name.last == "`" {
391
+ name.removeLast()
392
+ }
393
+ return String(name)
394
+ }
395
+
396
+ /// A generated identifier built from a member's name, such as `countDescriptor` from `count`. The
397
+ /// affixes join the bare name, and an escaped name keeps the backticks around the whole result
398
+ /// (`` `class` `` gives `` `classDescriptor` ``), since the bare name may not be an identifier on
399
+ /// its own.
400
+ internal func derivedIdentifier(from name: String, prefix: String = "", suffix: String = "") -> String {
401
+ let bareName = withoutBackticks(name)
402
+ let identifier = prefix + bareName + suffix
403
+ return bareName == name ? identifier : "`\(identifier)`"
404
+ }
405
+
344
406
  // MARK: - @JS property collection
345
407
 
346
408
  /// Collects the `@JS var` bindings of a declaration into `JSProperty` values for direct JSI binding.
@@ -365,7 +427,7 @@ internal func collectProperties(
365
427
  ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
366
428
  return JSProperty(
367
429
  swiftName: swiftName,
368
- jsName: jsNameOverride ?? swiftName,
430
+ jsName: jsNameOverride ?? withoutBackticks(swiftName),
369
431
  valueType: valueType,
370
432
  isSettable: isVar && bindingIsSettable(binding)
371
433
  )
@@ -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))
@@ -195,10 +195,11 @@ private enum FreeFormConversion {
195
195
 
196
196
  /**
197
197
  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.
198
+ conversions need: its property name, its written type, and how the source may omit it.
200
199
  */
201
200
  private struct RecordProperty {
201
+ /// The property name as written, backticks included for an escaped name (`` `default` ``), since
202
+ /// every identifier position in the generated code needs them.
202
203
  let name: String
203
204
  /// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
204
205
  /// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
@@ -222,6 +223,12 @@ private struct RecordProperty {
222
223
  var isRequired: Bool {
223
224
  return !hasDefault && !isOptional
224
225
  }
226
+
227
+ /// The key the property reads from and writes to, in both the JS object and the dictionary: the
228
+ /// name without escaping backticks.
229
+ var key: String {
230
+ return withoutBackticks(name)
231
+ }
225
232
  }
226
233
 
227
234
  /**
@@ -414,7 +421,7 @@ private func factoryBody(properties: [RecordProperty], readLines: [String]) -> S
414
421
  private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
415
422
  var lines: [String] = []
416
423
  for property in properties {
417
- let valueVar = "\(property.name)JSValue"
424
+ let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
418
425
  let exprType = expressionType(property.type)
419
426
  let decode: String
420
427
  switch property.freeForm {
@@ -426,10 +433,10 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
426
433
  case nil:
427
434
  decode = "try \(exprType).decode(\(valueVar), in: runtime)"
428
435
  }
429
- lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
436
+ lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
430
437
  if property.isRequired {
431
438
  lines.append(" guard !\(valueVar).isUndefined() else {")
432
- lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
439
+ lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
433
440
  lines.append(" }")
434
441
  lines.append(" let \(property.name) = \(decode)")
435
442
  } else if property.isOptional {
@@ -449,16 +456,16 @@ private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
449
456
  private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
450
457
  var lines: [String] = []
451
458
  for property in properties {
452
- let valueVar = "\(property.name)Value"
459
+ let valueVar = derivedIdentifier(from: property.name, suffix: "Value")
453
460
  let exprType = expressionType(property.type)
454
461
  let isBareAny = property.freeForm?.isBareAny == true
455
462
  let cast = isBareAny
456
463
  ? valueVar
457
464
  : "try \(exprType).getDynamicType().cast(\(valueVar), appContext: appContext) as! \(exprType)"
458
- lines.append(" let \(valueVar) = dictionary[\"\(property.name)\"]")
465
+ lines.append(" let \(valueVar) = dictionary[\"\(property.key)\"]")
459
466
  if property.isRequired {
460
467
  lines.append(" guard let \(valueVar) else {")
461
- lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
468
+ lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
462
469
  lines.append(" }")
463
470
  lines.append(" let \(property.name) = \(cast)")
464
471
  } else if property.isOptional {
@@ -483,7 +490,7 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
483
490
  lines.append(" var dictionary: [String: Any] = [:]")
484
491
  }
485
492
  for property in properties {
486
- lines.append(" dictionary[\"\(property.name)\"] = self.\(property.name)")
493
+ lines.append(" dictionary[\"\(property.key)\"] = self.\(property.name)")
487
494
  }
488
495
  lines.append(" return dictionary")
489
496
  let body = lines.joined(separator: "\n")
@@ -537,7 +544,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
537
544
  case nil:
538
545
  encode = "try \(exprType).encode(self.\(property.name), in: runtime)"
539
546
  }
540
- lines.append(" object.setProperty(\"\(property.name)\", value: \(encode))")
547
+ lines.append(" object.setProperty(\"\(property.key)\", value: \(encode))")
541
548
  }
542
549
  lines.append(" return object")
543
550
  let body = lines.joined(separator: "\n")
@@ -567,7 +574,7 @@ private func initializerParameterLabels(of declaration: some DeclGroupSyntax) ->
567
574
  }
568
575
  let labels = initDecl.signature.parameterClause.parameters.map { parameter in
569
576
  // `firstName` is the external label (or `_`); fall back to the internal name when omitted.
570
- let label = parameter.firstName.text
577
+ let label = withoutBackticks(parameter.firstName.text)
571
578
  return label == "_" ? "" : label
572
579
  }
573
580
  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
  }
@@ -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.
@@ -20,7 +20,7 @@ final class SurfaceVisitor: SyntaxVisitor {
20
20
 
21
21
  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
22
22
  if isTopLevel(node) {
23
- classify(name: node.name.text, attributes: node.attributes, members: node.memberBlock.members)
23
+ classify(name: node.name.identifierName, attributes: node.attributes, members: node.memberBlock.members)
24
24
  }
25
25
  // The member walk reads the body itself; nested types aren't part of this surface (matching
26
26
  // `DetectionVisitor`'s top-level-only scope), so there's no reason to descend.
@@ -29,7 +29,7 @@ final class SurfaceVisitor: SyntaxVisitor {
29
29
 
30
30
  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
31
31
  if isTopLevel(node) {
32
- classify(name: node.name.text, attributes: node.attributes, members: node.memberBlock.members)
32
+ classify(name: node.name.identifierName, attributes: node.attributes, members: node.memberBlock.members)
33
33
  }
34
34
  return .skipChildren
35
35
  }
@@ -48,7 +48,7 @@ final class SurfaceVisitor: SyntaxVisitor {
48
48
  if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
49
49
  unions.append(
50
50
  ExportedUnion(
51
- name: node.name.text,
51
+ name: node.name.identifierName,
52
52
  members: collectUnionMembers(node.memberBlock.members),
53
53
  file: file
54
54
  ))
@@ -56,7 +56,7 @@ final class SurfaceVisitor: SyntaxVisitor {
56
56
  let rawType = rawValueType(of: node.inheritanceClause)
57
57
  enums.append(
58
58
  ExportedEnum(
59
- name: node.name.text,
59
+ name: node.name.identifierName,
60
60
  rawType: rawType,
61
61
  cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
62
62
  file: file
@@ -178,7 +178,7 @@ final class SurfaceVisitor: SyntaxVisitor {
178
178
  functionType.parameters.count <= 1 else {
179
179
  continue
180
180
  }
181
- let name = ident.identifier.text
181
+ let name = ident.identifier.identifierName
182
182
  result.append(
183
183
  ExportedEvent(
184
184
  name: name,
@@ -196,8 +196,8 @@ final class SurfaceVisitor: SyntaxVisitor {
196
196
  let effects = funcDecl.signature.effectSpecifiers
197
197
  let returnType = funcDecl.signature.returnClause?.type
198
198
  return ExportedFunction(
199
- name: funcDecl.name.text,
200
- jsName: stringArgument(of: attribute) ?? funcDecl.name.text,
199
+ name: funcDecl.name.identifierName,
200
+ jsName: stringArgument(of: attribute) ?? funcDecl.name.identifierName,
201
201
  parameters: parameters(of: funcDecl.signature.parameterClause),
202
202
  returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
203
203
  isAsync: effects?.asyncSpecifier != nil,
@@ -219,7 +219,7 @@ final class SurfaceVisitor: SyntaxVisitor {
219
219
  guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
220
220
  continue
221
221
  }
222
- let name = ident.identifier.text
222
+ let name = ident.identifier.identifierName
223
223
  let type = valueTypeNode(annotation: binding.typeAnnotation?.type, initializer: binding.initializer?.value)
224
224
  result.append(
225
225
  ExportedProperty(
@@ -284,7 +284,7 @@ final class SurfaceVisitor: SyntaxVisitor {
284
284
  }
285
285
  properties.append(
286
286
  ExportedRecordProperty(
287
- name: ident.identifier.text,
287
+ name: ident.identifier.identifierName,
288
288
  type: type,
289
289
  isOptional: annotation.map { isOptionalType($0) } ?? false,
290
290
  hasDefault: binding.initializer != nil
@@ -312,7 +312,7 @@ final class SurfaceVisitor: SyntaxVisitor {
312
312
  continue
313
313
  }
314
314
  for element in caseDecl.elements where element.parameterClause == nil {
315
- let name = element.name.text
315
+ let name = element.name.identifierName
316
316
  cases.append(
317
317
  ExportedEnumCase(
318
318
  name: name,
@@ -343,7 +343,7 @@ final class SurfaceVisitor: SyntaxVisitor {
343
343
  continue
344
344
  }
345
345
  result.append(
346
- ExportedUnionMember(name: element.name.text, type: typeNode(from: parameter.type)))
346
+ ExportedUnionMember(name: element.name.identifierName, type: typeNode(from: parameter.type)))
347
347
  }
348
348
  }
349
349
  return result
@@ -353,8 +353,8 @@ final class SurfaceVisitor: SyntaxVisitor {
353
353
  /// first), and `optional` when it has a default value or an optional type.
354
354
  private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
355
355
  clause.parameters.map { parameter in
356
- let label = parameter.firstName.text
357
- let name = parameter.secondName?.text ?? label
356
+ let label = parameter.firstName.identifierName
357
+ let name = parameter.secondName?.identifierName ?? label
358
358
  return ExportedParameter(
359
359
  label: label,
360
360
  name: name,
@@ -376,6 +376,15 @@ final class SurfaceVisitor: SyntaxVisitor {
376
376
 
377
377
  // MARK: - Syntactic helpers (shared spelling with the macros)
378
378
 
379
+ extension TokenSyntax {
380
+ /// The name a token spells, without the backticks Swift requires around a reserved word used as
381
+ /// a name: `` `default` `` reads as `default`. Tokens that aren't identifiers, such as the `_`
382
+ /// wildcard label, read as written.
383
+ var identifierName: String {
384
+ identifier?.name ?? text
385
+ }
386
+ }
387
+
379
388
  /// True when the modifiers make a member type-level (`static` or `class`).
380
389
  // MARK: - `@Event` helpers
381
390
 
@@ -204,7 +204,7 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
204
204
  }
205
205
  // A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
206
206
  if let identifier = type.as(IdentifierTypeSyntax.self) {
207
- return nominalNode(name: identifier.name.text, generics: identifier.genericArgumentClause)
207
+ return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
208
208
  }
209
209
  // A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling.
210
210
  if let member = type.as(MemberTypeSyntax.self) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-macros",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Swift macro plugin for Expo modules",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "typescript": "^5.4.0"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@expo/modules-macros-win32-arm64": "0.17.0",
27
- "@expo/modules-macros-win32-x64": "0.17.0"
26
+ "@expo/modules-macros-win32-arm64": "0.18.0",
27
+ "@expo/modules-macros-win32-x64": "0.18.0"
28
28
  }
29
29
  }