expo-modules-macros 0.21.0 → 0.22.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.
Files changed (34) hide show
  1. package/.github/workflows/swift-format.yml +76 -0
  2. package/AGENTS.md +9 -4
  3. package/README.md +15 -2
  4. package/apple/ExpoModulesMacros +0 -0
  5. package/apple/Package.resolved +3 -3
  6. package/apple/Package.swift +1 -2
  7. package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
  8. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +113 -26
  9. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
  10. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +53 -45
  11. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
  12. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
  13. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
  14. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +180 -44
  15. package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
  16. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +163 -42
  17. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +285 -221
  18. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +45 -32
  19. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
  20. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
  21. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
  22. package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
  23. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
  24. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
  25. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
  26. package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
  27. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +3 -2
  28. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +15 -5
  29. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
  30. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +127 -45
  31. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +2 -1
  32. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
  33. package/build/types.d.ts +7 -1
  34. package/package.json +7 -5
@@ -2,26 +2,24 @@ import SwiftSyntax
2
2
  import SwiftSyntaxBuilder
3
3
  import SwiftSyntaxMacros
4
4
 
5
- /**
6
- Member macro applied to a `SharedObject` subclass. Scans the class body for
7
- declarations marked with `@JS` and synthesizes `_synthesizedClassDefinition()`,
8
- returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
9
-
10
- @SharedObject
11
- final class Cache: SharedObject {
12
- @JS
13
- init(name: String) { self.name = name }
14
-
15
- @JS
16
- func get(_ key: String) -> String? { ... }
17
-
18
- @JS
19
- var size: Int { 42 }
20
- }
21
-
22
- The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
23
- definition into the module's exposed surface.
24
- */
5
+ /// Member macro applied to a `SharedObject` subclass. Scans the class body for
6
+ /// declarations marked with `@JS` and synthesizes `_synthesizedClassDefinition()`,
7
+ /// returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
8
+ ///
9
+ /// @SharedObject
10
+ /// final class Cache: SharedObject {
11
+ /// @JS
12
+ /// init(name: String) { self.name = name }
13
+ ///
14
+ /// @JS
15
+ /// func get(_ key: String) -> String? { ... }
16
+ ///
17
+ /// @JS
18
+ /// var size: Int { 42 }
19
+ /// }
20
+ ///
21
+ /// The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
22
+ /// definition into the module's exposed surface.
25
23
  public struct SharedObjectMacro: MemberMacro {
26
24
  public static func expansion(
27
25
  of node: AttributeSyntax,
@@ -35,7 +33,8 @@ public struct SharedObjectMacro: MemberMacro {
35
33
 
36
34
  guard inheritsFromSharedObject(classDecl) else {
37
35
  throw MacroExpansionErrorMessage(
38
- "@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration.")
36
+ "@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration."
37
+ )
39
38
  }
40
39
 
41
40
  let typeName = classDecl.name.text
@@ -62,28 +61,33 @@ public struct SharedObjectMacro: MemberMacro {
62
61
  let decl = member.decl
63
62
 
64
63
  if let initDecl = decl.as(InitializerDeclSyntax.self),
65
- initDecl.attributes.firstAttribute(named: "JS") != nil {
64
+ initDecl.attributes.firstAttribute(named: "JS") != nil
65
+ {
66
66
  if constructor != nil {
67
67
  throw MacroExpansionErrorMessage(
68
- "@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor.")
68
+ "@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor."
69
+ )
69
70
  }
70
71
  constructor = JSConstructor(initDecl: initDecl)
71
72
  continue
72
73
  }
73
74
 
74
75
  if let funcDecl = decl.as(FunctionDeclSyntax.self),
75
- let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
76
+ let attribute = funcDecl.attributes.firstAttribute(named: "JS")
77
+ {
76
78
  let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
79
+ let bound = [function] + (function.asyncVariant.map { [$0] } ?? [])
77
80
  if isTypeLevel(funcDecl.modifiers) {
78
- staticFunctions.append(function)
81
+ staticFunctions.append(contentsOf: bound)
79
82
  } else {
80
- instanceFunctions.append(function)
83
+ instanceFunctions.append(contentsOf: bound)
81
84
  }
82
85
  continue
83
86
  }
84
87
 
85
88
  if let varDecl = decl.as(VariableDeclSyntax.self),
86
- let attribute = varDecl.attributes.firstAttribute(named: "JS") {
89
+ let attribute = varDecl.attributes.firstAttribute(named: "JS")
90
+ {
87
91
  let collected = collectProperties(varDecl: varDecl, attribute: attribute)
88
92
  if isTypeLevel(varDecl.modifiers) {
89
93
  staticProperties.append(contentsOf: collected)
@@ -97,7 +101,8 @@ public struct SharedObjectMacro: MemberMacro {
97
101
  diagnoseJSNameCollisions(functions: staticFunctions, properties: staticProperties, in: context)
98
102
 
99
103
  let lines = entries.map { " \($0)" }.joined(separator: "\n")
100
- let body = entries.isEmpty
104
+ let body =
105
+ entries.isEmpty
101
106
  ? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
102
107
  : " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
103
108
 
@@ -117,14 +122,22 @@ public struct SharedObjectMacro: MemberMacro {
117
122
  if !instanceFunctions.isEmpty || !instanceProperties.isEmpty {
118
123
  emitted.append(
119
124
  buildDecorateSharedObjectPhase(
120
- phase: .prototype, functions: instanceFunctions, properties: instanceProperties,
121
- typeName: typeName))
125
+ phase: .prototype,
126
+ functions: instanceFunctions,
127
+ properties: instanceProperties,
128
+ typeName: typeName
129
+ )
130
+ )
122
131
  }
123
132
  if !staticFunctions.isEmpty || !staticProperties.isEmpty {
124
133
  emitted.append(
125
134
  buildDecorateSharedObjectPhase(
126
- phase: .constructor, functions: staticFunctions, properties: staticProperties,
127
- typeName: typeName))
135
+ phase: .constructor,
136
+ functions: staticFunctions,
137
+ properties: staticProperties,
138
+ typeName: typeName
139
+ )
140
+ )
128
141
  }
129
142
  if let constructor {
130
143
  emitted.append(constructor.buildConstructor(typeName: typeName))
@@ -1,4 +1,5 @@
1
1
  import SwiftSyntax
2
+ import SwiftSyntaxBuilder
2
3
 
3
4
  /// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
4
5
  /// eventual rename (this is a placeholder name) is a single edit, and shared by the macros whose
@@ -38,7 +39,8 @@ internal let unionPayloadProtocolName = "JavaScriptDecodable & JavaScriptEncodab
38
39
  internal let eventEmitterProtocolName = "EventEmitter"
39
40
 
40
41
  /// Types we never assert because they're statically known to conform: the JS primitives. Asserting
41
- /// them would only add noise to the expansion.
42
+ /// them would only add noise to the expansion. `isKnownConforming` extends this to the containers built
43
+ /// from them.
42
44
  private let knownConformingPrimitives: Set<String> = ["Bool", "Int", "Double", "String"]
43
45
 
44
46
  /// One member's worth of conformance assertion: a name (the member it stands for) and the declared
@@ -60,7 +62,7 @@ internal struct ConformanceAssertion {
60
62
  /// body asserts each argument type is `JavaScriptDecodable` and the return/getter type is
61
63
  /// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
62
64
  /// encode on the way out). The check is a single member-named nested helper with one `A0…` generic
63
- /// parameter per non-primitive argument and a single `Return` parameter for the value, called once with
65
+ /// parameter per asserted argument and a single `Return` parameter for the value, called once with
64
66
  /// every type's metatype; naming the helper after the member puts the member in the compiler's
65
67
  /// diagnostic, and the per-slot constraint reports the offending type against the protocol for *its*
66
68
  /// direction.
@@ -68,9 +70,9 @@ internal struct ConformanceAssertion {
68
70
  /// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
69
71
  /// the single return type (or a property's value type on read), or `nil` when there's none.
70
72
  /// `callbackArgumentTypes` are the parameter types of closure arguments, which are encoded when native
71
- /// code calls the closure. Primitives are dropped from each; when nothing is left in any of them,
72
- /// returns `nil` so the caller emits no peer. `isStatic` mirrors a `static`/`class` member so the peer
73
- /// sits in the right metatype context.
73
+ /// code calls the closure. Types known to conform are dropped from each; when nothing is left in any of
74
+ /// them, returns `nil` so the caller emits no peer. `isStatic` mirrors a `static`/`class` member so the
75
+ /// peer sits in the right metatype context.
74
76
  internal func directionalConformanceAssertion(
75
77
  name: String,
76
78
  decodableTypes: [String],
@@ -78,9 +80,9 @@ internal func directionalConformanceAssertion(
78
80
  callbackArgumentTypes: [String] = [],
79
81
  isStatic: Bool
80
82
  ) -> DeclSyntax? {
81
- let decodables = distinctAssertableTypes(decodableTypes)
82
- let callbackArguments = distinctAssertableTypes(callbackArgumentTypes)
83
- let encodable = encodableType.flatMap(assertableBoundaryType)
83
+ let decodables = distinctAssertableTypes(decodableTypes, constraint: javaScriptDecodableProtocolName)
84
+ let callbackArguments = distinctAssertableTypes(callbackArgumentTypes, constraint: javaScriptEncodableProtocolName)
85
+ let encodable = encodableType.flatMap { assertableBoundaryType($0, constraint: javaScriptEncodableProtocolName) }
84
86
  guard !decodables.isEmpty || !callbackArguments.isEmpty || encodable != nil else {
85
87
  return nil
86
88
  }
@@ -126,7 +128,7 @@ internal func directionalConformanceAssertion(
126
128
  /// per property. Each assertion keeps its own named nested helper, so the per-member naming in the
127
129
  /// diagnostic is preserved even though they share one peer.
128
130
  ///
129
- /// Returns `nil` when no assertion has anything left to verify (all primitives / empty), so the
131
+ /// Returns `nil` when no assertion has anything left to verify (all known to conform / empty), so the
130
132
  /// caller emits nothing.
131
133
  internal func typeConformanceAssertions(
132
134
  for assertions: [ConformanceAssertion],
@@ -143,30 +145,57 @@ internal func typeConformanceAssertions(
143
145
  """
144
146
  }
145
147
 
146
- /// The type to assert for a boundary type as written, or `nil` when nothing needs asserting: an
147
- /// exactly-free-form type (converted through the `JavaScriptValue` free-form methods) and a
148
- /// known-conforming primitive both return `nil`; everything else is unwrapped of trailing optionals
149
- /// and asserted. Shared with `@Event`, where an *optional* free-form type (`[String: Any]?`) is kept
150
- /// and asserted for a clean diagnostic. `@JS` leaves out every type that mentions `Any` before it gets
151
- /// here.
152
- internal func assertableBoundaryType(_ type: String) -> String? {
148
+ /// The type to assert against `constraint` for a boundary type as written, or `nil` when nothing needs
149
+ /// asserting: an exactly-free-form type (converted through the `JavaScriptValue` free-form methods) and
150
+ /// a type known to conform (see `isKnownConforming`) both return `nil`; everything else is unwrapped of
151
+ /// trailing optionals and asserted. Shared with `@Event`, where an *optional* free-form type
152
+ /// (`[String: Any]?`) is kept and asserted for a clean diagnostic. `@JS` leaves out every type that
153
+ /// mentions `Any` before it gets here.
154
+ internal func assertableBoundaryType(_ type: String, constraint: String) -> String? {
153
155
  guard !isFreeFormBoundaryType(type) else {
154
156
  return nil
155
157
  }
156
- let unwrapped = unwrappedOptional(type)
157
- return knownConformingPrimitives.contains(unwrapped) ? nil : unwrapped
158
+ guard !isKnownConforming(TypeSyntax(stringLiteral: type), setsConform: setsConform(to: constraint)) else {
159
+ return nil
160
+ }
161
+ return unwrappedOptional(type)
162
+ }
163
+
164
+ /// True when `type` is known to conform without an assertion: a JS primitive, or an optional, array,
165
+ /// `String`-keyed dictionary or set of such a type, at any depth (`[[Int]]`, `[String: [Double]]?`,
166
+ /// `Set<String>`). Each of these containers conforms whenever its element does. A set counts only when
167
+ /// `setsConform` is true. Anything else, such as a dictionary with another key type, returns `false`.
168
+ private func isKnownConforming(_ type: TypeSyntax, setsConform: Bool) -> Bool {
169
+ if let identifier = type.as(IdentifierTypeSyntax.self), identifier.genericArgumentClause == nil {
170
+ return knownConformingPrimitives.contains(identifier.name.text)
171
+ }
172
+ if let element = optionalWrappedType(type) ?? arrayElementType(type) ?? stringKeyedDictionaryValueType(type) {
173
+ return isKnownConforming(element, setsConform: setsConform)
174
+ }
175
+ if setsConform, let element = setElementType(type) {
176
+ return isKnownConforming(element, setsConform: setsConform)
177
+ }
178
+ return false
179
+ }
180
+
181
+ /// True when `Set` conforms to `constraint`. Core conforms it to the JS codable protocols but not to
182
+ /// `AnyArgument`.
183
+ private func setsConform(to constraint: String) -> Bool {
184
+ return !constraint.split(separator: "&").contains { protocolName in
185
+ protocolName.filter { !$0.isWhitespace } == jsConvertibleProtocolName
186
+ }
158
187
  }
159
188
 
160
189
  /// The assertion's body fragment: a nested generic helper named after the member, plus one call per
161
- /// distinct non-primitive type. Nesting the helper keeps the constraint entirely local — no shared
162
- /// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
163
- /// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
164
- /// a known-conforming primitive or the list was empty.
190
+ /// distinct type that needs an assertion. Nesting the helper keeps the constraint entirely local — no
191
+ /// shared symbol, nothing to collide, nothing left in the type's namespace — and naming it after the
192
+ /// member puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every
193
+ /// type was known to conform or the list was empty.
165
194
  private func conformanceAssertionBody(
166
195
  _ assertion: ConformanceAssertion,
167
196
  constraint: String = jsConvertibleProtocolName
168
197
  ) -> String? {
169
- let distinct = distinctAssertableTypes(assertion.types)
198
+ let distinct = distinctAssertableTypes(assertion.types, constraint: constraint)
170
199
  guard !distinct.isEmpty else {
171
200
  return nil
172
201
  }
@@ -176,12 +205,13 @@ private func conformanceAssertionBody(
176
205
  return lines.joined(separator: "\n")
177
206
  }
178
207
 
179
- /// The distinct types to assert from a list, mapping each through `assertableBoundaryType` (which
180
- /// drops what needs no assertion) and deduping in first-seen order.
181
- private func distinctAssertableTypes(_ types: [String]) -> [String] {
208
+ /// The distinct types to assert against `constraint` from a list, mapping each through
209
+ /// `assertableBoundaryType` (which drops what needs no assertion) and deduping in first-seen order.
210
+ private func distinctAssertableTypes(_ types: [String], constraint: String) -> [String] {
182
211
  var seen: Set<String> = []
183
212
  var distinct: [String] = []
184
- for type in types.compactMap(assertableBoundaryType) where seen.insert(type).inserted {
213
+ let assertable = types.compactMap { assertableBoundaryType($0, constraint: constraint) }
214
+ for type in assertable where seen.insert(type).inserted {
185
215
  distinct.append(type)
186
216
  }
187
217
  return distinct
@@ -154,7 +154,8 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
154
154
  message: UnionDiagnosticMessage(
155
155
  "@Union cannot be applied to a generic enum: each case's payload type must be concrete so the macro can select its converter.",
156
156
  id: "union-generic-enum"
157
- ))
157
+ )
158
+ )
158
159
  ])
159
160
  }
160
161
 
@@ -175,7 +176,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
175
176
  message: UnionDiagnosticMessage(
176
177
  "@Union case '\(name)' must carry exactly one associated value: the type this alternative decodes from. A payload-less enum is not a union; make it a raw-value enum conforming to 'Enumerable' instead.",
177
178
  id: "union-case-without-payload"
178
- )))
179
+ )
180
+ )
181
+ )
179
182
  continue
180
183
  }
181
184
  guard parameters.count == 1, let parameter = parameters.first else {
@@ -185,7 +188,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
185
188
  message: UnionDiagnosticMessage(
186
189
  "@Union case '\(name)' must carry exactly one associated value, but has \(parameters.count). Group them in a @Record type and use it as the single payload.",
187
190
  id: "union-case-with-multiple-payloads"
188
- )))
191
+ )
192
+ )
193
+ )
189
194
  continue
190
195
  }
191
196
 
@@ -206,7 +211,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
206
211
  message: UnionDiagnosticMessage(
207
212
  "@Union case '\(name)' can't carry a closure: the payload is also encoded to JavaScript, and a native closure can't be encoded.",
208
213
  id: "union-closure-payload"
209
- )))
214
+ )
215
+ )
216
+ )
210
217
  continue
211
218
  }
212
219
 
@@ -223,7 +230,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
223
230
  message: UnionDiagnosticMessage(
224
231
  "@Union case '\(name)' repeats the payload type '\(payloadType)' of case '\(earlierCase)' and can never be decoded: alternatives are tried in declaration order and the first match wins.",
225
232
  id: "union-duplicate-payload-type"
226
- )))
233
+ )
234
+ )
235
+ )
227
236
  continue
228
237
  }
229
238
  seenPayloadTypes[normalizedType] = name
@@ -261,12 +270,15 @@ private func decodeMethod(union: UnionType) -> DeclSyntax {
261
270
  var lines: [String] = [" let kind = JavaScriptValueKinds(of: value)"]
262
271
  for unionCase in union.cases {
263
272
  let payloadType = expressionType(unionCase.payloadType)
264
- lines.append(" if \(payloadType).decodableKinds.contains(kind), let payload = try? \(payloadType).decode(value, in: runtime) {")
273
+ lines.append(
274
+ " if \(payloadType).decodableKinds.contains(kind), let payload = try? \(payloadType).decode(value, in: runtime) {"
275
+ )
265
276
  lines.append(" return \(unionCase.construction)")
266
277
  lines.append(" }")
267
278
  }
268
279
  let expected = union.cases.map { "\"\($0.payloadType)\"" }.joined(separator: ", ")
269
- let mismatch = "(unionName: \"\(union.name)\", received: value.copied(in: runtime).kind.rawValue, expected: [\(expected)])"
280
+ let mismatch =
281
+ "(unionName: \"\(union.name)\", received: value.copied(in: runtime).kind.rawValue, expected: [\(expected)])"
270
282
  lines.append(" throw Exceptions.UnionCaseMismatch(\(mismatch))")
271
283
  let body = lines.joined(separator: "\n")
272
284
  return """
@@ -322,7 +334,8 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
322
334
  private func accessorMethods(union: UnionType) -> [DeclSyntax] {
323
335
  return union.cases.map { unionCase in
324
336
  let payloadType = expressionType(unionCase.payloadType)
325
- let mismatch = "(unionName: \"\(union.name)\", received: _payloadTypeName, expected: [\"\(unionCase.payloadType)\"])"
337
+ let mismatch =
338
+ "(unionName: \"\(union.name)\", received: _payloadTypeName, expected: [\"\(unionCase.payloadType)\"])"
326
339
  return """
327
340
  public func `as`(_ type: \(raw: payloadType).Type) throws -> \(raw: payloadType) {
328
341
  if case \(raw: unionCase.pattern) = self {
@@ -369,7 +382,8 @@ private func defaultValueDiagnostic(
369
382
  newNode: Syntax(
370
383
  parameter
371
384
  .with(\.type, parameter.type.with(\.trailingTrivia, []))
372
- .with(\.defaultValue, nil))
385
+ .with(\.defaultValue, nil)
386
+ )
373
387
  )
374
388
  ]
375
389
  )
@@ -84,7 +84,8 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
84
84
  // stored constant with the mask already folded rather than a computed property rebuilding an
85
85
  // array literal per call. The macro knows the bit count, so the literal is exact: n props
86
86
  // occupy bits 0..<n, which is the low n bits set.
87
- let allPropsMask = props.valueProps.isEmpty
87
+ let allPropsMask =
88
+ props.valueProps.isEmpty
88
89
  ? "0"
89
90
  : "0b" + String(repeating: "1", count: props.valueProps.count)
90
91
 
@@ -206,7 +207,8 @@ private func innerFunctionType(of type: TypeSyntax) -> TypeSyntax? {
206
207
  guard let tuple = type.as(TupleTypeSyntax.self),
207
208
  tuple.elements.count == 1,
208
209
  let only = tuple.elements.first,
209
- only.type.is(FunctionTypeSyntax.self) else {
210
+ only.type.is(FunctionTypeSyntax.self)
211
+ else {
210
212
  return nil
211
213
  }
212
214
  return only.type
@@ -322,7 +324,8 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
322
324
  // (`var title = ""`). Anything a syntactic macro can't resolve still needs an annotation,
323
325
  // since the decode surface names the type.
324
326
  let declaredType = binding.typeAnnotation?.type
325
- let resolvedType = declaredType?.trimmedDescription
327
+ let resolvedType =
328
+ declaredType?.trimmedDescription
326
329
  ?? binding.initializer.flatMap { inferredLiteralType(of: $0.value) }
327
330
  guard let resolvedType else {
328
331
  throw MacroExpansionErrorMessage(
@@ -390,7 +393,8 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
390
393
  if let identifier = type.as(IdentifierTypeSyntax.self),
391
394
  identifier.name.text == "Optional",
392
395
  let argument = identifier.genericArgumentClause?.arguments.first,
393
- case .type(let wrapped) = argument.argument {
396
+ case .type(let wrapped) = argument.argument
397
+ {
394
398
  return underlyingFunctionType(of: wrapped)
395
399
  }
396
400
  return nil
@@ -404,7 +408,8 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
404
408
  private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
405
409
  // An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
406
410
  // the wire key visible in the generated source and independent of that implicit rule.
407
- let cases = valueProps
411
+ let cases =
412
+ valueProps
408
413
  .map { prop in
409
414
  prop.name == prop.wireName
410
415
  ? " case \(prop.name)"
@@ -50,7 +50,9 @@ public enum ScannerCLI {
50
50
  // scan-modules is platform-agnostic: each reported module carries the platforms that include
51
51
  // it, and the consumer filters. An option selecting one platform would silently drop data.
52
52
  guard platform == nil else {
53
- return usageError("scan-modules does not take --platform; each module reports its 'platforms' and the consumer filters")
53
+ return usageError(
54
+ "scan-modules does not take --platform; each module reports its 'platforms' and the consumer filters"
55
+ )
54
56
  }
55
57
  guard !paths.isEmpty else {
56
58
  return usageError("scan-modules requires at least one path")
@@ -94,11 +94,13 @@ final class DetectionVisitor: SyntaxVisitor {
94
94
  if node.is(SourceFileSyntax.self) {
95
95
  return true
96
96
  }
97
- guard node.is(CodeBlockItemListSyntax.self)
98
- || node.is(CodeBlockItemSyntax.self)
99
- || node.is(IfConfigClauseSyntax.self)
100
- || node.is(IfConfigClauseListSyntax.self)
101
- || node.is(IfConfigDeclSyntax.self) else {
97
+ guard
98
+ node.is(CodeBlockItemListSyntax.self)
99
+ || node.is(CodeBlockItemSyntax.self)
100
+ || node.is(IfConfigClauseSyntax.self)
101
+ || node.is(IfConfigClauseListSyntax.self)
102
+ || node.is(IfConfigDeclSyntax.self)
103
+ else {
102
104
  return false
103
105
  }
104
106
  current = node.parent
@@ -118,7 +120,8 @@ final class DetectionVisitor: SyntaxVisitor {
118
120
  for element in attributes {
119
121
  guard let attribute = element.as(AttributeSyntax.self),
120
122
  let macro = DetectedMacro(rawValue: attribute.attributeName.trimmedDescription),
121
- detectedMacros.contains(macro) else {
123
+ detectedMacros.contains(macro)
124
+ else {
122
125
  continue
123
126
  }
124
127
  let location = converter.location(for: node.positionAfterSkippingLeadingTrivia)
@@ -174,7 +177,8 @@ func stringArgument(of attribute: AttributeSyntax) -> String? {
174
177
  first.label == nil,
175
178
  let str = first.expression.as(StringLiteralExprSyntax.self),
176
179
  let segment = str.segments.first?.as(StringSegmentSyntax.self),
177
- str.segments.count == 1 else {
180
+ str.segments.count == 1
181
+ else {
178
182
  return nil
179
183
  }
180
184
  return segment.content.text
@@ -50,7 +50,8 @@ struct ScanBuildConfiguration: BuildConfiguration {
50
50
  // "yes" would surface a declaration that doesn't exist in the real build.
51
51
  guard importPath.count == 1,
52
52
  case .unversioned = version,
53
- let frameworkPlatforms = sdkFrameworkPlatforms[module] else {
53
+ let frameworkPlatforms = sdkFrameworkPlatforms[module]
54
+ else {
54
55
  throw ScanConfigurationError("cannot evaluate 'canImport(\(module))' in a static scan")
55
56
  }
56
57
  return frameworkPlatforms.contains(platform.lowercased())
@@ -1,10 +1,11 @@
1
+ import SwiftParser
2
+ import SwiftSyntax
3
+
1
4
  #if canImport(FoundationEssentials)
2
5
  import FoundationEssentials
3
6
  #else
4
7
  import Foundation
5
8
  #endif
6
- import SwiftParser
7
- import SwiftSyntax
8
9
 
9
10
  /// Walks `paths`, and for each `.swift` file that might contain one of `macros` or one of
10
11
  /// `conformances` (the pre-filter passes it), reads the source and hands it to `process` along with the
@@ -251,11 +252,13 @@ private func swiftFiles(inDirectoryAtPath directory: String, fileManager: FileMa
251
252
  /// The walk is the dominant cost of a whole-tree scan, and skipping that per-entry stat measurably
252
253
  /// shortens it.
253
254
  private func swiftFiles(inDirectory directory: URL, fileManager: FileManager) -> [String] {
254
- guard let enumerator = fileManager.enumerator(
255
- at: directory,
256
- includingPropertiesForKeys: nil,
257
- options: [.skipsHiddenFiles]
258
- ) else {
255
+ guard
256
+ let enumerator = fileManager.enumerator(
257
+ at: directory,
258
+ includingPropertiesForKeys: nil,
259
+ options: [.skipsHiddenFiles]
260
+ )
261
+ else {
259
262
  return []
260
263
  }
261
264
 
@@ -70,7 +70,8 @@ private struct Availability {
70
70
  for element in attributes {
71
71
  if let attribute = element.as(AttributeSyntax.self),
72
72
  attribute.attributeName.trimmedDescription == "available",
73
- case .availability(let arguments) = attribute.arguments {
73
+ case .availability(let arguments) = attribute.arguments
74
+ {
74
75
  read(arguments, readsDeprecation: isOwn)
75
76
  }
76
77
  }
@@ -110,7 +111,8 @@ private struct Availability {
110
111
  for argument in arguments {
111
112
  if case .availabilityVersionRestriction(let restriction) = argument.argument,
112
113
  let platform = availabilityPlatformNames[restriction.platform.text],
113
- let version = restriction.version {
114
+ let version = restriction.version
115
+ {
114
116
  raiseIntroduced(on: platform, to: version.trimmedDescription)
115
117
  }
116
118
  }
@@ -218,7 +220,8 @@ private func decodedText(of literal: SimpleStringLiteralExprSyntax) -> String {
218
220
  case "u":
219
221
  if text[index...].hasPrefix("{"), let close = text[index...].firstIndex(of: "}"),
220
222
  let scalarValue = UInt32(text[text.index(after: index)..<close], radix: 16),
221
- let scalar = Unicode.Scalar(scalarValue) {
223
+ let scalar = Unicode.Scalar(scalarValue)
224
+ {
222
225
  result.unicodeScalars.append(scalar)
223
226
  index = text.index(after: close)
224
227
  } else {
@@ -67,10 +67,11 @@ struct ExportedParameter: Encodable, Equatable {
67
67
 
68
68
  /// One `@JS func` on a module or shared object.
69
69
  struct ExportedFunction: Encodable, Equatable {
70
- /// The Swift declaration name.
70
+ /// The Swift declaration name. A function with `.withAsync("x")` has two entries with this name.
71
71
  let name: String
72
72
 
73
- /// The JS name it binds under: the `@JS("x")` override, else `name`.
73
+ /// The JS name it binds under: the `@JS("x")` override, else `name`. The second entry of a function
74
+ /// with `.withAsync("x")` binds under `x`.
74
75
  let jsName: String
75
76
 
76
77
  var parameters: [ExportedParameter]
@@ -44,7 +44,11 @@ struct RefIndex {
44
44
  }
45
45
  for sharedObject in surface.sharedObjects {
46
46
  entries[sharedObject.name] = Entry(
47
- name: sharedObject.name, kind: .sharedObject, jsType: .object, jsName: sharedObject.jsName)
47
+ name: sharedObject.name,
48
+ kind: .sharedObject,
49
+ jsType: .object,
50
+ jsName: sharedObject.jsName
51
+ )
48
52
  }
49
53
  for union in surface.unions {
50
54
  // A union crosses as whichever alternative matched, so its category is only meaningful per
@@ -53,7 +57,10 @@ struct RefIndex {
53
57
  }
54
58
  for enumeration in surface.enums {
55
59
  entries[enumeration.name] = Entry(
56
- name: enumeration.name, kind: .enum, jsType: jsType(ofRawType: enumeration.rawType))
60
+ name: enumeration.name,
61
+ kind: .enum,
62
+ jsType: jsType(ofRawType: enumeration.rawType)
63
+ )
57
64
  }
58
65
  }
59
66
 
@@ -113,7 +120,8 @@ extension TypeNode {
113
120
  return .ref(
114
121
  name: entry.name,
115
122
  refKind: entry.kind,
116
- jsTypeOverride: entry.jsType == .object ? nil : entry.jsType)
123
+ jsTypeOverride: entry.jsType == .object ? nil : entry.jsType
124
+ )
117
125
  case .optional(let wrapped):
118
126
  return .optional(wrapped: wrapped.resolvingRefs(using: index, in: scope))
119
127
  case .array(let element):
@@ -121,7 +129,8 @@ extension TypeNode {
121
129
  case .dictionary(let key, let value):
122
130
  return .dictionary(
123
131
  key: key.resolvingRefs(using: index, in: scope),
124
- value: value.resolvingRefs(using: index, in: scope))
132
+ value: value.resolvingRefs(using: index, in: scope)
133
+ )
125
134
  case .promise(let value):
126
135
  return .promise(value: value.resolvingRefs(using: index, in: scope))
127
136
  case .function(let parameters, let returns, let isAsync, let isThrowing):
@@ -153,7 +162,8 @@ extension ExportedSurface {
153
162
  resolved.events = module.events.map { $0.resolvingRefs(using: index, in: module.name) }
154
163
  resolved.classes = module.classes.map { exportedClass in
155
164
  guard let entry = index.lookup(exportedClass.name, in: module.name),
156
- entry.kind == .sharedObject else {
165
+ entry.kind == .sharedObject
166
+ else {
157
167
  return exportedClass
158
168
  }
159
169
  return ExportedClass(name: entry.name, jsName: entry.jsName)
@@ -1,10 +1,11 @@
1
+ import SwiftParser
2
+ import SwiftSyntax
3
+
1
4
  #if canImport(FoundationEssentials)
2
5
  import FoundationEssentials
3
6
  #else
4
7
  import Foundation
5
8
  #endif
6
- import SwiftParser
7
- import SwiftSyntax
8
9
 
9
10
  extension Scanner {
10
11
  /// Runs `scan-exports` over `paths`, prints the JSON report to stdout, and returns the exit code
@@ -58,7 +59,11 @@ func scanExports(paths: [String]) -> ScanExportsResult {
58
59
  // Refs resolve only once every file has been walked: a type parsed in one file may name a type
59
60
  // declared in another, so the full set of declarations has to exist before any lookup is valid.
60
61
  let surface = ExportedSurface(
61
- modules: modules, sharedObjects: sharedObjects, records: records, enums: enums, unions: unions
62
+ modules: modules,
63
+ sharedObjects: sharedObjects,
64
+ records: records,
65
+ enums: enums,
66
+ unions: unions
62
67
  ).resolvingRefs()
63
68
 
64
69
  return ScanExportsResult(