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.
- package/.github/workflows/swift-format.yml +76 -0
- package/AGENTS.md +9 -4
- package/README.md +15 -2
- 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 +113 -26
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +53 -45
- 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 +180 -44
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +163 -42
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +285 -221
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +45 -32
- 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 +3 -2
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +15 -5
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +127 -45
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +2 -1
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
- package/build/types.d.ts +7 -1
- package/package.json +7 -5
|
@@ -2,26 +2,24 @@ import SwiftSyntax
|
|
|
2
2
|
import SwiftSyntaxBuilder
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
81
|
+
staticFunctions.append(contentsOf: bound)
|
|
79
82
|
} else {
|
|
80
|
-
instanceFunctions.append(
|
|
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 =
|
|
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,
|
|
121
|
-
|
|
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,
|
|
127
|
-
|
|
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
|
|
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.
|
|
72
|
-
/// returns `nil` so the caller emits no peer. `isStatic` mirrors a `static`/`class` member so the
|
|
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(
|
|
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
|
|
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
|
|
147
|
-
/// exactly-free-form type (converted through the `JavaScriptValue` free-form methods) and
|
|
148
|
-
/// known
|
|
149
|
-
/// and asserted. Shared with `@Event`, where an *optional* free-form type
|
|
150
|
-
/// and asserted for a clean diagnostic. `@JS` leaves out every type that
|
|
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
|
-
|
|
157
|
-
|
|
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
|
|
162
|
-
/// symbol, nothing to collide, nothing left in the type's namespace — and naming it after the
|
|
163
|
-
/// puts the member's name in the compiler's conformance diagnostic. Returns `nil` when every
|
|
164
|
-
///
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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)
|
|
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
|
|
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]
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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,
|
|
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
|
|
|
@@ -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
|
|
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,
|
|
62
|
+
modules: modules,
|
|
63
|
+
sharedObjects: sharedObjects,
|
|
64
|
+
records: records,
|
|
65
|
+
enums: enums,
|
|
66
|
+
unions: unions
|
|
62
67
|
).resolvingRefs()
|
|
63
68
|
|
|
64
69
|
return ScanExportsResult(
|