expo-modules-macros 0.19.1 → 0.21.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.
@@ -147,61 +147,51 @@ extension ExportedSurface {
147
147
  let index = RefIndex(surface: self)
148
148
  return ExportedSurface(
149
149
  modules: modules.map { module in
150
- ExportedModule(
151
- name: module.name,
152
- jsName: module.jsName,
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)
162
- },
163
- file: module.file
164
- )
150
+ var resolved = module
151
+ resolved.functions = module.functions.map { $0.resolvingRefs(using: index, in: module.name) }
152
+ resolved.properties = module.properties.map { $0.resolvingRefs(using: index, in: module.name) }
153
+ resolved.events = module.events.map { $0.resolvingRefs(using: index, in: module.name) }
154
+ resolved.classes = module.classes.map { exportedClass in
155
+ guard let entry = index.lookup(exportedClass.name, in: module.name),
156
+ entry.kind == .sharedObject else {
157
+ return exportedClass
158
+ }
159
+ return ExportedClass(name: entry.name, jsName: entry.jsName)
160
+ }
161
+ return resolved
165
162
  },
166
163
  sharedObjects: sharedObjects.map { sharedObject in
167
- ExportedSharedObject(
168
- name: sharedObject.name,
169
- jsName: sharedObject.jsName,
170
- constructorParameters: sharedObject.constructorParameters?.map {
164
+ var resolved = sharedObject
165
+ if let initializer = sharedObject.initializer {
166
+ resolved.initializer?.parameters = initializer.parameters.map {
171
167
  $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) },
176
- file: sharedObject.file
177
- )
168
+ }
169
+ }
170
+ resolved.functions = sharedObject.functions.map { $0.resolvingRefs(using: index, in: sharedObject.name) }
171
+ resolved.properties = sharedObject.properties.map { $0.resolvingRefs(using: index, in: sharedObject.name) }
172
+ resolved.events = sharedObject.events.map { $0.resolvingRefs(using: index, in: sharedObject.name) }
173
+ return resolved
178
174
  },
179
175
  records: records.map { record in
180
- ExportedRecord(
181
- name: record.name,
182
- properties: record.properties.map { property in
183
- ExportedRecordProperty(
184
- name: property.name,
185
- type: property.type.resolvingRefs(using: index, in: record.name),
186
- isOptional: property.isOptional,
187
- hasDefault: property.hasDefault
188
- )
189
- },
190
- file: record.file
191
- )
176
+ var resolved = record
177
+ resolved.properties = record.properties.map { property in
178
+ var resolvedProperty = property
179
+ resolvedProperty.type = property.type.resolvingRefs(using: index, in: record.name)
180
+ return resolvedProperty
181
+ }
182
+ return resolved
192
183
  },
193
184
  // An enum's raw type is a primitive or an unscanned spelling, never a ref to another scanned
194
185
  // type, so there is nothing in one to resolve.
195
186
  enums: enums,
196
187
  unions: unions.map { union in
197
- ExportedUnion(
198
- name: union.name,
199
- members: union.members.map { member in
200
- ExportedUnionMember(
201
- name: member.name, type: member.type.resolvingRefs(using: index, in: union.name))
202
- },
203
- file: union.file
204
- )
188
+ var resolved = union
189
+ resolved.members = union.members.map { member in
190
+ var resolvedMember = member
191
+ resolvedMember.type = member.type.resolvingRefs(using: index, in: union.name)
192
+ return resolvedMember
193
+ }
194
+ return resolved
205
195
  }
206
196
  )
207
197
  }
