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.
@@ -5,65 +5,88 @@
5
5
  /// enum, or nothing at all. Resolution is therefore a second pass over the collected surface, run once
6
6
  /// every file has been walked.
7
7
  ///
8
- /// Two things change on a resolved ref:
8
+ /// A name is looked up the way Swift looks it up: from the type the use site is written in, outward to
9
+ /// file scope. In `Options`, `Encoding` names `Options.Encoding` when the scan declares one, else a
10
+ /// file-scope `Encoding`.
11
+ ///
12
+ /// Three things change on a resolved ref:
13
+ /// - `name` becomes the declaration's qualified name, so a consumer can match it to the declaration
14
+ /// without repeating the scoped lookup.
9
15
  /// - `refKind` names which kind declares it, so a consumer doesn't repeat this lookup.
10
16
  /// - the `typeof` category is corrected for an enum, which crosses as its raw value rather than as an
11
17
  /// object. That correction is the reason this can't be left to the consumer: the scanner is the only
12
18
  /// side that knows the enum's `rawType`.
13
19
  ///
14
- /// A name the scan never declared keeps `refKind: nil` and its `object` category. Not an error: it is
15
- /// a platform convertible (`CGPoint`, `URL`) or a type from another module, and the consumer's own
16
- /// catalog decides.
20
+ /// A name the scan never declared keeps its spelling, `refKind: nil`, and its `object` category. Not
21
+ /// an error: it is a platform convertible (`CGPoint`, `URL`) or a type from another module, and the
22
+ /// consumer's own catalog decides.
17
23
 
18
24
  /// The declared names of a scanned surface, each mapped to how it should be reported at a use site.
19
25
  /// Built once per scan and used for every ref lookup.
