expo-modules-macros 0.21.0 → 0.23.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.
- package/.github/workflows/linux.yml +8 -5
- package/.github/workflows/publish.yml +26 -11
- package/.github/workflows/swift-format.yml +79 -0
- package/.github/workflows/swift.yml +7 -4
- package/.github/workflows/windows.yml +8 -5
- package/AGENTS.md +16 -9
- package/README.md +20 -5
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +3 -3
- package/apple/Package.swift +1 -2
- package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +144 -30
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +184 -48
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
- package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
- package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +20 -4
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/index.d.ts +1 -1
- package/build/index.js +3 -3
- package/build/types.d.ts +32 -2
- package/build/types.js +1 -1
- package/package.json +8 -5
|
@@ -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,
|
|
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,
|
|
60
|
+
name: enumeration.name,
|
|
61
|
+
kind: .enum,
|
|
62
|
+
jsType: jsType(ofRawType: enumeration.rawType)
|
|
63
|
+
)
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
|
|
@@ -105,6 +112,9 @@ extension TypeNode {
|
|
|
105
112
|
switch self {
|
|
106
113
|
case .primitive, .unknown:
|
|
107
114
|
return self
|
|
115
|
+
case .sharedRef:
|
|
116
|
+
// The native type never reaches JS, so it isn't looked up among the scanned declarations.
|
|
117
|
+
return self
|
|
108
118
|
case .ref(let name, _, _):
|
|
109
119
|
guard let entry = index.lookup(name, in: scope) else {
|
|
110
120
|
return self
|
|
@@ -113,7 +123,8 @@ extension TypeNode {
|
|
|
113
123
|
return .ref(
|
|
114
124
|
name: entry.name,
|
|
115
125
|
refKind: entry.kind,
|
|
116
|
-
jsTypeOverride: entry.jsType == .object ? nil : entry.jsType
|
|
126
|
+
jsTypeOverride: entry.jsType == .object ? nil : entry.jsType
|
|
127
|
+
)
|
|
117
128
|
case .optional(let wrapped):
|
|
118
129
|
return .optional(wrapped: wrapped.resolvingRefs(using: index, in: scope))
|
|
119
130
|
case .array(let element):
|
|
@@ -121,7 +132,8 @@ extension TypeNode {
|
|
|
121
132
|
case .dictionary(let key, let value):
|
|
122
133
|
return .dictionary(
|
|
123
134
|
key: key.resolvingRefs(using: index, in: scope),
|
|
124
|
-
value: value.resolvingRefs(using: index, in: scope)
|
|
135
|
+
value: value.resolvingRefs(using: index, in: scope)
|
|
136
|
+
)
|
|
125
137
|
case .promise(let value):
|
|
126
138
|
return .promise(value: value.resolvingRefs(using: index, in: scope))
|
|
127
139
|
case .function(let parameters, let returns, let isAsync, let isThrowing):
|
|
@@ -153,7 +165,8 @@ extension ExportedSurface {
|
|
|
153
165
|
resolved.events = module.events.map { $0.resolvingRefs(using: index, in: module.name) }
|
|
154
166
|
resolved.classes = module.classes.map { exportedClass in
|
|
155
167
|
guard let entry = index.lookup(exportedClass.name, in: module.name),
|
|
156
|
-
entry.kind == .sharedObject
|
|
168
|
+
entry.kind == .sharedObject
|
|
169
|
+
else {
|
|
157
170
|
return exportedClass
|
|
158
171
|
}
|
|
159
172
|
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,
|
|
62
|
+
modules: modules,
|
|
63
|
+
sharedObjects: sharedObjects,
|
|
64
|
+
records: records,
|
|
65
|
+
enums: enums,
|
|
66
|
+
unions: unions
|
|
62
67
|
).resolvingRefs()
|
|
63
68
|
|
|
64
69
|
return ScanExportsResult(
|
|
@@ -100,7 +100,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
100
100
|
accessLevel: spelledAccessLevel(of: node.modifiers),
|
|
101
101
|
platforms: platforms,
|
|
102
102
|
file: file
|
|
103
|
-
)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
104
105
|
} else if inherits(from: enumerableConformanceName, in: node.inheritanceClause) {
|
|
105
106
|
let rawType = rawValueType(of: node.inheritanceClause)
|
|
106
107
|
enums.append(
|
|
@@ -111,7 +112,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
111
112
|
accessLevel: spelledAccessLevel(of: node.modifiers),
|
|
112
113
|
platforms: platforms,
|
|
113
114
|
file: file
|
|
114
|
-
)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
115
117
|
}
|
|
116
118
|
// An enum is often a namespace for other types, so its body is searched too.
|
|
117
119
|
return .visitChildren
|
|
@@ -156,7 +158,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
156
158
|
accessLevel: accessLevel,
|
|
157
159
|
platforms: platforms,
|
|
158
160
|
file: file
|
|
159
|
-
)
|
|
161
|
+
)
|
|
162
|
+
)
|
|
160
163
|
return
|
|
161
164
|
}
|
|
162
165
|
|
|
@@ -167,13 +170,15 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
167
170
|
name: qualified,
|
|
168
171
|
jsName: stringArgument(of: attribute) ?? name,
|
|
169
172
|
initializer: initializer,
|
|
173
|
+
sharedRef: sharedRefSuperclass(of: node),
|
|
170
174
|
functions: functions,
|
|
171
175
|
properties: properties,
|
|
172
176
|
events: events,
|
|
173
177
|
accessLevel: accessLevel,
|
|
174
178
|
platforms: platforms,
|
|
175
179
|
file: file
|
|
176
|
-
)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
177
182
|
return
|
|
178
183
|
}
|
|
179
184
|
|
|
@@ -185,7 +190,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
185
190
|
accessLevel: accessLevel,
|
|
186
191
|
platforms: platforms,
|
|
187
192
|
file: file
|
|
188
|
-
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
|
|
@@ -214,7 +220,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
214
220
|
|
|
215
221
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
216
222
|
varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) == nil,
|
|
217
|
-
let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue)
|
|
223
|
+
let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.event.rawValue)
|
|
224
|
+
{
|
|
218
225
|
events.append(contentsOf: makeEvents(varDecl: varDecl, attribute: attribute, platforms: platforms))
|
|
219
226
|
continue
|
|
220
227
|
}
|
|
@@ -224,7 +231,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
227
|
-
initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil
|
|
234
|
+
initDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue) != nil
|
|
235
|
+
{
|
|
228
236
|
// At most one `@JS init`; keep the first if a malformed source has more (the macro errors).
|
|
229
237
|
if initializer == nil {
|
|
230
238
|
initializer = ExportedInitializer(
|
|
@@ -237,13 +245,15 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
240
|
-
let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
|
|
241
|
-
|
|
248
|
+
let attribute = funcDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
|
|
249
|
+
{
|
|
250
|
+
functions.append(contentsOf: makeFunctions(funcDecl: funcDecl, attribute: attribute, platforms: platforms))
|
|
242
251
|
continue
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
246
|
-
let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
|
|
255
|
+
let attribute = varDecl.attributes.firstAttribute(named: DetectedMacro.js.rawValue)
|
|
256
|
+
{
|
|
247
257
|
properties.append(contentsOf: makeProperties(varDecl: varDecl, attribute: attribute, platforms: platforms))
|
|
248
258
|
}
|
|
249
259
|
}
|
|
@@ -256,10 +266,13 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
256
266
|
/// reporting one would describe a surface that does not exist. `@JS` on the same property is
|
|
257
267
|
/// caught by the caller, which reaches the `@JS` branch first.
|
|
258
268
|
private func makeEvents(
|
|
259
|
-
varDecl: VariableDeclSyntax,
|
|
269
|
+
varDecl: VariableDeclSyntax,
|
|
270
|
+
attribute: AttributeSyntax,
|
|
271
|
+
platforms: [ExportedPlatform]
|
|
260
272
|
) -> [ExportedEvent] {
|
|
261
273
|
guard varDecl.bindingSpecifier.tokenKind != .keyword(.let),
|
|
262
|
-
!isTypeLevel(varDecl.modifiers)
|
|
274
|
+
!isTypeLevel(varDecl.modifiers)
|
|
275
|
+
else {
|
|
263
276
|
return []
|
|
264
277
|
}
|
|
265
278
|
let override = stringArgument(of: attribute)
|
|
@@ -274,7 +287,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
274
287
|
binding.accessorBlock == nil,
|
|
275
288
|
let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type),
|
|
276
289
|
isVoidEventReturn(functionType.returnClause.type),
|
|
277
|
-
functionType.parameters.count <= 1
|
|
290
|
+
functionType.parameters.count <= 1
|
|
291
|
+
else {
|
|
278
292
|
continue
|
|
279
293
|
}
|
|
280
294
|
let name = ident.identifier.identifierName
|
|
@@ -286,38 +300,57 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
286
300
|
isSync: isSync,
|
|
287
301
|
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
288
302
|
platforms: platforms
|
|
289
|
-
)
|
|
303
|
+
)
|
|
304
|
+
)
|
|
290
305
|
}
|
|
291
306
|
return result
|
|
292
307
|
}
|
|
293
308
|
|
|
294
|
-
/// Builds
|
|
295
|
-
/// `nil`, and the effect/static flags.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
309
|
+
/// Builds the `ExportedFunction` entries for a `@JS func`: JS-name fallback, parameters, a `Void`
|
|
310
|
+
/// return as `nil`, and the effect/static flags. A synchronous function with `.withAsync("name")`
|
|
311
|
+
/// gets a second, `async` entry under that name, for the promise-returning variant the macro binds.
|
|
312
|
+
private func makeFunctions(
|
|
313
|
+
funcDecl: FunctionDeclSyntax,
|
|
314
|
+
attribute: AttributeSyntax,
|
|
315
|
+
platforms: [ExportedPlatform]
|
|
316
|
+
) -> [ExportedFunction] {
|
|
299
317
|
let effects = funcDecl.signature.effectSpecifiers
|
|
300
318
|
let returnType = funcDecl.signature.returnClause?.type
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
319
|
+
let isAsync = effects?.asyncSpecifier != nil
|
|
320
|
+
let exportedParameters = parameters(of: funcDecl.signature.parameterClause)
|
|
321
|
+
let returns = isVoidType(returnType) ? nil : returnType.map { typeNode(from: $0) }
|
|
322
|
+
let makeFunction = { (jsName: String, isAsync: Bool) in
|
|
323
|
+
ExportedFunction(
|
|
324
|
+
name: funcDecl.name.identifierName,
|
|
325
|
+
jsName: jsName,
|
|
326
|
+
parameters: exportedParameters,
|
|
327
|
+
returns: returns,
|
|
328
|
+
isAsync: isAsync,
|
|
329
|
+
isThrowing: effects?.throwsClause?.throwsSpecifier != nil,
|
|
330
|
+
isStatic: isTypeLevel(funcDecl.modifiers),
|
|
331
|
+
accessLevel: spelledAccessLevel(of: funcDecl.modifiers),
|
|
332
|
+
platforms: platforms
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
let function = makeFunction(stringArgument(of: attribute) ?? funcDecl.name.identifierName, isAsync)
|
|
336
|
+
// The macro binds no variant for an `async` function: it reports the option there instead.
|
|
337
|
+
guard !isAsync, let variantName = asyncVariantName(of: attribute) else {
|
|
338
|
+
return [function]
|
|
339
|
+
}
|
|
340
|
+
return [function, makeFunction(variantName, true)]
|
|
312
341
|
}
|
|
313
342
|
|
|
314
343
|
/// Builds the `ExportedProperty` entries for a `@JS var`/`let`. One declaration can introduce several
|
|
315
344
|
/// bindings (`var a, b: Int`), so this returns an array. The value type is the annotation, else the
|
|
316
345
|
/// literal default's inferred type, else `nil`.
|
|
317
346
|
private func makeProperties(
|
|
318
|
-
varDecl: VariableDeclSyntax,
|
|
347
|
+
varDecl: VariableDeclSyntax,
|
|
348
|
+
attribute: AttributeSyntax,
|
|
349
|
+
platforms: [ExportedPlatform]
|
|
319
350
|
) -> [ExportedProperty] {
|
|
320
351
|
let isLet = varDecl.bindingSpecifier.tokenKind == .keyword(.let)
|
|
352
|
+
// A `lazy var` is read-only from JS, like a `let`: the macro binds it getter-only.
|
|
353
|
+
let isLazy = varDecl.modifiers.contains { $0.name.tokenKind == .keyword(.lazy) }
|
|
321
354
|
let isStatic = isTypeLevel(varDecl.modifiers)
|
|
322
355
|
let override = stringArgument(of: attribute)
|
|
323
356
|
var result: [ExportedProperty] = []
|
|
@@ -333,13 +366,14 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
333
366
|
name: name,
|
|
334
367
|
jsName: override ?? name,
|
|
335
368
|
type: type,
|
|
336
|
-
isSettable: isSettable(binding: binding, isLet: isLet),
|
|
369
|
+
isSettable: !isLazy && isSettable(binding: binding, isLet: isLet),
|
|
337
370
|
isStatic: isStatic,
|
|
338
371
|
// Only a stored property has an initializer; a computed one has none to report.
|
|
339
372
|
defaultValue: binding.initializer?.value.trimmedDescription,
|
|
340
373
|
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
341
374
|
platforms: platforms
|
|
342
|
-
)
|
|
375
|
+
)
|
|
376
|
+
)
|
|
343
377
|
}
|
|
344
378
|
return result
|
|
345
379
|
}
|
|
@@ -380,7 +414,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
380
414
|
// `@Record` reads only the properties written directly in the body, so `#if` is not looked into.
|
|
381
415
|
for member in members {
|
|
382
416
|
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
383
|
-
!isExcludedRecordModifier(varDecl.modifiers)
|
|
417
|
+
!isExcludedRecordModifier(varDecl.modifiers)
|
|
418
|
+
else {
|
|
384
419
|
continue
|
|
385
420
|
}
|
|
386
421
|
let platforms = platformResolver.platforms(of: varDecl)
|
|
@@ -406,7 +441,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
406
441
|
defaultValue: binding.initializer?.value.trimmedDescription,
|
|
407
442
|
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
408
443
|
platforms: platforms
|
|
409
|
-
)
|
|
444
|
+
)
|
|
445
|
+
)
|
|
410
446
|
}
|
|
411
447
|
}
|
|
412
448
|
return properties
|
|
@@ -440,7 +476,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
440
476
|
name: name,
|
|
441
477
|
rawValue: writtenRawValue(of: element) ?? derivedStringRawValue(for: name, rawType: rawType),
|
|
442
478
|
platforms: platforms
|
|
443
|
-
)
|
|
479
|
+
)
|
|
480
|
+
)
|
|
444
481
|
}
|
|
445
482
|
}
|
|
446
483
|
return cases
|
|
@@ -467,12 +504,17 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
467
504
|
for element in caseDecl.elements {
|
|
468
505
|
guard let parameters = element.parameterClause?.parameters,
|
|
469
506
|
parameters.count == 1, let parameter = parameters.first,
|
|
470
|
-
parameter.defaultValue == nil
|
|
507
|
+
parameter.defaultValue == nil
|
|
508
|
+
else {
|
|
471
509
|
continue
|
|
472
510
|
}
|
|
473
511
|
result.append(
|
|
474
512
|
ExportedUnionMember(
|
|
475
|
-
name: element.name.identifierName,
|
|
513
|
+
name: element.name.identifierName,
|
|
514
|
+
type: typeNode(from: parameter.type),
|
|
515
|
+
platforms: platforms
|
|
516
|
+
)
|
|
517
|
+
)
|
|
476
518
|
}
|
|
477
519
|
}
|
|
478
520
|
return result
|
|
@@ -508,7 +550,8 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
508
550
|
}
|
|
509
551
|
guard let item = node.parent?.as(MemberBlockItemSyntax.self),
|
|
510
552
|
let parent = declaration(owning: item),
|
|
511
|
-
let scope = enclosingTypes(of: parent)
|
|
553
|
+
let scope = enclosingTypes(of: parent)
|
|
554
|
+
else {
|
|
512
555
|
return nil
|
|
513
556
|
}
|
|
514
557
|
if let decl = parent.as(ExtensionDeclSyntax.self) {
|
|
@@ -633,7 +676,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
633
676
|
return underlyingFunctionType(of: attributed.baseType)
|
|
634
677
|
}
|
|
635
678
|
if let tuple = type.as(TupleTypeSyntax.self),
|
|
636
|
-
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
679
|
+
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
680
|
+
{
|
|
637
681
|
return underlyingFunctionType(of: element.type)
|
|
638
682
|
}
|
|
639
683
|
return type.as(FunctionTypeSyntax.self)
|
|
@@ -643,7 +687,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
643
687
|
/// accepts neither `Swift.Void` nor a parenthesized `(Void)`, so it would drop valid events.
|
|
644
688
|
private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
645
689
|
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
646
|
-
let element = tuple.elements.first, element.firstName == nil
|
|
690
|
+
let element = tuple.elements.first, element.firstName == nil
|
|
691
|
+
{
|
|
647
692
|
return isVoidEventReturn(element.type)
|
|
648
693
|
}
|
|
649
694
|
let text = type.trimmedDescription
|
|
@@ -664,6 +709,89 @@ private func boolArgument(of attribute: AttributeSyntax, label: String) -> Bool?
|
|
|
664
709
|
return nil
|
|
665
710
|
}
|
|
666
711
|
|
|
712
|
+
/// The name in a `@JS` attribute's `.withAsync("name")` option, in the arguments or inside an array
|
|
713
|
+
/// literal (`@JS("load", [.withAsync("loadAsync")])`), or `nil` when the option is absent or its
|
|
714
|
+
/// argument isn't a plain string literal. Mirrors `asyncVariantOption` and `asyncVariantName` in the
|
|
715
|
+
/// macros.
|
|
716
|
+
private func asyncVariantName(of attribute: AttributeSyntax) -> String? {
|
|
717
|
+
guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
718
|
+
return nil
|
|
719
|
+
}
|
|
720
|
+
let expressions = arguments.flatMap { argument -> [ExprSyntax] in
|
|
721
|
+
if let array = argument.expression.as(ArrayExprSyntax.self) {
|
|
722
|
+
return array.elements.map(\.expression)
|
|
723
|
+
}
|
|
724
|
+
return [argument.expression]
|
|
725
|
+
}
|
|
726
|
+
for expression in expressions {
|
|
727
|
+
guard let call = expression.as(FunctionCallExprSyntax.self),
|
|
728
|
+
let member = call.calledExpression.as(MemberAccessExprSyntax.self),
|
|
729
|
+
member.declName.baseName.text == "withAsync"
|
|
730
|
+
else {
|
|
731
|
+
continue
|
|
732
|
+
}
|
|
733
|
+
guard call.arguments.count == 1,
|
|
734
|
+
let argument = call.arguments.first,
|
|
735
|
+
argument.label == nil,
|
|
736
|
+
let literal = argument.expression.as(StringLiteralExprSyntax.self),
|
|
737
|
+
literal.segments.count == 1,
|
|
738
|
+
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
739
|
+
else {
|
|
740
|
+
return nil
|
|
741
|
+
}
|
|
742
|
+
return segment.content.text
|
|
743
|
+
}
|
|
744
|
+
return nil
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// The `SharedRef<T>` a class names as its superclass, or `nil` when it names another one or none.
|
|
748
|
+
/// Swift requires the superclass to come first in the inheritance clause, so only that entry is read.
|
|
749
|
+
private func sharedRefSuperclass(of node: some DeclGroupSyntax) -> ExportedSharedRef? {
|
|
750
|
+
guard let superclass = node.inheritanceClause?.inheritedTypes.first?.type,
|
|
751
|
+
case .sharedRef(let native) = typeNode(from: superclass)
|
|
752
|
+
else {
|
|
753
|
+
return nil
|
|
754
|
+
}
|
|
755
|
+
return ExportedSharedRef(native: native, nativeRefType: nativeRefType(in: node.memberBlock.members))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/// The value of a `nativeRefType` override that returns a plain string literal: `"image"` in
|
|
759
|
+
/// `override var nativeRefType: String { "image" }`, also with `return` or inside a `get` block.
|
|
760
|
+
/// `unknown`, core's default, when the class declares no `nativeRefType`, and `nil` when its getter is
|
|
761
|
+
/// anything else.
|
|
762
|
+
private func nativeRefType(in members: MemberBlockItemListSyntax) -> String? {
|
|
763
|
+
let binding = members.lazy
|
|
764
|
+
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
|
|
765
|
+
.filter { !isTypeLevel($0.modifiers) }
|
|
766
|
+
.flatMap { $0.bindings }
|
|
767
|
+
.first { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "nativeRefType" }
|
|
768
|
+
guard let binding else {
|
|
769
|
+
return "unknown"
|
|
770
|
+
}
|
|
771
|
+
let statements: CodeBlockItemListSyntax?
|
|
772
|
+
switch binding.accessorBlock?.accessors {
|
|
773
|
+
case .getter(let body):
|
|
774
|
+
statements = body
|
|
775
|
+
case .accessors(let accessors):
|
|
776
|
+
statements = accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) }?.body?.statements
|
|
777
|
+
case nil:
|
|
778
|
+
statements = nil
|
|
779
|
+
}
|
|
780
|
+
guard let statements, statements.count == 1, let item = statements.first?.item else {
|
|
781
|
+
return nil
|
|
782
|
+
}
|
|
783
|
+
let expression = item.as(ReturnStmtSyntax.self)?.expression ?? item.as(ExprSyntax.self)
|
|
784
|
+
guard let literal = expression?.as(StringLiteralExprSyntax.self),
|
|
785
|
+
literal.segments.count == 1,
|
|
786
|
+
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
787
|
+
else {
|
|
788
|
+
return nil
|
|
789
|
+
}
|
|
790
|
+
// An escape would need real unescaping, so it's treated as unreadable, as in `writtenRawValue(of:)`.
|
|
791
|
+
let content = segment.content.text
|
|
792
|
+
return content.contains("\\") ? nil : content
|
|
793
|
+
}
|
|
794
|
+
|
|
667
795
|
private func isTypeLevel(_ modifiers: DeclModifierListSyntax) -> Bool {
|
|
668
796
|
modifiers.contains {
|
|
669
797
|
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
@@ -721,7 +849,8 @@ private func writtenRawValue(of element: EnumCaseElementSyntax) -> String? {
|
|
|
721
849
|
return value.trimmedDescription
|
|
722
850
|
}
|
|
723
851
|
guard literal.segments.count == 1,
|
|
724
|
-
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
852
|
+
let segment = literal.segments.first?.as(StringSegmentSyntax.self)
|
|
853
|
+
else {
|
|
725
854
|
return nil
|
|
726
855
|
}
|
|
727
856
|
// The segment's text is the literal's content with its delimiters already stripped, so a plain
|
|
@@ -777,7 +906,8 @@ private let knownNonRawValueProtocols: Set<String> = [
|
|
|
777
906
|
func rawValueType(of clause: InheritanceClauseSyntax?) -> TypeNode? {
|
|
778
907
|
guard let first = clause?.inheritedTypes.first?.type,
|
|
779
908
|
let trailing = first.trimmedDescription.split(separator: ".").last.map(String.init),
|
|
780
|
-
!knownNonRawValueProtocols.contains(trailing)
|
|
909
|
+
!knownNonRawValueProtocols.contains(trailing)
|
|
910
|
+
else {
|
|
781
911
|
return nil
|
|
782
912
|
}
|
|
783
913
|
return typeNode(from: first)
|
|
@@ -801,7 +931,8 @@ extension AttributeListSyntax {
|
|
|
801
931
|
fileprivate func firstAttribute(named name: String) -> AttributeSyntax? {
|
|
802
932
|
for element in self {
|
|
803
933
|
if let attribute = element.as(AttributeSyntax.self),
|
|
804
|
-
attribute.attributeName.trimmedDescription == name
|
|
934
|
+
attribute.attributeName.trimmedDescription == name
|
|
935
|
+
{
|
|
805
936
|
return attribute
|
|
806
937
|
}
|
|
807
938
|
}
|
|
@@ -814,14 +945,16 @@ extension AttributeListSyntax {
|
|
|
814
945
|
/// an array literal. A local copy of the macros' helper (the macro target can't be imported here).
|
|
815
946
|
private func classListArgument(of attribute: AttributeSyntax, label: String) -> [String] {
|
|
816
947
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
817
|
-
let array = args.first(where: { $0.label?.text == label })?.expression.as(ArrayExprSyntax.self)
|
|
948
|
+
let array = args.first(where: { $0.label?.text == label })?.expression.as(ArrayExprSyntax.self)
|
|
949
|
+
else {
|
|
818
950
|
return []
|
|
819
951
|
}
|
|
820
952
|
return array.elements.compactMap { element in
|
|
821
953
|
guard let memberAccess = element.expression.as(MemberAccessExprSyntax.self),
|
|
822
954
|
memberAccess.declName.baseName.text == "self",
|
|
823
955
|
let base = memberAccess.base,
|
|
824
|
-
base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self)
|
|
956
|
+
base.is(DeclReferenceExprSyntax.self) || base.is(MemberAccessExprSyntax.self)
|
|
957
|
+
else {
|
|
825
958
|
return nil
|
|
826
959
|
}
|
|
827
960
|
return base.trimmedDescription
|
|
@@ -8,8 +8,8 @@ import SwiftSyntax
|
|
|
8
8
|
/// never silently dropped.
|
|
9
9
|
|
|
10
10
|
/// The runtime category mirroring JavaScript's `typeof`. A coarse companion to `TypeNode.kind`: every
|
|
11
|
-
/// structured object kind (array, dictionary, promise, ref) reports `object`. `symbol` is
|
|
12
|
-
/// completeness; the scanner doesn't produce it.
|
|
11
|
+
/// structured object kind (array, dictionary, promise, ref, sharedRef) reports `object`. `symbol` is
|
|
12
|
+
/// included for completeness; the scanner doesn't produce it.
|
|
13
13
|
enum JSType: String, Encodable {
|
|
14
14
|
case undefined
|
|
15
15
|
case object
|
|
@@ -72,6 +72,11 @@ indirect enum TypeNode: Equatable {
|
|
|
72
72
|
/// node parsed at a use site has never seen.
|
|
73
73
|
case ref(name: String, refKind: RefKind? = nil, jsTypeOverride: JSType? = nil)
|
|
74
74
|
|
|
75
|
+
/// `SharedRef<T>` (or `ExpoModulesCore.SharedRef<T>`): a shared object holding a native `T`, which
|
|
76
|
+
/// JS sees as an instance of `expo.SharedRef`. `native` is `T` parsed like any other type, but it
|
|
77
|
+
/// never crosses the boundary itself, so refs in it are not resolved.
|
|
78
|
+
case sharedRef(native: TypeNode)
|
|
79
|
+
|
|
75
80
|
/// A spelling the parser doesn't model (generic parameter, tuple, metatype, …), kept verbatim so
|
|
76
81
|
/// nothing is lost. Has no `typeof`.
|
|
77
82
|
case unknown(text: String)
|
|
@@ -82,7 +87,7 @@ indirect enum TypeNode: Equatable {
|
|
|
82
87
|
switch self {
|
|
83
88
|
case .primitive(_, let jsType):
|
|
84
89
|
return jsType
|
|
85
|
-
case .array, .dictionary, .promise:
|
|
90
|
+
case .array, .dictionary, .promise, .sharedRef:
|
|
86
91
|
return .object
|
|
87
92
|
case .ref(_, _, let jsTypeOverride):
|
|
88
93
|
// An unresolved ref, and every resolved one but an enum, is an object.
|
|
@@ -105,7 +110,7 @@ extension TypeNode: Encodable {
|
|
|
105
110
|
// The `typeof` category. Spelled `typeof` in JSON (what the consumer reads); the Swift property is
|
|
106
111
|
// `jsType` since it's the cleaner Swift name.
|
|
107
112
|
case jsType = "typeof"
|
|
108
|
-
case wrapped, element, key, value, parameters, returns, text
|
|
113
|
+
case wrapped, element, key, value, parameters, returns, text, native
|
|
109
114
|
// A closure type's effects, matching `ExportedFunction`'s TS-keyword spellings.
|
|
110
115
|
case isAsync = "async"
|
|
111
116
|
case isThrowing = "throws"
|
|
@@ -143,6 +148,9 @@ extension TypeNode: Encodable {
|
|
|
143
148
|
try container.encode("ref", forKey: .kind)
|
|
144
149
|
try container.encode(name, forKey: .name)
|
|
145
150
|
try container.encodeIfPresent(refKind, forKey: .refKind)
|
|
151
|
+
case .sharedRef(let native):
|
|
152
|
+
try container.encode("sharedRef", forKey: .kind)
|
|
153
|
+
try container.encode(native, forKey: .native)
|
|
146
154
|
case .unknown(let text):
|
|
147
155
|
try container.encode("unknown", forKey: .kind)
|
|
148
156
|
try container.encode(text, forKey: .text)
|
|
@@ -201,15 +209,23 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
|
|
|
201
209
|
}
|
|
202
210
|
// `(T)`: one unlabeled element in parentheses is the same type as `T`, as in `((Int) -> Void)?`.
|
|
203
211
|
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
204
|
-
let element = tuple.elements.first, element.firstName == nil
|
|
212
|
+
let element = tuple.elements.first, element.firstName == nil
|
|
213
|
+
{
|
|
205
214
|
return typeNode(from: element.type)
|
|
206
215
|
}
|
|
207
216
|
// A nominal type, possibly generic: `Int`, `Point`, `Optional<T>`, `Array<T>`, `Promise<T>`.
|
|
208
217
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
209
218
|
return nominalNode(name: identifier.name.identifierName, generics: identifier.genericArgumentClause)
|
|
210
219
|
}
|
|
211
|
-
// A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling
|
|
220
|
+
// A qualified nominal type: `Foo.Bar`. Kept as a ref under its full spelling, except core's own
|
|
221
|
+
// `ExpoModulesCore.SharedRef<T>`, which is the same type as a bare `SharedRef<T>`.
|
|
212
222
|
if let member = type.as(MemberTypeSyntax.self) {
|
|
223
|
+
if member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "ExpoModulesCore",
|
|
224
|
+
member.name.text == "SharedRef",
|
|
225
|
+
let generics = member.genericArgumentClause
|
|
226
|
+
{
|
|
227
|
+
return nominalNode(name: "SharedRef", generics: generics)
|
|
228
|
+
}
|
|
213
229
|
return .ref(name: member.trimmedDescription)
|
|
214
230
|
}
|
|
215
231
|
// Tuples, metatypes, some/any types, etc.: not modeled, preserve the verbatim spelling.
|
|
@@ -217,8 +233,8 @@ func typeNode(from type: TypeSyntax) -> TypeNode {
|
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
/// A nominal type's node. The standard generic wrappers normalize to their sugared node
|
|
220
|
-
/// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`)
|
|
221
|
-
/// else a ref; any other generic is a ref under its full spelling.
|
|
236
|
+
/// (`Optional<T>`/`Array<T>`/`Dictionary<K,V>`/`Promise<T>`) and `SharedRef<T>` to its own node; a
|
|
237
|
+
/// bare name is a primitive when known, else a ref; any other generic is a ref under its full spelling.
|
|
222
238
|
private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -> TypeNode {
|
|
223
239
|
guard let generics else {
|
|
224
240
|
if let jsType = primitiveJSTypes[name] {
|
|
@@ -237,6 +253,8 @@ private func nominalNode(name: String, generics: GenericArgumentClauseSyntax?) -
|
|
|
237
253
|
return .dictionary(key: arguments[0], value: arguments[1])
|
|
238
254
|
case ("Promise", 1):
|
|
239
255
|
return .promise(value: arguments[0])
|
|
256
|
+
case ("SharedRef", 1):
|
|
257
|
+
return .sharedRef(native: arguments[0])
|
|
240
258
|
default:
|
|
241
259
|
// An unmodeled generic (`Set<Int>`, `Either<A, B>`, …). Keep the whole spelling as a ref so the
|
|
242
260
|
// name and its arguments survive for the generator to interpret or flag.
|
|
@@ -296,6 +314,8 @@ extension TypeNode {
|
|
|
296
314
|
return "[\(key.spelling): \(value.spelling)]"
|
|
297
315
|
case .promise(let value):
|
|
298
316
|
return "Promise<\(value.spelling)>"
|
|
317
|
+
case .sharedRef(let native):
|
|
318
|
+
return "SharedRef<\(native.spelling)>"
|
|
299
319
|
case .function(let parameters, let returns, _, _):
|
|
300
320
|
return "(\(parameters.map { $0.spelling }.joined(separator: ", "))) -> \(returns?.spelling ?? "Void")"
|
|
301
321
|
case .unknown(let text):
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import SwiftParser
|
|
2
|
+
|
|
1
3
|
#if canImport(FoundationEssentials)
|
|
2
4
|
import FoundationEssentials
|
|
3
5
|
#else
|
|
4
6
|
import Foundation
|
|
5
7
|
#endif
|
|
6
|
-
import SwiftParser
|
|
7
8
|
|
|
8
9
|
/// The scanner's public entry point. Argument parsing, subcommand dispatch, and usage text live in
|
|
9
10
|
/// the CLI target; this just runs a command and writes its JSON report to stdout.
|
|
@@ -122,7 +123,12 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
122
123
|
var platformsByDetection: [String: [String]] = [:]
|
|
123
124
|
for platform in platformUniverse {
|
|
124
125
|
let configuration = ScanBuildConfiguration(platform: platform.condition, defines: defines)
|
|
125
|
-
let visitor = DetectionVisitor(
|
|
126
|
+
let visitor = DetectionVisitor(
|
|
127
|
+
file: file,
|
|
128
|
+
tree: tree,
|
|
129
|
+
detectedMacros: [.expoModule],
|
|
130
|
+
configuration: configuration
|
|
131
|
+
)
|
|
126
132
|
visitor.walk(tree)
|
|
127
133
|
for detection in visitor.detections {
|
|
128
134
|
platformsByDetection[detectionKey(detection), default: []].append(platform.reported)
|
package/build/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export declare class ScannerSchemaVersionError extends Error {
|
|
|
43
43
|
* `bin` directory.
|
|
44
44
|
* - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
|
|
45
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 `
|
|
46
|
+
* installed, for example in this repository, it falls back to the executable that `pnpm run build`
|
|
47
47
|
* writes to `platforms/<platform>-<arch>`.
|
|
48
48
|
*
|
|
49
49
|
* The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to
|