@@ -209,40 +199,33 @@ extension ExportedSurface {
209
199
 
210
200
  extension ExportedParameter {
211
201
  fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedParameter {
212
- return ExportedParameter(
213
- label: label, name: name, type: type.resolvingRefs(using: index, in: scope), isOptional: isOptional)
202
+ var resolved = self
203
+ resolved.type = type.resolvingRefs(using: index, in: scope)
204
+ return resolved
214
205
  }
215
206
  }
216
207
 
217
208
  extension ExportedFunction {
218
209
  fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedFunction {
219
- return ExportedFunction(
220
- name: name,
221
- jsName: jsName,
222
- parameters: parameters.map { $0.resolvingRefs(using: index, in: scope) },
223
- returns: returns?.resolvingRefs(using: index, in: scope),
224
- isAsync: isAsync,
225
- isThrowing: isThrowing,
226
- isStatic: isStatic
227
- )
210
+ var resolved = self
211
+ resolved.parameters = parameters.map { $0.resolvingRefs(using: index, in: scope) }
212
+ resolved.returns = returns?.resolvingRefs(using: index, in: scope)
213
+ return resolved
228
214
  }
229
215
  }
230
216
 
231
217
  extension ExportedProperty {
232
218
  fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedProperty {
233
- return ExportedProperty(
234
- name: name,
235
- jsName: jsName,
236
- type: type?.resolvingRefs(using: index, in: scope),
237
- isSettable: isSettable,
238
- isStatic: isStatic
239
- )
219
+ var resolved = self
220
+ resolved.type = type?.resolvingRefs(using: index, in: scope)
221
+ return resolved
240
222
  }
241
223
  }
242
224
 
243
225
  extension ExportedEvent {
244
226
  fileprivate func resolvingRefs(using index: RefIndex, in scope: String) -> ExportedEvent {
245
- return ExportedEvent(
246
- name: name, jsName: jsName, payload: payload?.resolvingRefs(using: index, in: scope), isSync: isSync)
227
+ var resolved = self
228
+ resolved.payload = payload?.resolvingRefs(using: index, in: scope)
229
+ return resolved
247
230
  }
248
231
  }
@@ -46,7 +46,7 @@ func scanExports(paths: [String]) -> ScanExportsResult {
46
46
  conformances: [enumerableConformanceName]
47
47
  ) { source, file in
48
48
  let tree = Parser.parse(source: source)
49
- let visitor = SurfaceVisitor(file: file)
49
+ let visitor = SurfaceVisitor(file: file, tree: tree)
50
50
  visitor.walk(tree)
51
51
  modules.append(contentsOf: visitor.modules)
52
52
  sharedObjects.append(contentsOf: visitor.sharedObjects)
@@ -9,16 +9,25 @@ import SwiftSyntax
9
9
  /// Types nested in another type's body, or in a file-scope `extension`, are reported under their
10
10
  /// qualified Swift name (`Options.Encoding`), so a ref to one can be resolved from the scope it is
11
11
  /// written in. A type declared in a function body or a closure is local and is not reported.
12
+ ///
13
+ /// Each declaration and member reports the platforms that build it (see `PlatformResolver`), and one
14
+ /// that no platform builds is not reported. `#if` is read only where the runtime sees what it holds:
15
+ /// around types, `@Event` members, and enum cases. `@ExpoModule`, `@SharedObject`, `@Record`, and
16
+ /// `@Union` read only the members written directly in the type body, so `@JS` members, record
17
+ /// properties, and union cases in a `#if` clause are never bound and are not reported. An `@Event`
18
+ /// expands on its own declaration, and the compiler reads enum cases itself.
12
19
  final class SurfaceVisitor: SyntaxVisitor {
13
20
  private let file: String
21
+ private let platformResolver: PlatformResolver
14
22
  private(set) var modules: [ExportedModule] = []
15
23
  private(set) var sharedObjects: [ExportedSharedObject] = []
16
24
  private(set) var records: [ExportedRecord] = []
17
25
  private(set) var enums: [ExportedEnum] = []
18
26
  private(set) var unions: [ExportedUnion] = []
19
27
 
20
- init(file: String) {
28
+ init(file: String, tree: SourceFileSyntax) {
21
29
  self.file = file
30
+ self.platformResolver = PlatformResolver(tree: tree)
22
31
  super.init(viewMode: .sourceAccurate)
23
32
  }
24
33
 
@@ -26,8 +35,7 @@ final class SurfaceVisitor: SyntaxVisitor {
26
35
  guard let scope = enclosingTypes(of: node) else {
27
36
  return .skipChildren
28
37
  }
29
- classify(
30
- name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
38
+ classify(node, name: node.name.identifierName, scope: scope)
31
39
  // The member walk reads the body itself; the children are visited only to find nested types.
32
40
  return .visitChildren
33
41
  }
@@ -36,8 +44,7 @@ final class SurfaceVisitor: SyntaxVisitor {
36
44
  guard let scope = enclosingTypes(of: node) else {
37
45
  return .skipChildren
38
46
  }
39
- classify(
40
- name: node.name.identifierName, scope: scope, attributes: node.attributes, members: node.memberBlock.members)
47
+ classify(node, name: node.name.identifierName, scope: scope)
41
48
  return .visitChildren
42
49
  }
43
50
 
@@ -78,12 +85,20 @@ final class SurfaceVisitor: SyntaxVisitor {
78
85
  return .skipChildren
79
86
  }
80
87
  let name = qualifiedName(node.name.identifierName, in: scope)
88
+ let platforms = platformResolver.platforms(of: node)
89
+ // An enum is often a namespace for other types, so its body is searched too, even when this
90
+ // enum is not reported.
91
+ guard !platforms.isEmpty else {
92
+ return .visitChildren
93
+ }
81
94
 
82
95
  if node.attributes.firstAttribute(named: DetectedMacro.union.rawValue) != nil {
83
96
  unions.append(
84
97
  ExportedUnion(
85
98
  name: name,
86
99
  members: collectUnionMembers(node.memberBlock.members),
100
+ accessLevel: spelledAccessLevel(of: node.modifiers),
101
+ platforms: platforms,
87
102
  file: file
88
103
  ))
89
104
  } else if inherits(from: enumerableConformanceName, in: node.inheritanceClause) {
@@ -93,6 +108,8 @@ final class SurfaceVisitor: SyntaxVisitor {
93
108
  name: name,
94
109
  rawType: rawType,
95
110
  cases: collectEnumCases(node.memberBlock.members, rawType: rawType),
111
+ accessLevel: spelledAccessLevel(of: node.modifiers),
112
+ platforms: platforms,
96
113
  file: file
97
114
  ))
98
115
  }
@@ -107,10 +124,15 @@ final class SurfaceVisitor: SyntaxVisitor {
107
124
  /// `name` is the declared name and `scope` the names of the types enclosing it. The reported `name`
108
125
  /// is qualified with the scope, while a default `jsName` is the declared name alone, as the macros
109
126
  /// spell it.
110
- private func classify(
111
- name: String, scope: [String], attributes: AttributeListSyntax, members: MemberBlockItemListSyntax
112
- ) {
127
+ private func classify(_ node: some DeclGroupSyntax, name: String, scope: [String]) {
113
128
  let qualified = qualifiedName(name, in: scope)
129
+ let attributes = node.attributes
130
+ let members = node.memberBlock.members
131
+ let accessLevel = spelledAccessLevel(of: node.modifiers)
132
+ let platforms = platformResolver.platforms(of: node)
133
+ guard !platforms.isEmpty else {
134
+ return
135
+ }
114
136
 
115
137
  if let attribute = attributes.firstAttribute(named: DetectedMacro.expoModule.rawValue) {
116
138
  // Only a file-scope module is reported. Autolinking registers no other (`DetectionVisitor`
@@ -131,82 +153,111 @@ final class SurfaceVisitor: SyntaxVisitor {
131
153
  classes: classListArgument(of: attribute, label: "classes").map {
132
154
  ExportedClass(name: $0, jsName: nil)
133
155
  },
156
+ accessLevel: accessLevel,
157
+ platforms: platforms,
134
158
  file: file
135
159
  ))
136
160
  return
137
161
  }
138
162
 
139
163
  if let attribute = attributes.firstAttribute(named: DetectedMacro.sharedObject.rawValue) {
140
- let (functions, properties, events, constructor) = collectJSMembers(members)
164
+ let (functions, properties, events, initializer) = collectJSMembers(members)
141
165
  sharedObjects.append(
142
166
  ExportedSharedObject(
143
167
  name: qualified,
144
168
  jsName: stringArgument(of: attribute) ?? name,
145
- constructorParameters: constructor,
169
+ initializer: initializer,
146
170
  functions: functions,
147
171
  properties: properties,
148
172
  events: events,
173
+ accessLevel: accessLevel,
174
+ platforms: platforms,
149
175
  file: file
150
176
  ))
151
177
  return
152
178
  }
153
179
 
154
180
  if attributes.firstAttribute(named: DetectedMacro.record.rawValue) != nil {
155
- records.append(ExportedRecord(name: qualified, properties: collectRecordProperties(members), file: file))
181
+ records.append(
182
+ ExportedRecord(
183
+ name: qualified,
184
+ properties: collectRecordProperties(members),
185
+ accessLevel: accessLevel,
186
+ platforms: platforms,
187
+ file: file
188
+ ))
156
189
  }
157
190
  }
158
191
 
159
192
  /// The exported members of a module / shared-object body: `@JS` functions and properties, `@Event`
160
- /// events, and the single `@JS init` constructor parameters (`nil` when absent).
193
+ /// events, and the single `@JS init` (`nil` when absent).
161
194
  private func collectJSMembers(
162
195
  _ members: MemberBlockItemListSyntax
163
196
  ) -> (
164
197
  functions: [ExportedFunction], properties: [ExportedProperty], events: [ExportedEvent],
165
- constructor: [ExportedParameter]?
198
+ initializer: ExportedInitializer?
166
199
  ) {
167
200
  var functions: [ExportedFunction] = []
168
201
  var properties: [ExportedProperty] = []
169
202
  var events: [ExportedEvent] = []
170
- var constructor: [ExportedParameter]?
203
+ var initializer: ExportedInitializer?
171
204
 
172
- for member in members {
205
+ // The macros bind only the `@JS` members written directly in the body.
206
+ let directMembers = Set(members.map(\.id))
207
+
208
+ for member in flattened(members) {
173
209
  let decl = member.decl
210
+ let platforms = platformResolver.platforms(of: decl)
211
+ guard !platforms.isEmpty else {
212
+ continue
213
+ }
214
+
215
+ if let varDecl = decl.as(VariableDeclSyntax.self),
216
+ varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) == nil,
217
+ let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue) {
218
+ events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute, platforms: platforms))
219
+ continue
220
+ }
221
+
222
+ guard directMembers.contains(member.id) else {
223
+ continue
224
+ }
174
225
 
175
226
  if let initDecl = decl.as(InitializerDeclSyntax.self),
176
227
  initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil {
177
228
  // At most one `@JS init`; keep the first if a malformed source has more (the macro errors).
178
- if constructor == nil {
179
- constructor = parameters(of: initDecl.signature.parameterClause)
229
+ if initializer == nil {
230
+ initializer = ExportedInitializer(
231
+ parameters: parameters(of: initDecl.signature.parameterClause),
232
+ accessLevel: spelledAccessLevel(of: initDecl.modifiers),
233
+ platforms: platforms
234
+ )
180
235
  }
181
236
  continue
182
237
  }
183
238
 
184
239
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
185
240
  let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
186
- functions.append(makeFunction(funcDecl: funcDecl, attribute: attribute))
241
+ functions.append(makeFunction(funcDecl: funcDecl, attribute: attribute, platforms: platforms))
187
242
  continue
188
243
  }
189
244
 
190
245
  if let varDecl = decl.as(VariableDeclSyntax.self),
191
246
  let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) {
192
- properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute))
193
- continue
194
- }
195
-
196
- if let varDecl = decl.as(VariableDeclSyntax.self),
197
- let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue) {
198
- events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute))
247
+ properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute, platforms: platforms))
199
248
  }
