@expo/expo-modules-macros-plugin 0.5.1 → 0.6.1
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/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +51 -116
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +8 -16
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +43 -22
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +6 -24
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +4 -5
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +50 -27
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +88 -34
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +3 -2
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +23 -7
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +186 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +47 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +307 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +255 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +5 -5
- package/apple/Sources/ExpoModulesScannerCLI/main.swift +4 -3
- package/package.json +1 -1
|
@@ -29,10 +29,13 @@ import SwiftSyntaxMacros
|
|
|
29
29
|
an optional type makes it nullable and optional, and a non-optional property without a
|
|
30
30
|
default is required (the factories throw when the source omits it).
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
33
|
+
(`from(object:)` / `toObject(appContext:)`); the native-`Any` dictionary paths still go through the
|
|
34
|
+
public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
|
|
35
|
+
(`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
|
|
36
|
+
values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
37
|
+
user modules without any internal core symbols. Every property type must therefore conform to both
|
|
38
|
+
`AnyArgument` and `JavaScriptDecodable & JavaScriptEncodable`.
|
|
36
39
|
|
|
37
40
|
For classes that inherit from another `@Record`-annotated class, the synthesized
|
|
38
41
|
methods chain to `super` so inherited properties are handled first.
|
|
@@ -60,14 +63,15 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
60
63
|
|
|
61
64
|
var members: [DeclSyntax] = []
|
|
62
65
|
|
|
63
|
-
// A single never-called member that makes the compiler verify each property type is
|
|
64
|
-
//
|
|
65
|
-
// own named assertion inside, so the compiler's
|
|
66
|
-
// property (see `typeConformanceAssertions`). Emitted
|
|
67
|
-
// this clear "requires that '…' conform to '…'" error
|
|
68
|
-
// "no member '
|
|
66
|
+
// A single never-called member that makes the compiler verify each property type is convertible
|
|
67
|
+
// both ways (the JS-value paths call `decode`/`encode`; the native-`Any` dictionary paths call
|
|
68
|
+
// the dynamic-type API). Each property keeps its own named assertion inside, so the compiler's
|
|
69
|
+
// conformance diagnostic names the offending property (see `typeConformanceAssertions`). Emitted
|
|
70
|
+
// first so that, for a non-conforming type, this clear "requires that '…' conform to '…'" error
|
|
71
|
+
// is reported ahead of the noisier "no member 'decode'"/"getDynamicType" errors from the
|
|
72
|
+
// conversion code below.
|
|
69
73
|
let assertions = properties.map { ConformanceAssertion(name: $0.name, types: [$0.type]) }
|
|
70
|
-
if let assertionMember = typeConformanceAssertions(for: assertions) {
|
|
74
|
+
if let assertionMember = typeConformanceAssertions(for: assertions, constraint: recordFieldProtocolName) {
|
|
71
75
|
members.append(assertionMember)
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -131,8 +135,9 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
131
135
|
*/
|
|
132
136
|
private struct RecordProperty {
|
|
133
137
|
let name: String
|
|
134
|
-
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build
|
|
135
|
-
///
|
|
138
|
+
/// The property's declared type, verbatim (e.g. `String`, `Int`, `String?`). Used to build the
|
|
139
|
+
/// memberwise-init parameter and as the receiver of the per-property `decode`/`encode` (and, on the
|
|
140
|
+
/// dictionary paths, `getDynamicType()`) conversions.
|
|
136
141
|
let type: String
|
|
137
142
|
/// The default-value expression verbatim (`0`, `""`, `[]`), or `nil` when the property has none.
|
|
138
143
|
/// Inlined into the memberwise init and the factories' omitted-property branch so the synthesized
|
|
@@ -155,7 +160,7 @@ private struct RecordProperty {
|
|
|
155
160
|
/**
|
|
156
161
|
Discovers the record's properties: every stored `var`/`let` binding that is not `static`,
|
|
157
162
|
`private`, `fileprivate`, `lazy`, or computed. Each property must declare an explicit type
|
|
158
|
-
annotation, since the synthesized conversions
|
|
163
|
+
annotation, since the synthesized conversions name the type (e.g. `Type.decode(…)`).
|
|
159
164
|
*/
|
|
160
165
|
private func recordProperties(
|
|
161
166
|
of declaration: some DeclGroupSyntax
|
|
@@ -271,11 +276,20 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
271
276
|
/**
|
|
272
277
|
`from(object:appContext:)` — reads each property off the `JavaScriptObject` into a local,
|
|
273
278
|
then constructs the record through the memberwise init. Required properties throw when
|
|
274
|
-
undefined; defaulted properties fall back to the property's declared default (
|
|
275
|
-
|
|
279
|
+
undefined; defaulted properties fall back to the property's declared default (inlined) when
|
|
280
|
+
undefined; optional properties become `nil` when undefined/null. Each property is decoded with
|
|
281
|
+
`JavaScriptDecodable.decode`, so the factory binds the `runtime` from the app context once and
|
|
282
|
+
threads it to every read.
|
|
276
283
|
*/
|
|
277
284
|
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
278
|
-
|
|
285
|
+
var lines: [String] = []
|
|
286
|
+
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
287
|
+
// otherwise leave it unused.
|
|
288
|
+
if !properties.isEmpty {
|
|
289
|
+
lines.append(" let runtime = try appContext.runtime")
|
|
290
|
+
}
|
|
291
|
+
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
292
|
+
let body = lines.joined(separator: "\n")
|
|
279
293
|
return """
|
|
280
294
|
@JavaScriptActor
|
|
281
295
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
@@ -311,23 +325,25 @@ private func factoryBody(properties: [RecordProperty], readLines: [String]) -> S
|
|
|
311
325
|
return lines.joined(separator: "\n")
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
/// Per-property read statements for the JS-object factory, each producing a `let <name
|
|
328
|
+
/// Per-property read statements for the JS-object factory, each producing a `let <name>` by decoding
|
|
329
|
+
/// the JS value with `JavaScriptDecodable.decode` (recovering the app context from `runtime` itself).
|
|
315
330
|
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
316
331
|
var lines: [String] = []
|
|
317
332
|
for property in properties {
|
|
318
333
|
let valueVar = "\(property.name)JSValue"
|
|
319
334
|
let exprType = expressionType(property.type)
|
|
320
|
-
let
|
|
335
|
+
let decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
321
336
|
lines.append(" let \(valueVar) = object.getProperty(\"\(property.name)\")")
|
|
322
337
|
if property.isRequired {
|
|
323
338
|
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
324
339
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.name)\")")
|
|
325
340
|
lines.append(" }")
|
|
326
|
-
lines.append(" let \(property.name) = \(
|
|
341
|
+
lines.append(" let \(property.name) = \(decode)")
|
|
327
342
|
} else if property.isOptional {
|
|
328
|
-
|
|
343
|
+
// `Optional.decode` already maps `undefined`/`null` to `nil`, so the read is a plain decode.
|
|
344
|
+
lines.append(" let \(property.name) = \(decode)")
|
|
329
345
|
} else {
|
|
330
|
-
lines.append(" let \(property.name) = \(valueVar).isUndefined() ? \(property.defaultValue!) : \(
|
|
346
|
+
lines.append(" let \(property.name) = \(valueVar).isUndefined() ? \(property.defaultValue!) : \(decode)")
|
|
331
347
|
}
|
|
332
348
|
}
|
|
333
349
|
return lines
|
|
@@ -388,20 +404,27 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
|
|
|
388
404
|
}
|
|
389
405
|
|
|
390
406
|
/**
|
|
391
|
-
`toObject(appContext:)` — builds a `JavaScriptObject` directly,
|
|
392
|
-
`
|
|
393
|
-
|
|
407
|
+
`toObject(appContext:)` — builds a `JavaScriptObject` directly, encoding each property with
|
|
408
|
+
`JavaScriptEncodable.encode`. The fast write path mirroring `from(object:)`. The `runtime` is bound
|
|
409
|
+
from the app context once and threaded to every write. Subclasses chain to `super` so inherited
|
|
410
|
+
properties are written first.
|
|
394
411
|
*/
|
|
395
412
|
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
396
413
|
let overrideKeyword = inheritsRecord ? "override " : ""
|
|
397
414
|
var lines: [String] = []
|
|
415
|
+
// The base case needs the runtime to create the object; the inheriting case only needs it when
|
|
416
|
+
// there's a property to encode (it chains to `super` for the object itself). Binding it when unused
|
|
417
|
+
// would warn.
|
|
418
|
+
if !inheritsRecord || !properties.isEmpty {
|
|
419
|
+
lines.append(" let runtime = try appContext.runtime")
|
|
420
|
+
}
|
|
398
421
|
if inheritsRecord {
|
|
399
422
|
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
400
423
|
} else {
|
|
401
|
-
lines.append(" let object =
|
|
424
|
+
lines.append(" let object = runtime.createObject()")
|
|
402
425
|
}
|
|
403
426
|
for property in properties {
|
|
404
|
-
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).
|
|
427
|
+
lines.append(" object.setProperty(\"\(property.name)\", value: try \(expressionType(property.type)).encode(self.\(property.name), in: runtime))")
|
|
405
428
|
}
|
|
406
429
|
lines.append(" return object")
|
|
407
430
|
let body = lines.joined(separator: "\n")
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import SwiftSyntax
|
|
2
2
|
|
|
3
3
|
/// The protocol that every type crossing the JS boundary must conform to. Centralized here so the
|
|
4
|
-
/// eventual rename (this is a placeholder name) is a single edit, and shared by
|
|
5
|
-
///
|
|
4
|
+
/// eventual rename (this is a placeholder name) is a single edit, and shared by the macros whose
|
|
5
|
+
/// generated conversions go through the dynamic-type API (`@Record`, `@Event`).
|
|
6
6
|
internal let jsConvertibleProtocolName = "AnyArgument"
|
|
7
7
|
|
|
8
|
+
/// The constraint a `@JS` boundary type must satisfy, by direction: an argument (and a settable
|
|
9
|
+
/// property's incoming value) is decoded, so it must be `JavaScriptDecodable`; a return value (and a
|
|
10
|
+
/// property's outgoing value) is encoded, so it must be `JavaScriptEncodable`. Asserting each
|
|
11
|
+
/// direction on its own keeps a decode-only or encode-only type from being over-constrained, and
|
|
12
|
+
/// surfaces a missing conformance as a clear "requires that '…' conform to …" diagnostic on the
|
|
13
|
+
/// member instead of an opaque error inside the generated closure.
|
|
14
|
+
internal let javaScriptDecodableProtocolName = "JavaScriptDecodable"
|
|
15
|
+
internal let javaScriptEncodableProtocolName = "JavaScriptEncodable"
|
|
16
|
+
|
|
17
|
+
/// The constraint a `@Record` property type must satisfy: it must be both `AnyArgument` (the
|
|
18
|
+
/// `from(dictionary:)` / `toDictionary(appContext:)` paths still convert native `Any` through the
|
|
19
|
+
/// dynamic-type API) and `JavaScriptDecodable & JavaScriptEncodable` (the `from(object:)` /
|
|
20
|
+
/// `toObject(appContext:)` paths convert JS values through `decode`/`encode`). A record field has to
|
|
21
|
+
/// support both directions, so the assertion requires the intersection.
|
|
22
|
+
internal let recordFieldProtocolName = "AnyArgument & JavaScriptDecodable & JavaScriptEncodable"
|
|
23
|
+
|
|
8
24
|
/// The protocol a type must conform to for `self.emit(event:…)` to resolve; core conforms
|
|
9
25
|
/// `BaseModule` and `SharedObject` to it. Asserted by `@Event` so attaching it to a type that can't
|
|
10
26
|
/// emit fails with a conformance diagnostic instead of an opaque "no member 'emit'" error.
|
|
11
27
|
internal let eventEmitterProtocolName = "EventEmitter"
|
|
12
28
|
|
|
13
|
-
/// Types we never assert because they're statically known to conform
|
|
14
|
-
///
|
|
15
|
-
/// (rather than reusing the decode-path's `fastDecodeAccessor`) because "known-to-conform" is a
|
|
16
|
-
/// concept that belongs with the assertion logic, not with how a value is decoded.
|
|
29
|
+
/// Types we never assert because they're statically known to conform: the JS primitives. Asserting
|
|
30
|
+
/// them would only add noise to the expansion.
|
|
17
31
|
private let knownConformingPrimitives: Set<String> = ["Bool", "Int", "Double", "String"]
|
|
18
32
|
|
|
19
33
|
/// One member's worth of conformance assertion: a name (the member it stands for) and the declared
|
|
@@ -27,27 +41,55 @@ internal struct ConformanceAssertion {
|
|
|
27
41
|
let types: [String]
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
/// A
|
|
31
|
-
///
|
|
32
|
-
///
|
|
33
|
-
///
|
|
34
|
-
///
|
|
35
|
-
///
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
/// members collapse to `static` here too: the peer is private and never called or overridden, so
|
|
39
|
-
/// `static` is always sufficient.
|
|
44
|
+
/// A directional conformance-assertion peer for a `@JS` member: a never-called `private func` whose
|
|
45
|
+
/// body asserts each argument type is `JavaScriptDecodable` and the return/getter type is
|
|
46
|
+
/// `JavaScriptEncodable`, matching how the generated binding converts each (decode on the way in,
|
|
47
|
+
/// encode on the way out). The check is a single member-named nested helper with one `A0…` generic
|
|
48
|
+
/// parameter per non-primitive argument and a single `Return` parameter for the value, called once with
|
|
49
|
+
/// every type's metatype; naming the helper after the member puts the member in the compiler's
|
|
50
|
+
/// diagnostic, and the per-slot constraint reports the offending type against the protocol for *its*
|
|
51
|
+
/// direction.
|
|
40
52
|
///
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/// `decodableTypes` are the argument types (and a settable property's value type); `encodableType` is
|
|
54
|
+
/// the single return type (or a property's value type on read), or `nil` when there's none. Primitives
|
|
55
|
+
/// are dropped from each; when nothing is left in either, returns `nil` so the caller emits no peer.
|
|
56
|
+
/// `isStatic` mirrors a `static`/`class` member so the peer sits in the right metatype context.
|
|
57
|
+
internal func directionalConformanceAssertion(
|
|
58
|
+
name: String,
|
|
59
|
+
decodableTypes: [String],
|
|
60
|
+
encodableType: String?,
|
|
61
|
+
isStatic: Bool
|
|
62
|
+
) -> DeclSyntax? {
|
|
63
|
+
let decodables = distinctAssertableTypes(decodableTypes)
|
|
64
|
+
let encodable = encodableType.flatMap(assertableBoundaryType)
|
|
65
|
+
guard !decodables.isEmpty || encodable != nil else {
|
|
45
66
|
return nil
|
|
46
67
|
}
|
|
68
|
+
|
|
69
|
+
// Build the generic parameter list and the matching call arguments in lockstep: one `A0…` slot
|
|
70
|
+
// constrained `JavaScriptDecodable` per argument, and a single `Return` slot constrained
|
|
71
|
+
// `JavaScriptEncodable` for the return/getter value (there's only ever one, so it isn't indexed).
|
|
72
|
+
var parameters: [String] = []
|
|
73
|
+
var typeParameters: [String] = []
|
|
74
|
+
var arguments: [String] = []
|
|
75
|
+
for (index, type) in decodables.enumerated() {
|
|
76
|
+
parameters.append("A\(index): \(javaScriptDecodableProtocolName)")
|
|
77
|
+
typeParameters.append("_: A\(index).Type")
|
|
78
|
+
arguments.append("\(type).self")
|
|
79
|
+
}
|
|
80
|
+
if let encodable {
|
|
81
|
+
parameters.append("Return: \(javaScriptEncodableProtocolName)")
|
|
82
|
+
typeParameters.append("_: Return.Type")
|
|
83
|
+
arguments.append("\(encodable).self")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let helper = "func \(name)<\(parameters.joined(separator: ", "))>(\(typeParameters.joined(separator: ", "))) {}"
|
|
87
|
+
let call = "\(name)(\(arguments.joined(separator: ", ")))"
|
|
47
88
|
let staticKeyword = isStatic ? "static " : ""
|
|
48
89
|
return """
|
|
49
|
-
private \(raw: staticKeyword)func _assertTypesConformance_\(raw:
|
|
50
|
-
\(raw:
|
|
90
|
+
private \(raw: staticKeyword)func _assertTypesConformance_\(raw: name)() {
|
|
91
|
+
\(raw: helper)
|
|
92
|
+
\(raw: call)
|
|
51
93
|
}
|
|
52
94
|
"""
|
|
53
95
|
}
|
|
@@ -59,8 +101,11 @@ internal func typeConformanceAssertion(for assertion: ConformanceAssertion, isSt
|
|
|
59
101
|
///
|
|
60
102
|
/// Returns `nil` when no assertion has anything left to verify (all primitives / empty), so the
|
|
61
103
|
/// caller emits nothing.
|
|
62
|
-
internal func typeConformanceAssertions(
|
|
63
|
-
|
|
104
|
+
internal func typeConformanceAssertions(
|
|
105
|
+
for assertions: [ConformanceAssertion],
|
|
106
|
+
constraint: String = jsConvertibleProtocolName
|
|
107
|
+
) -> DeclSyntax? {
|
|
108
|
+
let bodies = assertions.compactMap { conformanceAssertionBody($0, constraint: constraint) }
|
|
64
109
|
guard !bodies.isEmpty else {
|
|
65
110
|
return nil
|
|
66
111
|
}
|
|
@@ -85,24 +130,33 @@ internal func assertableBoundaryType(_ type: String) -> String? {
|
|
|
85
130
|
/// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the member
|
|
86
131
|
/// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every type was
|
|
87
132
|
/// a known-conforming primitive or the list was empty.
|
|
88
|
-
private func conformanceAssertionBody(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for type in assertion.types.map(unwrappedOptional)
|
|
94
|
-
where !knownConformingPrimitives.contains(type) && seen.insert(type).inserted {
|
|
95
|
-
distinct.append(type)
|
|
96
|
-
}
|
|
133
|
+
private func conformanceAssertionBody(
|
|
134
|
+
_ assertion: ConformanceAssertion,
|
|
135
|
+
constraint: String = jsConvertibleProtocolName
|
|
136
|
+
) -> String? {
|
|
137
|
+
let distinct = distinctAssertableTypes(assertion.types)
|
|
97
138
|
guard !distinct.isEmpty else {
|
|
98
139
|
return nil
|
|
99
140
|
}
|
|
100
141
|
|
|
101
|
-
var lines = ["func \(assertion.name)<T: \(
|
|
142
|
+
var lines = ["func \(assertion.name)<T: \(constraint)>(_: T.Type) {}"]
|
|
102
143
|
lines.append(contentsOf: distinct.map { "\(assertion.name)(\($0).self)" })
|
|
103
144
|
return lines.joined(separator: "\n")
|
|
104
145
|
}
|
|
105
146
|
|
|
147
|
+
/// Normalizes a list of boundary types for assertion: unwraps each to its core type (trailing
|
|
148
|
+
/// optionals stripped), drops the known-conforming primitives that never need asserting, and dedups
|
|
149
|
+
/// while preserving first-seen order so a type is asserted once even when it appears more than once.
|
|
150
|
+
private func distinctAssertableTypes(_ types: [String]) -> [String] {
|
|
151
|
+
var seen: Set<String> = []
|
|
152
|
+
var distinct: [String] = []
|
|
153
|
+
for type in types.map(unwrappedOptional)
|
|
154
|
+
where !knownConformingPrimitives.contains(type) && seen.insert(type).inserted {
|
|
155
|
+
distinct.append(type)
|
|
156
|
+
}
|
|
157
|
+
return distinct
|
|
158
|
+
}
|
|
159
|
+
|
|
106
160
|
/// Strips every trailing optional marker (`?`/`!`) so the assertion targets the core wrapped type.
|
|
107
161
|
/// `Optional<W>: AnyArgument` holds exactly when `W: AnyArgument` (and `T!` is just `T?`), so each
|
|
108
162
|
/// layer is conformance-equivalent to its wrapped type. Asserting the core gives a cleaner diagnostic
|
|
@@ -95,8 +95,9 @@ private func arguments(of attribute: AttributeSyntax) -> [MacroArgument] {
|
|
|
95
95
|
|
|
96
96
|
/// The first string-literal argument of an attribute, e.g. `@JS("doWork")` -> "doWork". Returns
|
|
97
97
|
/// `nil` when there's no argument or it isn't a plain string literal. (Same shape the
|
|
98
|
-
/// `jsNameArgument` helper reads inside the macros.)
|
|
99
|
-
|
|
98
|
+
/// `jsNameArgument` helper reads inside the macros.) Shared with the `scan-exports` surface visitor,
|
|
99
|
+
/// which reads the same JS-name override off `@JS`/`@ExpoModule`/`@SharedObject`.
|
|
100
|
+
func stringArgument(of attribute: AttributeSyntax) -> String? {
|
|
100
101
|
guard let args = attribute.arguments?.as(LabeledExprListSyntax.self),
|
|
101
102
|
let first = args.first,
|
|
102
103
|
first.label == nil,
|
|
@@ -2,14 +2,19 @@ import Foundation
|
|
|
2
2
|
import SwiftParser
|
|
3
3
|
import SwiftSyntax
|
|
4
4
|
|
|
5
|
-
/// Walks `paths`,
|
|
6
|
-
///
|
|
7
|
-
/// scan command builds on
|
|
8
|
-
|
|
5
|
+
/// Walks `paths`, and for each `.swift` file that might contain one of `macros` (the pre-filter passes
|
|
6
|
+
/// it), reads the source and hands it to `process` along with the file path. Returns the run's stats.
|
|
7
|
+
/// The shared core every scan command builds on: the walk, read, pre-filter, and stats are identical;
|
|
8
|
+
/// only what each command does per parsed file differs (`scan-modules` collects `Detection`s,
|
|
9
|
+
/// `scan-exports` walks a `SurfaceVisitor`), and that lives in `process`.
|
|
10
|
+
func scanFiles(
|
|
11
|
+
paths: [String],
|
|
12
|
+
macros: Set<DetectedMacro>,
|
|
13
|
+
process: (_ source: String, _ file: String) -> Void
|
|
14
|
+
) -> ScanStats {
|
|
9
15
|
let clock = ContinuousClock()
|
|
10
16
|
let start = clock.now
|
|
11
17
|
|
|
12
|
-
var detections: [Detection] = []
|
|
13
18
|
var filesScanned = 0
|
|
14
19
|
var filesParsed = 0
|
|
15
20
|
|
|
@@ -29,13 +34,24 @@ func collectDetections(paths: [String], macros: Set<DetectedMacro>) -> (detectio
|
|
|
29
34
|
continue
|
|
30
35
|
}
|
|
31
36
|
filesParsed += 1
|
|
32
|
-
|
|
37
|
+
process(source, file)
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
let elapsed = (clock.now - start).components
|
|
36
41
|
let durationMs = Double(elapsed.seconds) * 1000 + Double(elapsed.attoseconds) / 1e15
|
|
37
42
|
|
|
38
|
-
return
|
|
43
|
+
return ScanStats(filesScanned: filesScanned, filesParsed: filesParsed, durationMs: durationMs)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Walks `paths`, parses each `.swift` file that might contain one of `macros`, and returns every
|
|
47
|
+
/// detection (in file then source order) with the run's stats — the shape `scan-modules` projects.
|
|
48
|
+
/// A thin layer over `scanFiles` that accumulates the per-file detections.
|
|
49
|
+
func collectDetections(paths: [String], macros: Set<DetectedMacro>) -> (detections: [Detection], stats: ScanStats) {
|
|
50
|
+
var detections: [Detection] = []
|
|
51
|
+
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
52
|
+
detections.append(contentsOf: detect(source: source, file: file, macros: macros))
|
|
53
|
+
}
|
|
54
|
+
return (detections, stats)
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
/// Parses one source string and returns its detections for the given macro set. The unit of work the
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The deep-scan surface: the full JS-exported shape of every `@ExpoModule`, `@SharedObject`, and
|
|
4
|
+
/// `@Record` type, with the per-member detail a TypeScript type generator needs. Read syntactically
|
|
5
|
+
/// (like the macros): each boundary type becomes a structured `TypeNode` tree so the consumer walks a
|
|
6
|
+
/// tagged tree rather than re-parsing Swift type syntax.
|
|
7
|
+
|
|
8
|
+
/// One parameter of a `@JS` function or `@JS init`.
|
|
9
|
+
struct ExportedParameter: Encodable, Equatable {
|
|
10
|
+
/// The argument label (the `first` name): `to` in `func move(to point: Point)`, or `_` if unlabeled.
|
|
11
|
+
let label: String
|
|
12
|
+
|
|
13
|
+
/// The internal parameter name (the `second` name, else the same as `label`): `point` above.
|
|
14
|
+
let name: String
|
|
15
|
+
|
|
16
|
+
let type: TypeNode
|
|
17
|
+
|
|
18
|
+
/// True when the caller may omit it: a default value or an optional type. Distinct from the type
|
|
19
|
+
/// being `.optional` (a defaulted non-optional is also omittable). Encoded as `optional`.
|
|
20
|
+
let isOptional: Bool
|
|
21
|
+
|
|
22
|
+
private enum CodingKeys: String, CodingKey {
|
|
23
|
+
case label, name, type
|
|
24
|
+
case isOptional = "optional"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// One `@JS func` on a module or shared object.
|
|
29
|
+
struct ExportedFunction: Encodable, Equatable {
|
|
30
|
+
/// The Swift declaration name.
|
|
31
|
+
let name: String
|
|
32
|
+
|
|
33
|
+
/// The JS name it binds under: the `@JS("x")` override, else `name`.
|
|
34
|
+
let jsName: String
|
|
35
|
+
|
|
36
|
+
let parameters: [ExportedParameter]
|
|
37
|
+
|
|
38
|
+
/// The return type, or `nil` for `Void`. Named `returns` to pair with `parameters`.
|
|
39
|
+
let returns: TypeNode?
|
|
40
|
+
|
|
41
|
+
/// `async` (an async function is promise-returning in JS).
|
|
42
|
+
let isAsync: Bool
|
|
43
|
+
|
|
44
|
+
/// `throws`.
|
|
45
|
+
let isThrowing: Bool
|
|
46
|
+
|
|
47
|
+
/// `static`/`class` member.
|
|
48
|
+
let isStatic: Bool
|
|
49
|
+
|
|
50
|
+
/// The flags are encoded under their TS-keyword spellings.
|
|
51
|
+
private enum CodingKeys: String, CodingKey {
|
|
52
|
+
case name, jsName, parameters, returns
|
|
53
|
+
case isAsync = "async"
|
|
54
|
+
case isThrowing = "throws"
|
|
55
|
+
case isStatic = "static"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// One `@JS var` on a module or shared object.
|
|
60
|
+
struct ExportedProperty: Encodable, Equatable {
|
|
61
|
+
/// The Swift declaration name.
|
|
62
|
+
let name: String
|
|
63
|
+
|
|
64
|
+
/// The JS name it binds under: the `@JS("x")` override, else `name`.
|
|
65
|
+
let jsName: String
|
|
66
|
+
|
|
67
|
+
/// The value type, or `nil` when undeterminable syntactically (no annotation and no literal
|
|
68
|
+
/// default), the case the macro binds getter-only.
|
|
69
|
+
let type: TypeNode?
|
|
70
|
+
|
|
71
|
+
/// True when JS can assign to it: a stored `var` or a computed `var` with a `set`. Encoded as its
|
|
72
|
+
/// inverse, `readonly`.
|
|
73
|
+
let isSettable: Bool
|
|
74
|
+
|
|
75
|
+
/// `static`/`class` member. Encoded as `static`.
|
|
76
|
+
let isStatic: Bool
|
|
77
|
+
|
|
78
|
+
private enum CodingKeys: String, CodingKey {
|
|
79
|
+
case name, jsName, type
|
|
80
|
+
case isReadonly = "readonly"
|
|
81
|
+
case isStatic = "static"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func encode(to encoder: Encoder) throws {
|
|
85
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
86
|
+
try container.encode(name, forKey: .name)
|
|
87
|
+
try container.encode(jsName, forKey: .jsName)
|
|
88
|
+
try container.encodeIfPresent(type, forKey: .type)
|
|
89
|
+
try container.encode(!isSettable, forKey: .isReadonly)
|
|
90
|
+
try container.encode(isStatic, forKey: .isStatic)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// One `@Record` property: a plain data slot exposing `optional`/`required` (vs. `ExportedProperty`,
|
|
95
|
+
/// a JS accessor exposing `readonly`/`static`). A record crosses the boundary by value, so the whole
|
|
96
|
+
/// type is read-only in JS; that's a record-level fact and isn't stamped per property.
|
|
97
|
+
struct ExportedRecordProperty: Encodable, Equatable {
|
|
98
|
+
let name: String
|
|
99
|
+
|
|
100
|
+
/// `@Record` requires a determinable type on every property, so this is never `nil`.
|
|
101
|
+
let type: TypeNode
|
|
102
|
+
|
|
103
|
+
/// Optional-typed. Encoded as `optional`.
|
|
104
|
+
let isOptional: Bool
|
|
105
|
+
|
|
106
|
+
/// Has a default value. Not encoded (derivable as `!isOptional && !isRequired`, and a Swift default
|
|
107
|
+
/// never reaches JS); kept only to derive `isRequired`.
|
|
108
|
+
let hasDefault: Bool
|
|
109
|
+
|
|
110
|
+
/// Whether JS must supply this property, matching the macro's `RecordProperty.isRequired`. Encoded
|
|
111
|
+
/// as `required`.
|
|
112
|
+
var isRequired: Bool {
|
|
113
|
+
return !hasDefault && !isOptional
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private enum CodingKeys: String, CodingKey {
|
|
117
|
+
case name, type
|
|
118
|
+
case isOptional = "optional"
|
|
119
|
+
case isRequired = "required"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func encode(to encoder: Encoder) throws {
|
|
123
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
124
|
+
try container.encode(name, forKey: .name)
|
|
125
|
+
try container.encode(type, forKey: .type)
|
|
126
|
+
try container.encode(isOptional, forKey: .isOptional)
|
|
127
|
+
try container.encode(isRequired, forKey: .isRequired)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// A `@ExpoModule` type and its `@JS` surface.
|
|
132
|
+
struct ExportedModule: Encodable, Equatable {
|
|
133
|
+
/// The Swift class name.
|
|
134
|
+
let name: String
|
|
135
|
+
|
|
136
|
+
/// The JS module name: `@ExpoModule("Foo")` override, else the class name.
|
|
137
|
+
let jsName: String
|
|
138
|
+
|
|
139
|
+
let functions: [ExportedFunction]
|
|
140
|
+
let properties: [ExportedProperty]
|
|
141
|
+
|
|
142
|
+
/// Absolute source path, matching `scan-modules`.
|
|
143
|
+
let file: String
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// A `@SharedObject` type: a JS class with an optional `@JS init` constructor plus its `@JS` members.
|
|
147
|
+
struct ExportedSharedObject: Encodable, Equatable {
|
|
148
|
+
/// The Swift class name.
|
|
149
|
+
let name: String
|
|
150
|
+
|
|
151
|
+
/// The JS class name: `@SharedObject("Foo")` override, else the class name.
|
|
152
|
+
let jsName: String
|
|
153
|
+
|
|
154
|
+
/// The `@JS init` parameters, or `nil` when there's none. A shared object has at most one.
|
|
155
|
+
let constructorParameters: [ExportedParameter]?
|
|
156
|
+
|
|
157
|
+
let functions: [ExportedFunction]
|
|
158
|
+
let properties: [ExportedProperty]
|
|
159
|
+
|
|
160
|
+
let file: String
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// A `@Record` type and its properties (data only: no functions, accessors, or constructor).
|
|
164
|
+
struct ExportedRecord: Encodable, Equatable {
|
|
165
|
+
/// The Swift type name (struct or class).
|
|
166
|
+
let name: String
|
|
167
|
+
|
|
168
|
+
let properties: [ExportedRecordProperty]
|
|
169
|
+
|
|
170
|
+
let file: String
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// The exported types grouped by kind, nested under `exports` in the result so the surface is one
|
|
174
|
+
/// self-contained object separate from `stats`.
|
|
175
|
+
struct ExportedSurface: Encodable, Equatable {
|
|
176
|
+
let modules: [ExportedModule]
|
|
177
|
+
let sharedObjects: [ExportedSharedObject]
|
|
178
|
+
let records: [ExportedRecord]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
182
|
+
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
183
|
+
struct ScanExportsResult: Encodable, Equatable {
|
|
184
|
+
let exports: ExportedSurface
|
|
185
|
+
let stats: ScanStats
|
|
186
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftParser
|
|
3
|
+
import SwiftSyntax
|
|
4
|
+
|
|
5
|
+
extension Scanner {
|
|
6
|
+
/// Runs `scan-exports` over `paths`, prints the JSON report to stdout, and returns the exit code
|
|
7
|
+
/// (`0` on success, `1` if encoding fails). The deep counterpart to `runModules`.
|
|
8
|
+
public static func runExports(paths: [String]) -> Int32 {
|
|
9
|
+
let result = scanExports(paths: paths)
|
|
10
|
+
|
|
11
|
+
do {
|
|
12
|
+
let encoder = JSONEncoder()
|
|
13
|
+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
14
|
+
let data = try encoder.encode(result)
|
|
15
|
+
FileHandle.standardOutput.write(data)
|
|
16
|
+
FileHandle.standardOutput.write(Data("\n".utf8))
|
|
17
|
+
return 0
|
|
18
|
+
} catch {
|
|
19
|
+
FileHandle.standardError.write(Data("error: failed to encode results: \(error)\n".utf8))
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Scans `paths` for `@ExpoModule`, `@SharedObject`, and `@Record` types and returns their exported
|
|
26
|
+
/// surface plus the run's stats. Separate from the public entry so tests can drive it without
|
|
27
|
+
/// argv/stdout. The shared `scanFiles` walk + pre-filter selects files; a `SurfaceVisitor` extracts
|
|
28
|
+
/// each one.
|
|
29
|
+
func scanExports(paths: [String]) -> ScanExportsResult {
|
|
30
|
+
var modules: [ExportedModule] = []
|
|
31
|
+
var sharedObjects: [ExportedSharedObject] = []
|
|
32
|
+
var records: [ExportedRecord] = []
|
|
33
|
+
|
|
34
|
+
let stats = scanFiles(paths: paths, macros: [.expoModule, .sharedObject, .record]) { source, file in
|
|
35
|
+
let tree = Parser.parse(source: source)
|
|
36
|
+
let visitor = SurfaceVisitor(file: file)
|
|
37
|
+
visitor.walk(tree)
|
|
38
|
+
modules.append(contentsOf: visitor.modules)
|
|
39
|
+
sharedObjects.append(contentsOf: visitor.sharedObjects)
|
|
40
|
+
records.append(contentsOf: visitor.records)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ScanExportsResult(
|
|
44
|
+
exports: ExportedSurface(modules: modules, sharedObjects: sharedObjects, records: records),
|
|
45
|
+
stats: stats
|
|
46
|
+
)
|
|
47
|
+
}
|