20
26
  struct RefIndex {
21
- /// `name` -> the kind declaring it, plus the JS category a ref to it crosses as.
22
- private var entries: [String: (kind: RefKind, jsType: JSType)] = [:]
27
+ /// The declaration a ref resolves to.
28
+ struct Entry: Equatable {
29
+ /// The declaration's qualified name.
30
+ let name: String
31
+ let kind: RefKind
32
+ /// The JS category a ref to the declaration crosses as.
33
+ let jsType: JSType
34
+ /// A `@SharedObject`'s JS name, `nil` for every other kind.
35
+ var jsName: String? = nil
36
+ }
23
37
 
24
- /// A `@SharedObject`'s Swift name -> its JS name.
25
- private var sharedObjectJSNames: [String: String] = [:]
38
+ /// A declaration's qualified name -> its entry.
39
+ private var entries: [String: Entry] = [:]
26
40
 
27
41
  init(surface: ExportedSurface) {
28
42
  for record in surface.records {
29
- entries[record.name] = (.record, .object)
43
+ entries[record.name] = Entry(name: record.name, kind: .record, jsType: .object)
30
44
  }
31
45
  for sharedObject in surface.sharedObjects {
32
- entries[sharedObject.name] = (.sharedObject, .object)
33
- sharedObjectJSNames[sharedObject.name] = sharedObject.jsName
46
+ entries[sharedObject.name] = Entry(
47
+ name: sharedObject.name, kind: .sharedObject, jsType: .object, jsName: sharedObject.jsName)
34
48
  }
35
49
  for union in surface.unions {
36
50
  // A union crosses as whichever alternative matched, so its category is only meaningful per
37
51
  // member. `object` is the honest coarse answer; the consumer reads `members` for the real shape.
38
- entries[union.name] = (.union, .object)
52
+ entries[union.name] = Entry(name: union.name, kind: .union, jsType: .object)
39
53
  }
40
54
  for enumeration in surface.enums {
41
- entries[enumeration.name] = (.enum, jsType(ofRawType: enumeration.rawType))
55
+ entries[enumeration.name] = Entry(
56
+ name: enumeration.name, kind: .enum, jsType: jsType(ofRawType: enumeration.rawType))
42
57
  }
43
58
  }
44
59
 
45
- /// How a ref to `name` should be reported, or `nil` when the scan declares no such type.
46
- func lookup(_ name: String) -> (kind: RefKind, jsType: JSType)? {
47
- lookupQualified(name, in: entries)
48
- }
49
-
50
- /// The JS name of the `@SharedObject` declared as `name`, or `nil` when the scan declares none.
51
- func sharedObjectJSName(_ name: String) -> String? {
52
- lookupQualified(name, in: sharedObjectJSNames)
53
- }
54
- }
55
-
56
- /// The value for `name` in `table`, falling back to its trailing component. A qualified use site
57
- /// (`Media.Status`) names the same type a bare declaration did, which is how the conformance and
58
- /// raw-type checks already match names.
59
- private func lookupQualified<Value>(_ name: String, in table: [String: Value]) -> Value? {
60
- if let value = table[name] {
61
- return value
62
- }
63
- guard let trailing = name.split(separator: ".").last.map(String.init), trailing != name else {
60
+ /// The declaration a ref to `name` written inside the type `scope` resolves to, or `nil` when the
61
+ /// scan declares no such type. `scope` is the qualified name of the type the use site is written
62
+ /// in.
63
+ ///
64
+ /// The name is first looked up in `scope` and then in each enclosing scope out to file scope, so the
65
+ /// innermost declaration wins, as in Swift. A leading `Self.` names `scope` itself, so it is dropped
66
+ /// before the lookup. When that finds nothing, the leading components of a qualified spelling are
67
+ /// dropped one at a time (`Media.Status` -> `Status`): a use site may qualify a name with a module
68
+ /// the scan doesn't see, which is how the conformance and raw-type checks already match names.
69
+ func lookup(_ name: String, in scope: String) -> Entry? {
70
+ let name = name.hasPrefix("Self.") ? String(name.dropFirst("Self.".count)) : name
71
+ var components = scope.split(separator: ".").map(String.init)
72
+ while true {
73
+ if let entry = entries[(components + [name]).joined(separator: ".")] {
74
+ return entry
75
+ }
76
+ if components.isEmpty {
77
+ break
78
+ }
79
+ components.removeLast()
80
+ }
81
+ var suffix = name.split(separator: ".").map(String.init).dropFirst()
82
+ while !suffix.isEmpty {
83
+ if let entry = entries[suffix.joined(separator: ".")] {
84
+ return entry
85
+ }
86
+ suffix = suffix.dropFirst()
87
+ }
64
88
  return nil
65
89
  }
66
- return table[trailing]
67
90
  }
68
91
 
69
92
  /// The JS category a raw-value enum crosses as: its raw value's. A bare `Enumerable` conformance with
@@ -76,32 +99,35 @@ private func jsType(ofRawType rawType: TypeNode?) -> JSType {
76
99
  }
77
100
 
78
101
  extension TypeNode {
79
- /// This node with every `.ref` in it resolved against `index`, recursively. Returns an unchanged
80
- /// node when nothing in it resolves.
81
- func resolvingRefs(using index: RefIndex) -> TypeNode {
102
+ /// This node with every `.ref` in it resolved against `index` from the type `scope`, recursively.
103
+ /// Returns an unchanged node when nothing in it resolves.
104
+ func resolvingRefs(using index: RefIndex, in scope: String) -> TypeNode {
82
105
  switch self {
83
106
  case .primitive, .unknown:
84
107
  return self
85
108
  case .ref(let name, _, _):
86
- guard let entry = index.lookup(name) else {
109
+ guard let entry = index.lookup(name, in: scope) else {
87
110
  return self
88
111
  }
89
112
  // Only an enum crosses as something other than an object, so only it carries an override.
90
113
  return .ref(
91
- name: name, refKind: entry.kind, jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
114
+ name: entry.name,
115
+ refKind: entry.kind,
116
+ jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
92
117
  case .optional(let wrapped):
93
- return .optional(wrapped: wrapped.resolvingRefs(using: index))
118
+ return .optional(wrapped: wrapped.resolvingRefs(using: index, in: scope))
94
119
  case .array(let element):
95
- return .array(element: element.resolvingRefs(using: index))
120
+ return .array(element: element.resolvingRefs(using: index, in: scope))
96
121
  case .dictionary(let key, let value):
97
122
  return .dictionary(
98
- key: key.resolvingRefs(using: index), value: value.resolvingRefs(using: index))
123
+ key: key.resolvingRefs(using: index, in: scope),
124
+ value: value.resolvingRefs(using: index, in: scope))
99
125
  case .promise(let value):
100
- return .promise(value: value.resolvingRefs(using: index))
126
+ return .promise(value: value.resolvingRefs(using: index, in: scope))
101
127
  case .function(let parameters, let returns, let isAsync, let isThrowing):
102
128
  return .function(
103
- parameters: parameters.map { $0.resolvingRefs(using: index) },
104
- returns: returns?.resolvingRefs(using: index),
129
+ parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
130
+ returns: returns?.resolvingRefs(using: index, in: scope),
105
131
  isAsync: isAsync,
106
132
  isThrowing: isThrowing
107
133
  )
@@ -113,9 +139,10 @@ extension ExportedSurface {
113
139
  /// This surface with every `.ref` in every reported type resolved against its own declarations. The
114
140
  /// second pass `scanExports` runs once the walk is done.
115
141
  ///
116
- /// Only the boundary types are rewritten; names, flags, and ordering are untouched. The declared
117
- /// types (a record's own name, a union's members) are not themselves refs, so they carry no
118
- /// `refKind`: it is the *use sites* that gain one.
142
+ /// Each type's refs are looked up from that type, so a ref resolves to a type nested in it before
143
+ /// one at file scope. Only the boundary types are rewritten; names, flags, and ordering are
144
+ /// untouched. The declared types (a record's own name, a union's members) are not themselves refs,
145
+ /// so they carry no `refKind`: it is the *use sites* that gain one.
119
146
  func resolvingRefs() -> ExportedSurface {
120
147
  let index = RefIndex(surface: self)
121
148
  return ExportedSurface(
@@ -123,11 +150,15 @@ extension ExportedSurface {
123
150
  ExportedModule(
124
151
  name: module.name,
125
152
  jsName: module.jsName,
126
- functions: module.functions.map { $0.resolvingRefs(using: index) },
127
- properties: module.properties.map { $0.resolvingRefs(using: index) },
128
- events: module.events.map { $0.resolvingRefs(using: index) },
129
- classes: module.classes.map {
130
- ExportedClass(name: $0.name, jsName: index.sharedObjectJSName($0.name))
153
+ functions: module.functions.map { $0.resolvingRefs(using: index, in: module.name) },
154
+ properties: module.properties.map { $0.resolvingRefs(using: index, in: module.name) },
155
+ events: module.events.map { $0.resolvingRefs(using: index, in: module.name) },
156
+ classes: module.classes.map { exportedClass in
157
+ guard let entry = index.lookup(exportedClass.name, in: module.name),
158
+ entry.kind == .sharedObject else {
159
+ return exportedClass
160
+ }
161
+ return ExportedClass(name: entry.name, jsName: entry.jsName)
131
162
  },
132
163
  file: module.file
133
164
  )
@@ -136,10 +167,12 @@ extension ExportedSurface {
136
167
  ExportedSharedObject(
137
168
  name: sharedObject.name,
138
169
  jsName: sharedObject.jsName,
139
- constructorParameters: sharedObject.constructorParameters?.map { $0.resolvingRefs(using: index) },
140
- functions: sharedObject.functions.map { $0.resolvingRefs(using: index) },
141
- properties: sharedObject.properties.map { $0.resolvingRefs(using: index) },
142
- events: sharedObject.events.map { $0.resolvingRefs(using: index) },
170
+ constructorParameters: sharedObject.constructorParameters?.map {
171
+ $0.resolvingRefs(using: index, in: sharedObject.name)
172
+ },
173
+ functions: sharedObject.functions.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
174
+ properties: sharedObject.properties.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
175
+ events: sharedObject.events.map { $0.resolvingRefs(using: index, in: sharedObject.name) },
143
176
  file: sharedObject.file
144
177
  )
145
178
  },
@@ -149,7 +182,7 @@ extension ExportedSurface {
149
182
  properties: record.properties.map { property in
150
183
  ExportedRecordProperty(
151
184
  name: property.name,
152
- type: property.type.resolvingRefs(using: index),
185
+ type: property.type.resolvingRefs(using: index, in: record.name),
153
186
  isOptional: property.isOptional,
154
187
  hasDefault: property.hasDefault
155
188
  )
@@ -164,7 +197,8 @@ extension ExportedSurface {
164
197
  ExportedUnion(
165
198
  name: union.name,
166
199
  members: union.members.map { member in
167
- ExportedUnionMember(name: member.name, type: member.type.resolvingRefs(using: index))
200
+ ExportedUnionMember(
201
+ name: member.name, type: member.type.resolvingRefs(using: index, in: union.name))
168
202
  },
169
203
  file: union.file
170
204
  )
@@ -174,19 +208,19 @@ extension ExportedSurface {
174
208
  }
175
209
 
176
210
  extension ExportedParameter {
177
- fileprivate func resolvingRefs(using index: RefIndex) -> ExportedParameter {
211
+ fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedParameter {
178
212
  return ExportedParameter(
179
- label: label, name: name, type: type.resolvingRefs(using: index), isOptional: isOptional)
213
+ label: label, name: name, type: type.resolvingRefs(using: index, in: scope), isOptional: isOptional)
180
214
  }
181
215
  }
182
216
 
183
217
  extension ExportedFunction {
184
- fileprivate func resolvingRefs(using index: RefIndex) -> ExportedFunction {
218
+ fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedFunction {
185
219
  return ExportedFunction(
186
220
  name: name,
187
221
  jsName: jsName,
188
- parameters: parameters.map { $0.resolvingRefs(using: index) },
189
- returns: returns?.resolvingRefs(using: index),
222
+ parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
223
+ returns: returns?.resolvingRefs(using: index, in: scope),
190
224
  isAsync: isAsync,
191
225
  isThrowing: isThrowing,
192
226
  isStatic: isStatic
@@ -195,11 +229,11 @@ extension ExportedFunction {
195
229
  }
196
230
 
197
231
  extension ExportedProperty {
198
- fileprivate func resolvingRefs(using index: RefIndex) -> ExportedProperty {
232
+ fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedProperty {
199
233
  return ExportedProperty(
200
234
  name: name,
201
235
  jsName: jsName,
202
- type: type?.resolvingRefs(using: index),
236
+ type: type?.resolvingRefs(using: index, in: scope),
203
237
  isSettable: isSettable,
204
238
  isStatic: isStatic
205
239
  )
@@ -207,8 +241,8 @@ extension ExportedProperty {
207
241
  }
208
242
 
209
243
  extension ExportedEvent {
210
- fileprivate func resolvingRefs(using index: RefIndex) -> ExportedEvent {
244
+ fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedEvent {
211
245
  return ExportedEvent(
212
- name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index), isSync: isSync)
246
+ name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index, in: scope), isSync: isSync)
213
247
  }
214
248
  }
@@ -1,10 +1,14 @@
1
1
  import SwiftSyntax
2
2
 
3
- /// Extracts the full JS-exported surface of every top-level `@ExpoModule`, `@SharedObject`, and
4
- /// `@Record` type: their `@JS` members and record properties. The deep counterpart to
5
- /// `DetectionVisitor`. Recognition is purely syntactic and re-reads what the macros read (the macro
6
- /// target can't be imported), so it stays in step with `JSFunction` / `JSProperty` / `JSConstructor` /
3
+ /// Extracts the full JS-exported surface of every `@ExpoModule`, `@SharedObject`, and `@Record` type:
4
+ /// their `@JS` members and record properties. The deep counterpart to `DetectionVisitor`.
5
+ /// Recognition is purely syntactic and re-reads what the macros read (the macro target can't be
6
+ /// imported), so it stays in step with `JSFunction` / `JSProperty` / `JSConstructor` /
7
7
  /// `RecordProperty`.
8
+ ///
9
+ /// Types nested in another type's body, or in a file-scope `extension`, are reported under their
10
+ /// qualified Swift name (`Options.Encoding`), so a ref to one can be resolved from the scope it is
11
+ /// written in. A type declared in a function body or a closure is local and is not reported.
8
12
  final class SurfaceVisitor: SyntaxVisitor {
9
13
  private let file: String
10
14
  private(set) var modules: [ExportedModule] = []
@@ -19,19 +23,48 @@ final class SurfaceVisitor: SyntaxVisitor {
19
23
  }
20
24
 
21
25
  override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
22
- if isTopLevel(node) {
23
- classify(name: node.name.text, attributes: node.attributes, members: node.memberBlock.members)
26
+ guard let scope = enclosingTypes(of: node) else {
27
+ return .skipChildren
24
28
  }
25
- // The member walk reads the body itself; nested types aren't part of this surface (matching
26
- // `DetectionVisitor`'s top-level-only scope), so there's no reason to descend.
27
- return .skipChildren
29
+ classify(
30
+ name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
31
+ // The member walk reads the body itself; the children are visited only to find nested types.
32
+ return .visitChildren
28
33
  }
29
34
 
30
35
  override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
31
- if isTopLevel(node) {
32
- classify(name: node.name.text, attributes: node.attributes, members: node.memberBlock.members)
36
+ guard let scope = enclosingTypes(of: node) else {
37
+ return .skipChildren
33
38
  }
34
- return .skipChildren
39
+ classify(
40
+ name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
41
+ return .visitChildren
42
+ }
43
+
44
+ override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
45
+ // No macro the surface reports applies to an actor, but its body can declare types that do.
46
+ enclosingTypes(of: node) == nil ? .skipChildren : .visitChildren
47
+ }
48
+
49
+ override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
50
+ // Only for the types declared in its body. Members an extension adds to a module, shared object,
51
+ // or record are not part of the surface, since the macros only read the type's own body.
52
+ enclosingTypes(of: node) == nil ? .skipChildren : .visitChildren
53
+ }
54
+
55
+ // A type declared in a function or accessor body or in a closure is local and never reported, so
56
+ // these are not walked.
57
+
58
+ override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
59
+ .skipChildren
60
+ }
61
+
62
+ override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
63
+ .skipChildren
64
+ }
65
+
66
+ override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
67
+ .skipChildren
35
68
  }
36
69
 
37
70
  /// The two kinds of enum the surface reports: a `@Union` by its attribute, an `Enumerable` enum by
@@ -41,14 +74,15 @@ final class SurfaceVisitor: SyntaxVisitor {
41
74
  /// would be nothing to report as an enum, and listing it in both arrays would describe two
42
75
  /// contradictory JS types for one declaration.
43
76
  override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
44
- guard isTopLevel(node) else {
77
+ guard let scope = enclosingTypes(of: node) else {
45
78
  return .skipChildren
46
79
  }
80
+ let name = qualifiedName(node.name.identifierName, in: scope)
47
81
 
48
82
  if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
49
83
  unions.append(
50
84
  ExportedUnion(
51
- name: node.name.text,
85
+ name: name,
52
86
  members: collectUnionMembers(node.memberBlock.members),
53
87
  file: file
54
88
  ))
@@ -56,24 +90,38 @@ final class SurfaceVisitor: SyntaxVisitor {
56
90
  let rawType = rawValueType(of: node.inheritanceClause)
57
91
  enums.append(
58
92
  ExportedEnum(
59
- name: node.name.text,
93
+ name: name,
60
94
  rawType: rawType,
61
95
  cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
62
96
  file: file
63
97
  ))
64
98
  }
65
- return .skipChildren
99
+ // An enum is often a namespace for other types, so its body is searched too.
100
+ return .visitChildren
66
101
  }
67
102
 
68
- /// Routes a top-level type to the right collector based on which Expo macro it carries. A type
103
+ /// Routes a class or struct to the right collector based on which Expo macro it carries. A type
69
104
  /// carrying none of them is ignored. `@Record` and `@ExpoModule`/`@SharedObject` are mutually
70
105
  /// exclusive in practice, so the first match wins.
71
- private func classify(name: String, attributes: AttributeListSyntax, members: MemberBlockItemListSyntax) {
106
+ ///
107
+ /// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
108
+ /// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
109
+ /// spell it.
110
+ private func classify(
111
+ name: String, scope: [String], attributes: AttributeListSyntax, members: MemberBlockItemListSyntax
112
+ ) {
113
+ let qualified = qualifiedName(name, in: scope)
114
+
72
115
  if let attribute = attributes.firstAttribute(named: DetectedMacro.expoModule.rawValue) {
116
+ // Only a file-scope module is reported. Autolinking registers no other (`DetectionVisitor`
117
+ // reads file scope only), so a nested one never loads.
118
+ guard scope.isEmpty else {
119
+ return
120
+ }
73
121
  let (functions, properties, events, _) = collectJSMembers(members)
74
122
  modules.append(
75
123
  ExportedModule(
76
- name: name,
124
+ name: qualified,
77
125
  jsName: stringArgument(of: attribute) ?? name,
78
126
  functions: functions,
79
127
  properties: properties,
@@ -92,7 +140,7 @@ final class SurfaceVisitor: SyntaxVisitor {
92
140
  let (functions, properties, events, constructor) = collectJSMembers(members)
93
141
  sharedObjects.append(
94
142
  ExportedSharedObject(
95
- name: name,
143
+ name: qualified,
96
144
  jsName: stringArgument(of: attribute) ?? name,
97
145
  constructorParameters: constructor,
98
146
  functions: functions,
@@ -104,7 +152,7 @@ final class SurfaceVisitor: SyntaxVisitor {
104
152
  }
105
153
 
106
154
  if attributes.firstAttribute(named: DetectedMacro.record.rawValue) != nil {
107
- records.append(ExportedRecord(name: name, properties: collectRecordProperties(members), file: file))
155
+ records.append(ExportedRecord(name: qualified, properties: collectRecordProperties(members), file: file))
108
156
  }
109
157
  }
110
158
 
@@ -178,7 +226,7 @@ final class SurfaceVisitor: SyntaxVisitor {
178
226
  functionType.parameters.count <= 1 else {
179
227
  continue
180
228
  }
181
- let name = ident.identifier.text
229
+ let name = ident.identifier.identifierName
182
230
  result.append(
183
231
  ExportedEvent(
184
232
  name: name,
@@ -196,8 +244,8 @@ final class SurfaceVisitor: SyntaxVisitor {
196
244
  let effects = funcDecl.signature.effectSpecifiers
197
245
  let returnType = funcDecl.signature.returnClause?.type
198
246
  return ExportedFunction(
199
- name: funcDecl.name.text,
200
- jsName: stringArgument(of: attribute) ?? funcDecl.name.text,
247
+ name: funcDecl.name.identifierName,
248
+ jsName: stringArgument(of: attribute) ?? funcDecl.name.identifierName,
201
249
  parameters: parameters(of: funcDecl.signature.parameterClause),
202
250
  returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
203
251
  isAsync: effects?.asyncSpecifier != nil,
@@ -219,7 +267,7 @@ final class SurfaceVisitor: SyntaxVisitor {
219
267
  guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
220
268
  continue
221
269
  }
222
- let name = ident.identifier.text
270
+ let name = ident.identifier.identifierName
223
271
  let type = valueTypeNode(annotation: binding.typeAnnotation?.type, initializer: binding.initializer?.value)
224
272
  result.append(
225
273
  ExportedProperty(
@@ -284,7 +332,7 @@ final class SurfaceVisitor: SyntaxVisitor {
284
332
  }
285
333
  properties.append(
286
334
  ExportedRecordProperty(
287
- name: ident.identifier.text,
335
+ name: ident.identifier.identifierName,
288
336
  type: type,
289
337
  isOptional: annotation.map { isOptionalType($0) } ?? false,
290
338
  hasDefault: binding.initializer != nil
@@ -312,7 +360,7 @@ final class SurfaceVisitor: SyntaxVisitor {
312
360
  continue
313
361
  }
314
362
  for element in caseDecl.elements where element.parameterClause == nil {
315
- let name = element.name.text
363
+ let name = element.name.identifierName
316
364
  cases.append(
317
365
  ExportedEnumCase(
318
366
  name: name,
@@ -343,7 +391,7 @@ final class SurfaceVisitor: SyntaxVisitor {
343
391
  continue
344
392
  }
345
393
  result.append(
346
- ExportedUnionMember(name: element.name.text, type: typeNode(from: parameter.type)))
394
+ ExportedUnionMember(name: element.name.identifierName, type: typeNode(from: parameter.type)))
347
395
  }
348
396
  }
349
397
  return result
@@ -353,8 +401,8 @@ final class SurfaceVisitor: SyntaxVisitor {
353
401
  /// first), and `optional` when it has a default value or an optional type.
354
402
  private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
355
403
  clause.parameters.map { parameter in
356
- let label = parameter.firstName.text
357
- let name = parameter.secondName?.text ?? label
404
+ let label = parameter.firstName.identifierName
405
+ let name = parameter.secondName?.identifierName ?? label
358
406
  return ExportedParameter(
359
407
  label: label,
360
408
  name: name,
@@ -364,18 +412,51 @@ final class SurfaceVisitor: SyntaxVisitor {
364
412
  }
365
413
  }
366
414
 
367
- /// True when the declaration sits at file scope. Same rule as `DetectionVisitor.isTopLevel`: its
368
- /// parent is a `CodeBlockItemSyntax` directly under the source file's top-level item list.
369
- private func isTopLevel(_ node: some SyntaxProtocol) -> Bool {
370
- guard let item = node.parent?.as(CodeBlockItemSyntax.self) else {
371
- return false
415
+ /// The names of the types enclosing a declaration, outermost first: empty at file scope, and `nil`
416
+ /// when the declaration is local to a function body or a closure, or sits in a `#if` block.
417
+ ///
418
+ /// File scope uses the same rule as `DetectionVisitor.isTopLevel`, except that `#if` is not looked
419
+ /// through: the parent is a `CodeBlockItemSyntax` directly under the source file's top-level item
420
+ /// list. A member is reached from its type through `MemberBlockItemSyntax`; an `extension`
421
+ /// contributes its extended type as written (without backticks), so its spelling may already be
422
+ /// qualified.
423
+ private func enclosingTypes(of node: some SyntaxProtocol) -> [String]? {
424
+ if let item = node.parent?.as(CodeBlockItemSyntax.self) {
425
+ return item.parent?.parent?.is(SourceFileSyntax.self) == true ? [] : nil
426
+ }
427
+ guard let item = node.parent?.as(MemberBlockItemSyntax.self),
428
+ let parent = item.parent?.parent?.parent,
429
+ let scope = enclosingTypes(of: parent) else {
430
+ return nil
372
431
  }
373
- return item.parent?.parent?.is(SourceFileSyntax.self) == true
432
+ if let decl = parent.as(ExtensionDeclSyntax.self) {
433
+ // Without backticks, like every other reported name.
434
+ return scope + [String(decl.extendedType.trimmedDescription.filter { $0 != "`" })]
435
+ }
436
+ // A class, struct, enum, or actor. A protocol body cannot declare types.
437
+ guard !parent.is(ProtocolDeclSyntax.self), let decl = parent.asProtocol(NamedDeclSyntax.self) else {
438
+ return nil
439
+ }
440
+ return scope + [decl.name.identifierName]
374
441
  }
375
442
  }
376
443
 
444
+ /// A declared name qualified with the names of its enclosing types: `Options.Encoding`.
445
+ private func qualifiedName(_ name: String, in scope: [String]) -> String {
446
+ (scope + [name]).joined(separator: ".")
447
+ }
448
+
377
449
  // MARK: - Syntactic helpers (shared spelling with the macros)
378
450
 
451
+ extension TokenSyntax {
452
+ /// The name a token spells, without the backticks Swift requires around a reserved word used as
453
+ /// a name: `` `default` `` reads as `default`. Tokens that aren't identifiers, such as the `_`
454
+ /// wildcard label, read as written.
455
+ var identifierName: String {
456
+ identifier?.name ?? text
457
+ }
458
+ }
459
+
379
460
  /// True when the modifiers make a member type-level (`static` or `class`).
380
461
  // MARK: - `@Event` helpers
381
462
 
@@ -58,7 +58,9 @@ indirect enum TypeNode: Equatable {
58
58
  case function(parameters: [TypeNode], returns: TypeNode?, isAsync: Bool, isThrowing: Bool)
59
59
 
60
60
  /// Any other named type (record, shared object, enum, union, …). `name` is the possibly-qualified
61
- /// spelling.
61
+ /// spelling until resolution. A resolved ref takes the qualified name of its declaration instead,
62
+ /// so `Encoding` written in `Options` becomes `Options.Encoding`, and a consumer finds the
63
+ /// declaration by an exact match.
62
64
  ///
63
65
  /// `refKind` says which scanned kind declares that name, filled in after the scan by
64
66
  /// `ExportedSurface.resolvingRefs()`; it is `nil` until then, and stays `nil` for a name the scan
@@ -204,7 +206,7 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
204
206
  }
205
207
  // A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
206
208
  if let identifier = type.as(IdentifierTypeSyntax.self) {
207
- return nominalNode(name: identifier.name.text, generics: identifier.genericArgumentClause)
209
+ return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
208
210
  }
209
211
  // A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling.
210
212
  if let member = type.as(MemberTypeSyntax.self) {