200
249
  }
201
250
 
202
- return (functions, properties, events, constructor)
251
+ return (functions, properties, events, initializer)
203
252
  }
204
253
 
205
254
  /// Builds the `ExportedEvent` entries for an `@Event var`, applying the same checks
206
255
  /// `EventMacro.validatedEvent(of:on:)` does. A rejected declaration expands to no event, so
207
256
  /// reporting one would describe a surface that does not exist. `@JS` on the same property is
208
257
  /// caught by the caller, which reaches the `@JS` branch first.
209
- private func makeEvents(varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> [ExportedEvent] {
258
+ private func makeEvents(
259
+ varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
260
+ ) -> [ExportedEvent] {
210
261
  guard varDecl.bindingSpecifier.tokenKind != .keyword(.let),
211
262
  !isTypeLevel(varDecl.modifiers) else {
212
263
  return []
@@ -232,7 +283,9 @@ final class SurfaceVisitor: SyntaxVisitor {
232
283
  name: name,
233
284
  jsName: override ?? defaultEventName(for: name),
234
285
  payload: functionType.parameters.first.map { typeNode(from: $0.type) },
235
- isSync: isSync
286
+ isSync: isSync,
287
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
288
+ platforms: platforms
236
289
  ))
237
290
  }
238
291
  return result
@@ -240,7 +293,9 @@ final class SurfaceVisitor: SyntaxVisitor {
240
293
 
241
294
  /// Builds an `ExportedFunction` from a `@JS func`: JS-name fallback, parameters, a `Void` return as
242
295
  /// `nil`, and the effect/static flags.
243
- private func makeFunction(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) -> ExportedFunction {
296
+ private func makeFunction(
297
+ funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
298
+ ) -> ExportedFunction {
244
299
  let effects = funcDecl.signature.effectSpecifiers
245
300
  let returnType = funcDecl.signature.returnClause?.type
246
301
  return ExportedFunction(
@@ -250,14 +305,18 @@ final class SurfaceVisitor: SyntaxVisitor {
250
305
  returns: isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) },
251
306
  isAsync: effects?.asyncSpecifier != nil,
252
307
  isThrowing: effects?.throwsClause?.throwsSpecifier != nil,
253
- isStatic: isTypeLevel(funcDecl.modifiers)
308
+ isStatic: isTypeLevel(funcDecl.modifiers),
309
+ accessLevel: spelledAccessLevel(of: funcDecl.modifiers),
310
+ platforms: platforms
254
311
  )
255
312
  }
256
313
 
257
314
  /// Builds the `ExportedProperty` entries for a `@JS var`/`let`. One declaration can introduce several
258
315
  /// bindings (`var a, b: Int`), so this returns an array. The value type is the annotation, else the
259
316
  /// literal default's inferred type, else `nil`.
260
- private func makeProperties(varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> [ExportedProperty] {
317
+ private func makeProperties(
318
+ varDecl: VariableDeclSyntax, attribute: AttributeSyntax, platforms: [ExportedPlatform]
319
+ ) -> [ExportedProperty] {
261
320
  let isLet = varDecl.bindingSpecifier.tokenKind == .keyword(.let)
262
321
  let isStatic = isTypeLevel(varDecl.modifiers)
263
322
  let override = stringArgument(of: attribute)
@@ -275,7 +334,11 @@ final class SurfaceVisitor: SyntaxVisitor {
275
334
  jsName: override ?? name,
276
335
  type: type,
277
336
  isSettable: isSettable(binding: binding, isLet: isLet),
278
- isStatic: isStatic
337
+ isStatic: isStatic,
338
+ // Only a stored property has an initializer; a computed one has none to report.
339
+ defaultValue: binding.initializer?.value.trimmedDescription,
340
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
341
+ platforms: platforms
279
342
  ))
280
343
  }
281
344
  return result
@@ -314,11 +377,16 @@ final class SurfaceVisitor: SyntaxVisitor {
314
377
  private func collectRecordProperties(_ members: MemberBlockItemListSyntax) -> [ExportedRecordProperty] {
315
378
  var properties: [ExportedRecordProperty] = []
316
379
 
380
+ // `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
317
381
  for member in members {
318
382
  guard let varDecl = member.decl.as(VariableDeclSyntax.self),
319
383
  !isExcludedRecordModifier(varDecl.modifiers) else {
320
384
  continue
321
385
  }
386
+ let platforms = platformResolver.platforms(of: varDecl)
387
+ guard !platforms.isEmpty else {
388
+ continue
389
+ }
322
390
  for binding in varDecl.bindings {
323
391
  if binding.accessorBlock != nil {
324
392
  continue
@@ -335,7 +403,9 @@ final class SurfaceVisitor: SyntaxVisitor {
335
403
  name: ident.identifier.identifierName,
336
404
  type: type,
337
405
  isOptional: annotation.map { isOptionalType($0) } ?? false,
338
- hasDefault: binding.initializer != nil
406
+ defaultValue: binding.initializer?.value.trimmedDescription,
407
+ accessLevel: spelledAccessLevel(of: varDecl.modifiers),
408
+ platforms: platforms
339
409
  ))
340
410
  }
341
411
  }
@@ -355,16 +425,21 @@ final class SurfaceVisitor: SyntaxVisitor {
355
425
  ) -> [ExportedEnumCase] {
356
426
  var cases: [ExportedEnumCase] = []
357
427
 
358
- for member in members {
428
+ for member in flattened(members) {
359
429
  guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
360
430
  continue
361
431
  }
432
+ let platforms = platformResolver.platforms(of: caseDecl)
433
+ guard !platforms.isEmpty else {
434
+ continue
435
+ }
362
436
  for element in caseDecl.elements where element.parameterClause == nil {
363
437
  let name = element.name.identifierName
364
438
  cases.append(
365
439
  ExportedEnumCase(
366
440
  name: name,
367
- rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType)
441
+ rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType),
442
+ platforms: platforms
368
443
  ))
369
444
  }
370
445
  }
@@ -380,10 +455,15 @@ final class SurfaceVisitor: SyntaxVisitor {
380
455
  private func collectUnionMembers(_ members: MemberBlockItemListSyntax) -> [ExportedUnionMember] {
381
456
  var result: [ExportedUnionMember] = []
382
457
 
458
+ // `@Union` reads only the cases written directly in the body, so `#if` is not looked into.
383
459
  for member in members {
384
460
  guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
385
461
  continue
386
462
  }
463
+ let platforms = platformResolver.platforms(of: caseDecl)
464
+ guard !platforms.isEmpty else {
465
+ continue
466
+ }
387
467
  for element in caseDecl.elements {
388
468
  guard let parameters = element.parameterClause?.parameters,
389
469
  parameters.count == 1, let parameter = parameters.first,
@@ -391,14 +471,15 @@ final class SurfaceVisitor: SyntaxVisitor {
391
471
  continue
392
472
  }
393
473
  result.append(
394
- ExportedUnionMember(name: element.name.identifierName, type: typeNode(from: parameter.type)))
474
+ ExportedUnionMember(
475
+ name: element.name.identifierName, type: typeNode(from: parameter.type), platforms: platforms))
395
476
  }
396
477
  }
397
478
  return result
398
479
  }
399
480
 
400
481
  /// Projects a parameter clause into `ExportedParameter`s: label = first name, name = second (else
401
- /// first), and `optional` when it has a default value or an optional type.
482
+ /// first), `optional` when it has a default value or an optional type, and the default as written.
402
483
  private func parameters(of clause: FunctionParameterClauseSyntax) -> [ExportedParameter] {
403
484
  clause.parameters.map { parameter in
404
485
  let label = parameter.firstName.identifierName
@@ -407,25 +488,26 @@ final class SurfaceVisitor: SyntaxVisitor {
407
488
  label: label,
408
489
  name: name,
409
490
  type: typeNode(from: parameter.type),
410
- isOptional: parameter.defaultValue != nil || isOptionalType(parameter.type)
491
+ isOptional: parameter.defaultValue != nil || isOptionalType(parameter.type),
492
+ defaultValue: parameter.defaultValue?.value.trimmedDescription
411
493
  )
412
494
  }
413
495
  }
414
496
 
415
497
  /// 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.
498
+ /// when the declaration is local to a function body or a closure.
417
499
  ///
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.
500
+ /// File scope uses the same rule as `DetectionVisitor.isTopLevel`: the declaration's item sits in
501
+ /// the source file's top-level item list, directly or through `#if` clauses. A member is reached
502
+ /// from its type through `MemberBlockItemSyntax`, also directly or through `#if` clauses; an
503
+ /// `extension` contributes its extended type as written (without backticks), so its spelling may
504
+ /// already be qualified.
423
505
  private func enclosingTypes(of node: some SyntaxProtocol) -> [String]? {
424
506
  if let item = node.parent?.as(CodeBlockItemSyntax.self) {
425
- return item.parent?.parent?.is(SourceFileSyntax.self) == true ? [] : nil
507
+ return isAtFileScope(item) ? [] : nil
426
508
  }
427
509
  guard let item = node.parent?.as(MemberBlockItemSyntax.self),
428
- let parent = item.parent?.parent?.parent,
510
+ let parent = declaration(owning: item),
429
511
  let scope = enclosingTypes(of: parent) else {
430
512
  return nil
431
513
  }
@@ -441,6 +523,57 @@ final class SurfaceVisitor: SyntaxVisitor {
441
523
  }
442
524
  }
443
525
 
526
+ /// True when a code block item is in the source file's top-level item list, directly or in a clause
527
+ /// of a top-level `#if`.
528
+ private func isAtFileScope(_ item: CodeBlockItemSyntax) -> Bool {
529
+ guard let list = item.parent?.as(CodeBlockItemListSyntax.self) else {
530
+ return false
531
+ }
532
+ if list.parent?.is(SourceFileSyntax.self) == true {
533
+ return true
534
+ }
535
+ guard let ifConfigItem = enclosingIfConfig(of: list)?.parent?.as(CodeBlockItemSyntax.self) else {
536
+ return false
537
+ }
538
+ return isAtFileScope(ifConfigItem)
539
+ }
540
+
541
+ /// The type or extension whose body holds a member, looking through the `#if` clauses the member may
542
+ /// be written in.
543
+ private func declaration(owning item: MemberBlockItemSyntax) -> Syntax? {
544
+ guard let list = item.parent?.as(MemberBlockItemListSyntax.self) else {
545
+ return nil
546
+ }
547
+ if let block = list.parent?.as(MemberBlockSyntax.self) {
548
+ return block.parent
549
+ }
550
+ guard let ifConfigItem = enclosingIfConfig(of: list)?.parent?.as(MemberBlockItemSyntax.self) else {
551
+ return nil
552
+ }
553
+ return declaration(owning: ifConfigItem)
554
+ }
555
+
556
+ /// The `#if` declaration whose clause holds `elements`, or `nil` when they are not in a clause.
557
+ private func enclosingIfConfig(of elements: some SyntaxProtocol) -> IfConfigDeclSyntax? {
558
+ return elements.parent?.as(IfConfigClauseSyntax.self)?.parent?.parent?.as(IfConfigDeclSyntax.self)
559
+ }
560
+
561
+ /// The members of a type body in source order, with the members of every clause of each `#if` in
562
+ /// place of the `#if`. Whether a platform builds a member is left to `PlatformResolver`.
563
+ private func flattened(_ members: MemberBlockItemListSyntax) -> [MemberBlockItemSyntax] {
564
+ return members.flatMap { member -> [MemberBlockItemSyntax] in
565
+ guard let ifConfig = member.decl.as(IfConfigDeclSyntax.self) else {
566
+ return [member]
567
+ }
568
+ return ifConfig.clauses.flatMap { clause -> [MemberBlockItemSyntax] in
569
+ guard case .decls(let decls) = clause.elements else {
570
+ return []
571
+ }
572
+ return flattened(decls)
573
+ }
574
+ }
575
+ }
576
+
444
577
  /// A declared name qualified with the names of its enclosing types: `Options.Encoding`.
445
578
  private func qualifiedName(_ name: String, in scope: [String]) -> String {
446
579
  (scope + [name]).joined(separator: ".")
package/build/index.d.ts CHANGED
@@ -6,11 +6,18 @@ export interface ScanModulesOptions {
6
6
  defines?: string[];
7
7
  /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
8
8
  binaryPath?: string;
9
+ /** Milliseconds after which the scanner is killed and the scan fails. No limit by default. */
10
+ timeout?: number;
9
11
  }
10
- /** Options accepted by `scanExports`. The deep scan doesn't evaluate `#if`, so it takes no flags. */
12
+ /**
13
+ * Options accepted by `scanExports`. The deep scan evaluates `#if` per platform with no flags set, so
14
+ * it takes no define option.
15
+ */
11
16
  export interface ScanExportsOptions {
12
17
  /** Overrides the scanner binary path. Defaults to the one shipped with this package. */
13
18
  binaryPath?: string;
19
+ /** Milliseconds after which the scanner is killed and the scan fails. No limit by default. */
20
+ timeout?: number;
14
21
  }
15
22
  /**
16
23
  * Thrown when the scanner exits non-zero. Exit code 2 is a usage error (bad subcommand or flags) and
@@ -34,9 +41,10 @@ export declare class ScannerSchemaVersionError extends Error {
34
41
  *
35
42
  * - macOS: the universal binary shipped in this package, next to the Swift package rather than in a
36
43
  * `bin` directory.
37
- * - Windows: `ExpoModulesMacros.exe` from the package for the current architecture (see
38
- * `windowsBinaryPackageName`). When that package isn't installed, for example in this repository,
39
- * it falls back to the executable that `npm run build` writes to `platforms/win32-<arch>`.
44
+ * - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
45
+ * current platform and architecture (see `platformBinaryPackageName`). When that package isn't
46
+ * installed, for example in this repository, it falls back to the executable that `npm run build`
47
+ * writes to `platforms/<platform>-<arch>`.
40
48
  *
41
49
  * The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to
42
50
  * get it.
@@ -54,7 +62,9 @@ export declare function scanModules(paths: string[], options?: ScanModulesOption
54
62
  * Deep scan of the full JS-exported surface, for TypeScript type generation. Each path is a `.swift`
55
63
  * file or a directory, scanned recursively.
56
64
  *
57
- * Unlike `scanModules`, this doesn't evaluate `#if` blocks, so it takes no define option:
58
- * conditional declarations are reported as if their conditions held.
65
+ * Each declaration and member reports the Apple OSes whose `#if` conditions and `@available`
66
+ * attributes include it. `#if` is read only where the runtime sees what it holds: around types,
67
+ * `@Event` members, and enum cases. No define option is taken: a condition on a custom flag never holds, so
68
+ * what it guards is not reported.
59
69
  */
60
70
  export declare function scanExports(paths: string[], options?: ScanExportsOptions): Promise<ScanExportsResult>;