expo-modules-macros 0.20.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/linux.yml +132 -0
- package/.github/workflows/publish.yml +160 -11
- package/.github/workflows/swift-format.yml +76 -0
- package/.github/workflows/windows.yml +3 -28
- package/AGENTS.md +19 -9
- package/README.md +27 -8
- 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 +117 -25
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +55 -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 +84 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +165 -43
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +358 -234
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +48 -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/index.d.ts +4 -3
- package/build/index.js +33 -18
- package/build/types.d.ts +7 -1
- package/package.json +7 -3
package/apple/Package.resolved
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"originHash" : "
|
|
2
|
+
"originHash" : "a90a0c6d6b4023098b6a673ded4f314ec06c219ded1559e5d5595f23c7dc323b",
|
|
3
3
|
"pins" : [
|
|
4
4
|
{
|
|
5
5
|
"identity" : "swift-syntax",
|
|
6
6
|
"kind" : "remoteSourceControl",
|
|
7
7
|
"location" : "https://github.com/swiftlang/swift-syntax.git",
|
|
8
8
|
"state" : {
|
|
9
|
-
"revision" : "
|
|
10
|
-
"version" : "
|
|
9
|
+
"revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
|
|
10
|
+
"version" : "604.0.0"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
],
|
package/apple/Package.swift
CHANGED
|
@@ -9,7 +9,7 @@ let package = Package(
|
|
|
9
9
|
name: "ExpoModulesMacros",
|
|
10
10
|
platforms: [.macOS(.v13)],
|
|
11
11
|
dependencies: [
|
|
12
|
-
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "
|
|
12
|
+
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "604.0.0-latest")
|
|
13
13
|
],
|
|
14
14
|
targets: [
|
|
15
15
|
// The plugin executable doubles as the scanner CLI: the compiler launches it without arguments
|
|
@@ -44,7 +44,6 @@ if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
|
|
|
44
44
|
name: "ExpoModulesMacrosTests",
|
|
45
45
|
dependencies: [
|
|
46
46
|
"ExpoModulesMacros",
|
|
47
|
-
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
|
|
48
47
|
.product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"),
|
|
49
48
|
]
|
|
50
49
|
)
|
|
@@ -189,7 +189,9 @@ internal func diagnoseClosureTypes(
|
|
|
189
189
|
message: ClosureDiagnosticMessage(
|
|
190
190
|
"A @JS function can't return a closure: a native closure can't be encoded to a JavaScript function.",
|
|
191
191
|
id: "js-closure-return"
|
|
192
|
-
)
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
193
195
|
}
|
|
194
196
|
return
|
|
195
197
|
}
|
|
@@ -199,14 +201,17 @@ internal func diagnoseClosureTypes(
|
|
|
199
201
|
}
|
|
200
202
|
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
201
203
|
let type = varDecl.bindings.first?.typeAnnotation?.type,
|
|
202
|
-
let nested = firstFunctionType(in: type)
|
|
204
|
+
let nested = firstFunctionType(in: type)
|
|
205
|
+
{
|
|
203
206
|
context.diagnose(
|
|
204
207
|
Diagnostic(
|
|
205
208
|
node: nested,
|
|
206
209
|
message: ClosureDiagnosticMessage(
|
|
207
210
|
"A @JS property can't have a closure type: a native closure can't be encoded to a JavaScript function.",
|
|
208
211
|
id: "js-closure-property"
|
|
209
|
-
)
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -223,7 +228,9 @@ private func diagnoseClosureParameters(
|
|
|
223
228
|
message: ClosureDiagnosticMessage(
|
|
224
229
|
"A closure in a @JS argument must be the argument's own type, optionally wrapped in an optional. Closures inside other types, such as arrays or dictionaries, aren't supported.",
|
|
225
230
|
id: "js-closure-nested-in-type"
|
|
226
|
-
)
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
)
|
|
227
234
|
}
|
|
228
235
|
continue
|
|
229
236
|
}
|
|
@@ -245,7 +252,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
245
252
|
message: ClosureDiagnosticMessage(
|
|
246
253
|
"'@\(name)' isn't supported on a @JS closure argument. The closure calls JavaScript through its own thread hop, so only '@escaping' and '@Sendable' are allowed.",
|
|
247
254
|
id: "js-closure-attribute"
|
|
248
|
-
)
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
)
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
if let throwsClause = functionType.effectSpecifiers?.throwsClause, throwsClause.type != nil {
|
|
@@ -255,7 +264,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
255
264
|
message: ClosureDiagnosticMessage(
|
|
256
265
|
"A @JS closure argument can't use typed throws: the error from JavaScript can be any error. Write 'throws' without a type.",
|
|
257
266
|
id: "js-closure-typed-throws"
|
|
258
|
-
)
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
)
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
for parameter in closure.parameters {
|
|
@@ -266,7 +277,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
266
277
|
message: ClosureDiagnosticMessage(
|
|
267
278
|
"A @JS closure argument can't take an '\(specifier)' parameter: its value is encoded and sent to JavaScript, which can't write it back or share its isolation.",
|
|
268
279
|
id: "js-closure-parameter-specifier"
|
|
269
|
-
)
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
)
|
|
270
283
|
}
|
|
271
284
|
if parameter.isVariadic {
|
|
272
285
|
context.diagnose(
|
|
@@ -275,7 +288,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
275
288
|
message: ClosureDiagnosticMessage(
|
|
276
289
|
"A @JS closure argument can't take a variadic parameter. Use an array instead.",
|
|
277
290
|
id: "js-closure-variadic-parameter"
|
|
278
|
-
)
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
)
|
|
279
294
|
}
|
|
280
295
|
}
|
|
281
296
|
|
|
@@ -290,7 +305,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
290
305
|
message: ClosureDiagnosticMessage(
|
|
291
306
|
"A @JS closure argument can't take or return another closure.",
|
|
292
307
|
id: "js-closure-nested"
|
|
293
|
-
)
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
)
|
|
294
311
|
} else if mentionsFreeFormAny(type) {
|
|
295
312
|
context.diagnose(
|
|
296
313
|
Diagnostic(
|
|
@@ -298,7 +315,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
298
315
|
message: ClosureDiagnosticMessage(
|
|
299
316
|
"A @JS closure argument can't use the free-form '\(type.trimmedDescription)'. Use a concrete type, or 'JavaScriptValue' to pass a JS value through unchanged.",
|
|
300
317
|
id: "js-closure-free-form"
|
|
301
|
-
)
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
)
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
323
|
|
|
@@ -310,7 +329,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
310
329
|
"A @JS closure argument that returns a value must be 'throws': when the JavaScript function throws, the closure has no value to return.",
|
|
311
330
|
id: "js-closure-requires-throws"
|
|
312
331
|
),
|
|
313
|
-
fixIts: [insertThrowsFixIt(for: functionType)]
|
|
332
|
+
fixIts: [insertThrowsFixIt(for: functionType)]
|
|
333
|
+
)
|
|
334
|
+
)
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
|
|
@@ -321,13 +342,16 @@ private func insertThrowsFixIt(for functionType: FunctionTypeSyntax) -> FixIt {
|
|
|
321
342
|
var newType = functionType
|
|
322
343
|
if var effectSpecifiers = functionType.effectSpecifiers, let asyncSpecifier = effectSpecifiers.asyncSpecifier {
|
|
323
344
|
effectSpecifiers.throwsClause = ThrowsClauseSyntax(
|
|
324
|
-
throwsSpecifier: .keyword(.throws, trailingTrivia: asyncSpecifier.trailingTrivia)
|
|
345
|
+
throwsSpecifier: .keyword(.throws, trailingTrivia: asyncSpecifier.trailingTrivia)
|
|
346
|
+
)
|
|
325
347
|
effectSpecifiers.asyncSpecifier = asyncSpecifier.with(\.trailingTrivia, .space)
|
|
326
348
|
newType.effectSpecifiers = effectSpecifiers
|
|
327
349
|
} else {
|
|
328
350
|
newType.effectSpecifiers = TypeEffectSpecifiersSyntax(
|
|
329
351
|
throwsClause: ThrowsClauseSyntax(
|
|
330
|
-
throwsSpecifier: .keyword(.throws, trailingTrivia: functionType.rightParen.trailingTrivia)
|
|
352
|
+
throwsSpecifier: .keyword(.throws, trailingTrivia: functionType.rightParen.trailingTrivia)
|
|
353
|
+
)
|
|
354
|
+
)
|
|
331
355
|
newType.rightParen = functionType.rightParen.with(\.trailingTrivia, .space)
|
|
332
356
|
}
|
|
333
357
|
return FixIt(
|
|
@@ -360,7 +384,8 @@ internal func firstFunctionType(in type: some SyntaxProtocol) -> FunctionTypeSyn
|
|
|
360
384
|
/// same type as `(Int) -> Void`.
|
|
361
385
|
private func unparenthesized(_ type: TypeSyntax) -> TypeSyntax {
|
|
362
386
|
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
363
|
-
let element = tuple.elements.first, element.firstName == nil
|
|
387
|
+
let element = tuple.elements.first, element.firstName == nil
|
|
388
|
+
{
|
|
364
389
|
return unparenthesized(element.type)
|
|
365
390
|
}
|
|
366
391
|
return type
|
|
@@ -13,20 +13,32 @@ import SwiftSyntax
|
|
|
13
13
|
/// unwrap, arity guard, argument decode, all while `this` and the arguments are still valid)
|
|
14
14
|
/// and returns the async body (call and result encode), so nothing JSI-owned crosses the asynchronous
|
|
15
15
|
/// boundary and JS gets a promise.
|
|
16
|
+
///
|
|
17
|
+
/// A synchronous function marked `@JS(.withAsync("name"))` also binds a promise-returning variant
|
|
18
|
+
/// under that name (see `asyncVariant`). It decodes and encodes like an `async` function, and calls
|
|
19
|
+
/// the function on the concurrent pool.
|
|
16
20
|
internal struct JSFunction {
|
|
17
21
|
let swiftName: String
|
|
18
|
-
|
|
22
|
+
private(set) var jsName: String
|
|
19
23
|
let parameters: [FunctionParameterSyntax]
|
|
20
24
|
/// The declared return type as written, or `nil` when the function returns `Void`/nothing.
|
|
21
25
|
let returnType: String?
|
|
22
26
|
/// How the return value encodes when its type mentions `Any`, or `nil` for any other type.
|
|
23
27
|
let returnFreeForm: FreeFormConversion?
|
|
24
28
|
let isThrowing: Bool
|
|
25
|
-
|
|
29
|
+
private(set) var isAsync: Bool
|
|
30
|
+
/// The function's name in the source, where a JavaScript name collision is reported.
|
|
31
|
+
private(set) var nameToken: TokenSyntax
|
|
32
|
+
/// The name from `.withAsync("name")` and its token, or `nil` when the attribute doesn't list it.
|
|
33
|
+
private var asyncVariantName: (name: String, token: TokenSyntax)?
|
|
34
|
+
/// True for the binding `asyncVariant` returns: it calls the synchronous function on the concurrent
|
|
35
|
+
/// pool instead of on the JavaScript thread.
|
|
36
|
+
private(set) var isAsyncVariant = false
|
|
26
37
|
|
|
27
38
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
28
39
|
self.swiftName = funcDecl.name.text
|
|
29
40
|
self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
|
|
41
|
+
self.nameToken = funcDecl.name
|
|
30
42
|
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
31
43
|
|
|
32
44
|
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
@@ -36,6 +48,23 @@ internal struct JSFunction {
|
|
|
36
48
|
let effectSpecifiers = funcDecl.signature.effectSpecifiers
|
|
37
49
|
self.isThrowing = effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
38
50
|
self.isAsync = effectSpecifiers?.asyncSpecifier != nil
|
|
51
|
+
self.asyncVariantName = asyncVariantOption(of: attribute).flatMap(asyncVariantName(of:))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The promise-returning binding that `.withAsync("name")` adds next to a synchronous function, or
|
|
55
|
+
/// `nil` when the attribute doesn't list it. An `async` function gets none: `@JS` reports the option
|
|
56
|
+
/// there.
|
|
57
|
+
var asyncVariant: JSFunction? {
|
|
58
|
+
guard let asyncVariantName, !isAsync else {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
var variant = self
|
|
62
|
+
variant.jsName = asyncVariantName.name
|
|
63
|
+
variant.nameToken = asyncVariantName.token
|
|
64
|
+
variant.isAsync = true
|
|
65
|
+
variant.isAsyncVariant = true
|
|
66
|
+
variant.asyncVariantName = nil
|
|
67
|
+
return variant
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
/// The number of leading parameters that must always be supplied: the total minus the maximal
|
|
@@ -76,14 +105,16 @@ internal struct JSFunction {
|
|
|
76
105
|
guard arguments.count == \(maximum) else {
|
|
77
106
|
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
78
107
|
}
|
|
79
|
-
"""
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
80
110
|
} else {
|
|
81
111
|
lines.append(
|
|
82
112
|
"""
|
|
83
113
|
guard arguments.count >= \(required) && arguments.count <= \(maximum) else {
|
|
84
114
|
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(jsName)", received: arguments.count, required: \(required), maximum: \(maximum)))
|
|
85
115
|
}
|
|
86
|
-
"""
|
|
116
|
+
"""
|
|
117
|
+
)
|
|
87
118
|
}
|
|
88
119
|
|
|
89
120
|
// Decode the required prefix once — these slots are present in every accepted arity, so the
|
|
@@ -139,7 +170,8 @@ internal struct JSFunction {
|
|
|
139
170
|
lines.append(contentsOf: encodeResultLines())
|
|
140
171
|
}
|
|
141
172
|
|
|
142
|
-
return
|
|
173
|
+
return
|
|
174
|
+
lines
|
|
143
175
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
144
176
|
.map { indent + $0 }
|
|
145
177
|
.joined(separator: "\n")
|
|
@@ -149,14 +181,22 @@ internal struct JSFunction {
|
|
|
149
181
|
/// the function's async body (`AsyncFunctionBody`), awaiting the call and encoding the result. It
|
|
150
182
|
/// captures only what the call needs — the decoded `arg<i>` locals, the receiver, and `runtime`
|
|
151
183
|
/// for the encode — never `this` or the arguments buffer, which are only valid for the duration
|
|
152
|
-
/// of the host call.
|
|
184
|
+
/// of the host call. The `asyncVariant` binding awaits its call on the concurrent pool instead.
|
|
153
185
|
private func asyncBodyLines(receiver: Receiver, arity: Int) -> [String] {
|
|
154
186
|
var lines: [String] = ["return {"]
|
|
187
|
+
let call =
|
|
188
|
+
isAsyncVariant
|
|
189
|
+
? concurrentCallExpression(receiver: receiver, arity: arity)
|
|
190
|
+
: callExpression(receiver: receiver, arity: arity)
|
|
191
|
+
// The concurrent call spans several lines, so each of its lines is indented into the body.
|
|
192
|
+
let indented = { (statement: String) in
|
|
193
|
+
statement.split(separator: "\n", omittingEmptySubsequences: false).map { " " + $0 }
|
|
194
|
+
}
|
|
155
195
|
if returnType != nil {
|
|
156
|
-
lines.append("
|
|
196
|
+
lines.append(contentsOf: indented("let result = \(call)"))
|
|
157
197
|
lines.append(contentsOf: encodeResultLines().map { " " + $0 })
|
|
158
198
|
} else {
|
|
159
|
-
lines.append(
|
|
199
|
+
lines.append(contentsOf: indented(call))
|
|
160
200
|
lines.append(" return .undefined()")
|
|
161
201
|
}
|
|
162
202
|
lines.append("}")
|
|
@@ -176,10 +216,36 @@ internal struct JSFunction {
|
|
|
176
216
|
)
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
/// The `<callee>.<name>(...)` call for the given arity
|
|
219
|
+
/// The `<callee>.<name>(...)` call for the given arity, with the arguments from `callArguments`.
|
|
220
|
+
private func callExpression(receiver: Receiver, arity: Int) -> String {
|
|
221
|
+
// `try` only for a throwing function. An `async` call that doesn't throw must not get one, or the
|
|
222
|
+
// expansion warns that no throwing call occurs within the `try` expression.
|
|
223
|
+
let tryKeyword = isThrowing ? "try " : ""
|
|
224
|
+
let awaitKeyword = isAsync ? "await " : ""
|
|
225
|
+
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments(arity: arity)))"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// The call of the `asyncVariant` binding for the given arity: the synchronous function runs in a
|
|
229
|
+
/// `@concurrent` closure, so it leaves the JavaScript thread, and the body continues once it returns.
|
|
230
|
+
private func concurrentCallExpression(receiver: Receiver, arity: Int) -> String {
|
|
231
|
+
// The closure captures the receiver and the decoded arguments, so Swift 6 requires them to be
|
|
232
|
+
// `Sendable`, as it does for the receiver and arguments of a `.concurrent` call. The signature is
|
|
233
|
+
// spelled out because `@concurrent` needs an `async` closure, and a body without `await` would
|
|
234
|
+
// infer a synchronous one.
|
|
235
|
+
let tryKeyword = isThrowing ? "try " : ""
|
|
236
|
+
let effects = isThrowing ? "async throws" : "async"
|
|
237
|
+
let returnClause = returnType.map { " -> \(expressionType($0))" } ?? ""
|
|
238
|
+
return """
|
|
239
|
+
\(tryKeyword)await { @concurrent () \(effects)\(returnClause) in
|
|
240
|
+
\(tryKeyword)\(receiver.callee).\(swiftName)(\(callArguments(arity: arity)))
|
|
241
|
+
}()
|
|
242
|
+
"""
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// The argument list of a call for the given arity. Slots `0..<arity` are passed their decoded
|
|
180
246
|
/// `arg<i>`; a trailing optional-without-default slot that this arity omits is passed `nil`; a
|
|
181
247
|
/// trailing defaulted slot that this arity omits is dropped entirely so Swift applies its default.
|
|
182
|
-
private func
|
|
248
|
+
private func callArguments(arity: Int) -> String {
|
|
183
249
|
var callArguments: [String] = []
|
|
184
250
|
for (index, parameter) in parameters.enumerated() {
|
|
185
251
|
let label = parameter.firstName.text
|
|
@@ -198,11 +264,7 @@ internal struct JSFunction {
|
|
|
198
264
|
}
|
|
199
265
|
callArguments.append(label == "_" ? value : "\(label): \(value)")
|
|
200
266
|
}
|
|
201
|
-
|
|
202
|
-
// expansion warns that no throwing call occurs within the `try` expression.
|
|
203
|
-
let tryKeyword = isThrowing ? "try " : ""
|
|
204
|
-
let awaitKeyword = isAsync ? "await " : ""
|
|
205
|
-
return "\(tryKeyword)\(awaitKeyword)\(receiver.callee).\(swiftName)(\(callArguments.joined(separator: ", ")))"
|
|
267
|
+
return callArguments.joined(separator: ", ")
|
|
206
268
|
}
|
|
207
269
|
|
|
208
270
|
/// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
|
|
@@ -313,6 +375,8 @@ internal struct JSProperty {
|
|
|
313
375
|
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
314
376
|
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
315
377
|
let isSettable: Bool
|
|
378
|
+
/// The property's name in the source, where a JavaScript name collision is reported.
|
|
379
|
+
let nameToken: TokenSyntax
|
|
316
380
|
|
|
317
381
|
/// The statements that install this property's accessor on the JS object, indented for the
|
|
318
382
|
/// decorator body. Builds a descriptor object (`enumerable` + `get`, and `set` when settable) via
|
|
@@ -342,7 +406,8 @@ internal struct JSProperty {
|
|
|
342
406
|
getEncode = "return \(callee).\(swiftName).toJavaScriptValue(in: runtime)"
|
|
343
407
|
}
|
|
344
408
|
lines.append(
|
|
345
|
-
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)")
|
|
409
|
+
accessorClosure(descriptorName, "get", receiver: receiver, body: "\(unwrap)\(getEncode)")
|
|
410
|
+
)
|
|
346
411
|
|
|
347
412
|
// Setter: decode argument 0 by the static type through `decode` and write `<callee>.<name>`. A
|
|
348
413
|
// typed setter needs a known value type; when the type couldn't be inferred the property is bound
|
|
@@ -353,12 +418,19 @@ internal struct JSProperty {
|
|
|
353
418
|
let setDecode = "\(callee).\(swiftName) = try \(decode)"
|
|
354
419
|
lines.append(
|
|
355
420
|
accessorClosure(
|
|
356
|
-
descriptorName,
|
|
421
|
+
descriptorName,
|
|
422
|
+
"set",
|
|
423
|
+
receiver: receiver,
|
|
424
|
+
returnsVoid: true,
|
|
425
|
+
body: "\(unwrap)\(setDecode)"
|
|
426
|
+
)
|
|
427
|
+
)
|
|
357
428
|
}
|
|
358
429
|
|
|
359
430
|
lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
|
|
360
431
|
|
|
361
|
-
return
|
|
432
|
+
return
|
|
433
|
+
lines
|
|
362
434
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
363
435
|
.map { " " + $0 }
|
|
364
436
|
.joined(separator: "\n")
|
|
@@ -369,12 +441,17 @@ internal struct JSProperty {
|
|
|
369
441
|
/// A setter passes `returnsVoid`, so its closure declares `-> Void` and binds through the `Void`
|
|
370
442
|
/// overload, like a function that returns nothing.
|
|
371
443
|
private func accessorClosure(
|
|
372
|
-
_ descriptorName: String,
|
|
444
|
+
_ descriptorName: String,
|
|
445
|
+
_ key: String,
|
|
446
|
+
receiver: Receiver,
|
|
447
|
+
returnsVoid: Bool = false,
|
|
448
|
+
body: String
|
|
373
449
|
) -> String {
|
|
374
450
|
let captures = receiver.captureClause
|
|
375
451
|
// Indent each line of a (possibly multi-line) body to sit one level inside the closure; a bare
|
|
376
452
|
// `\(body)` interpolation would only indent the first line.
|
|
377
|
-
let indentedBody =
|
|
453
|
+
let indentedBody =
|
|
454
|
+
body
|
|
378
455
|
.split(separator: "\n", omittingEmptySubsequences: false)
|
|
379
456
|
.map { " \($0)" }
|
|
380
457
|
.joined(separator: "\n")
|
|
@@ -399,7 +476,10 @@ internal struct JSProperty {
|
|
|
399
476
|
/// how each binding reaches its Swift value (module `self`, shared-object instance `_self`, or the
|
|
400
477
|
/// metatype for a static member).
|
|
401
478
|
private func decorateBody(
|
|
402
|
-
functions: [JSFunction],
|
|
479
|
+
functions: [JSFunction],
|
|
480
|
+
properties: [JSProperty],
|
|
481
|
+
object: String,
|
|
482
|
+
receiver: Receiver
|
|
403
483
|
) -> String {
|
|
404
484
|
let functionBody = functions.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
405
485
|
let propertyBody = properties.map { $0.decorateStatements(object: object, receiver: receiver) }
|
|
@@ -416,7 +496,11 @@ private func decorateBody(
|
|
|
416
496
|
/// receiver; the shared-object counterpart is `buildDecorateSharedObjectPhase`.
|
|
417
497
|
internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties: [JSProperty]) -> DeclSyntax {
|
|
418
498
|
let body = decorateBody(
|
|
419
|
-
functions: functions,
|
|
499
|
+
functions: functions,
|
|
500
|
+
properties: properties,
|
|
501
|
+
object: Phase.object.rawValue,
|
|
502
|
+
receiver: .module
|
|
503
|
+
)
|
|
420
504
|
return """
|
|
421
505
|
@JavaScriptActor
|
|
422
506
|
public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
@@ -437,13 +521,21 @@ internal func buildDecorateJavaScriptObject(functions: [JSFunction], properties:
|
|
|
437
521
|
/// which builds an instance and is emitted separately (see `JSConstructor.buildConstructor`). Each phase
|
|
438
522
|
/// is emitted only when the type has a member for it.
|
|
439
523
|
internal func buildDecorateSharedObjectPhase(
|
|
440
|
-
phase: Phase,
|
|
524
|
+
phase: Phase,
|
|
525
|
+
functions: [JSFunction],
|
|
526
|
+
properties: [JSProperty],
|
|
527
|
+
typeName: String
|
|
441
528
|
) -> DeclSyntax {
|
|
442
|
-
let receiver: Receiver =
|
|
529
|
+
let receiver: Receiver =
|
|
530
|
+
phase == .constructor
|
|
443
531
|
? .staticMember(typeName: typeName)
|
|
444
532
|
: .sharedObject(typeName: typeName)
|
|
445
533
|
let body = decorateBody(
|
|
446
|
-
functions: functions,
|
|
534
|
+
functions: functions,
|
|
535
|
+
properties: properties,
|
|
536
|
+
object: phase.rawValue,
|
|
537
|
+
receiver: receiver
|
|
538
|
+
)
|
|
447
539
|
return """
|
|
448
540
|
@JavaScriptActor
|
|
449
541
|
public override class func _decorateSharedObject(\(raw: phase.rawValue): borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
|
|
@@ -54,7 +54,8 @@ public struct EventMacro: AccessorMacro {
|
|
|
54
54
|
// through the getter, so the body never has to spell the payload type. A sync event calls
|
|
55
55
|
// `emitSync` (inline dispatch, JS thread only); the default calls the scheduling `emit`.
|
|
56
56
|
let emitMethod = event.isSync ? "emitSync" : "emit"
|
|
57
|
-
let closure =
|
|
57
|
+
let closure =
|
|
58
|
+
event.hasPayload
|
|
58
59
|
? "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
|
|
59
60
|
: "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
|
|
60
61
|
return [
|
|
@@ -85,9 +86,10 @@ extension EventMacro: PeerMacro {
|
|
|
85
86
|
// and asserting the enclosing type by its spelled name so the conformance error names the
|
|
86
87
|
// user's type rather than 'Self'.
|
|
87
88
|
let name = event.swiftName
|
|
88
|
-
let payload = event.payloadType.flatMap(
|
|
89
|
+
let payload = event.payloadType.flatMap { assertableBoundaryType($0, constraint: jsConvertibleProtocolName) }
|
|
89
90
|
let owner = enclosingTypeName(in: context) ?? "Self"
|
|
90
|
-
let helper =
|
|
91
|
+
let helper =
|
|
92
|
+
payload != nil
|
|
91
93
|
? "func \(name)<P: \(jsConvertibleProtocolName), E: \(eventEmitterProtocolName)>(_: P.Type, _: E.Type) {}"
|
|
92
94
|
: "func \(name)<E: \(eventEmitterProtocolName)>(_: E.Type) {}"
|
|
93
95
|
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
@@ -132,7 +134,8 @@ private func validatedEvent(
|
|
|
132
134
|
}
|
|
133
135
|
if varDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
134
136
|
throw MacroExpansionErrorMessage(
|
|
135
|
-
"@Event and @JS cannot be combined on the same property; an event is exposed to JS on its own, so remove one of the attributes"
|
|
137
|
+
"@Event and @JS cannot be combined on the same property; an event is exposed to JS on its own, so remove one of the attributes"
|
|
138
|
+
)
|
|
136
139
|
}
|
|
137
140
|
// The compiler also rejects accessor macros on a `let`, but with a generic message; this one says
|
|
138
141
|
// what to do instead and carries the fix-it doing it. The synthesized property is getter-only, so
|
|
@@ -142,32 +145,40 @@ private func validatedEvent(
|
|
|
142
145
|
}
|
|
143
146
|
if varDecl.modifiers.contains(where: isTypeLevelModifier) {
|
|
144
147
|
throw MacroExpansionErrorMessage(
|
|
145
|
-
"@Event must be an instance property; events are emitted from a module or shared object instance."
|
|
148
|
+
"@Event must be an instance property; events are emitted from a module or shared object instance."
|
|
149
|
+
)
|
|
146
150
|
}
|
|
147
151
|
guard varDecl.bindings.count == 1, let binding = varDecl.bindings.first,
|
|
148
|
-
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
|
|
152
|
+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
|
|
153
|
+
else {
|
|
149
154
|
throw MacroExpansionErrorMessage(
|
|
150
|
-
"@Event must be applied to a single named property; declare each event separately"
|
|
155
|
+
"@Event must be applied to a single named property; declare each event separately"
|
|
156
|
+
)
|
|
151
157
|
}
|
|
152
158
|
if binding.initializer != nil {
|
|
153
159
|
throw MacroExpansionErrorMessage(
|
|
154
|
-
"@Event property cannot have an initial value; the macro synthesizes the closure"
|
|
160
|
+
"@Event property cannot have an initial value; the macro synthesizes the closure"
|
|
161
|
+
)
|
|
155
162
|
}
|
|
156
163
|
if binding.accessorBlock != nil {
|
|
157
164
|
throw MacroExpansionErrorMessage(
|
|
158
|
-
"@Event property cannot declare its own accessors; the macro synthesizes the getter"
|
|
165
|
+
"@Event property cannot declare its own accessors; the macro synthesizes the getter"
|
|
166
|
+
)
|
|
159
167
|
}
|
|
160
168
|
guard let functionType = underlyingFunctionType(of: binding.typeAnnotation?.type) else {
|
|
161
169
|
throw MacroExpansionErrorMessage(
|
|
162
|
-
"@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'"
|
|
170
|
+
"@Event property must declare a function type, such as '(Payload) -> Void' or '() -> Void'"
|
|
171
|
+
)
|
|
163
172
|
}
|
|
164
173
|
guard isVoidReturn(functionType.returnClause.type) else {
|
|
165
174
|
throw MacroExpansionErrorMessage(
|
|
166
|
-
"@Event function type must return 'Void'; an event dispatches to JS and has no return value"
|
|
175
|
+
"@Event function type must return 'Void'; an event dispatches to JS and has no return value"
|
|
176
|
+
)
|
|
167
177
|
}
|
|
168
178
|
guard functionType.parameters.count <= 1 else {
|
|
169
179
|
throw MacroExpansionErrorMessage(
|
|
170
|
-
"@Event function type takes at most one payload parameter; combine multiple values into a single record"
|
|
180
|
+
"@Event function type takes at most one payload parameter; combine multiple values into a single record"
|
|
181
|
+
)
|
|
171
182
|
}
|
|
172
183
|
// The payload is encoded to JavaScript, and a native closure can't be encoded.
|
|
173
184
|
if let payloadClosure = functionType.parameters.first.flatMap({ firstFunctionType(in: $0.type) }) {
|
|
@@ -177,7 +188,8 @@ private func validatedEvent(
|
|
|
177
188
|
message: EventDiagnosticMessage(
|
|
178
189
|
"@Event payload can't hold a closure: the payload is encoded to JavaScript, and a native closure can't be encoded.",
|
|
179
190
|
id: "event-closure-payload"
|
|
180
|
-
)
|
|
191
|
+
)
|
|
192
|
+
)
|
|
181
193
|
])
|
|
182
194
|
}
|
|
183
195
|
|
|
@@ -321,7 +333,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
321
333
|
return underlyingFunctionType(of: attributed.baseType)
|
|
322
334
|
}
|
|
323
335
|
if let tuple = type.as(TupleTypeSyntax.self),
|
|
324
|
-
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
336
|
+
tuple.elements.count == 1, let element = tuple.elements.first, element.firstName == nil
|
|
337
|
+
{
|
|
325
338
|
return underlyingFunctionType(of: element.type)
|
|
326
339
|
}
|
|
327
340
|
return type.as(FunctionTypeSyntax.self)
|
|
@@ -334,7 +347,8 @@ private func underlyingFunctionType(of type: TypeSyntax?) -> FunctionTypeSyntax?
|
|
|
334
347
|
private func isVoidReturn(_ type: TypeSyntax) -> Bool {
|
|
335
348
|
// Peel single-element, unlabeled parentheses: `(Void)` and `(())` are the same type as their content.
|
|
336
349
|
if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
|
|
337
|
-
let element = tuple.elements.first, element.firstName == nil
|
|
350
|
+
let element = tuple.elements.first, element.firstName == nil
|
|
351
|
+
{
|
|
338
352
|
return isVoidReturn(element.type)
|
|
339
353
|
}
|
|
340
354
|
let text = type.trimmedDescription
|