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
|
@@ -2,34 +2,32 @@ 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
*/
|
|
5
|
+
/// Macro applied to a module class. It plays three roles, each implemented in its own
|
|
6
|
+
/// extension below:
|
|
7
|
+
///
|
|
8
|
+
/// - `MemberMacro`: binds `@JS`-marked members directly into the module's JS object via the
|
|
9
|
+
/// synthesized `_decorateModule`, and emits the resolved module name as a non-optional
|
|
10
|
+
/// `_jsName` static (no `Name(…)` DSL element). It also synthesizes a
|
|
11
|
+
/// framework-internal `_synthesizedDefinition()` returning `[AnyDefinition]` (now carrying
|
|
12
|
+
/// only nested `classes` entries) that `expo-modules-core` merges into the module's
|
|
13
|
+
/// definition. When the class doesn't already inherit `Module`/`BaseModule`, it also
|
|
14
|
+
/// synthesizes the `appContext` storage and `init(appContext:)` those base classes provide.
|
|
15
|
+
/// - `MemberAttributeMacro`: stamps `@JavaScriptActor` on `@JS` sync members and
|
|
16
|
+
/// `@ModuleDefinitionBuilder` on a `definition()` method.
|
|
17
|
+
/// - `ExtensionMacro`: adds the `AnyModule` conformance when the class doesn't inherit it.
|
|
18
|
+
///
|
|
19
|
+
/// Inheriting from `Module` is therefore optional — a class can carry any superclass (or
|
|
20
|
+
/// none) and still be a module, because everything inheritance used to provide is synthesized.
|
|
21
|
+
///
|
|
22
|
+
/// @ExpoModule
|
|
23
|
+
/// public final class MyModule {
|
|
24
|
+
/// public func definition() -> ModuleDefinition {
|
|
25
|
+
/// Name("MyModule")
|
|
26
|
+
/// }
|
|
27
|
+
///
|
|
28
|
+
/// @JS
|
|
29
|
+
/// func greet(name: String) -> String { "Hi, \(name)" }
|
|
30
|
+
/// }
|
|
33
31
|
public struct ExpoModuleMacro: MemberMacro {
|
|
34
32
|
public static func expansion(
|
|
35
33
|
of node: AttributeSyntax,
|
|
@@ -62,17 +60,25 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
62
60
|
let decl = member.decl
|
|
63
61
|
|
|
64
62
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
65
|
-
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
66
|
-
|
|
63
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
64
|
+
{
|
|
65
|
+
let function = JSFunction(funcDecl: funcDecl, attribute: attribute)
|
|
66
|
+
functions.append(function)
|
|
67
|
+
if let variant = function.asyncVariant {
|
|
68
|
+
functions.append(variant)
|
|
69
|
+
}
|
|
67
70
|
continue
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
71
|
-
let attribute = varDecl.attributes.firstAttribute(named: "JS")
|
|
74
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS")
|
|
75
|
+
{
|
|
72
76
|
properties.append(contentsOf: collectProperties(varDecl: varDecl, attribute: attribute))
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
diagnoseJSNameCollisions(functions: functions, properties: properties, in: context)
|
|
81
|
+
|
|
76
82
|
let body: String
|
|
77
83
|
if entries.isEmpty {
|
|
78
84
|
body = " return []"
|
|
@@ -152,12 +158,14 @@ extension ExpoModuleMacro: MemberAttributeMacro {
|
|
|
152
158
|
|
|
153
159
|
// `@JS` sync members run on the JS thread; stamp `@JavaScriptActor` so isolation is
|
|
154
160
|
// checked at compile time. Skipped when the member already chose an isolation
|
|
155
|
-
// (`
|
|
161
|
+
// (`nonisolated` or another global actor), or when `.concurrent` or `.withAsync("name")` runs
|
|
162
|
+
// it on the concurrent pool. See `shouldStampJavaScriptActor`.
|
|
156
163
|
// `@Event(sync: true)` members get the stamp too: a sync event dispatches inline, so the
|
|
157
164
|
// isolation forces its call site onto the JS thread. Async events (the default) are
|
|
158
165
|
// deliberately left unstamped — their `emit` schedules onto the JS thread itself.
|
|
159
166
|
if memberHasJSAttribute(member) || isSyncEventMember(member),
|
|
160
|
-
shouldStampJavaScriptActor(on: member, enclosedBy: declaration)
|
|
167
|
+
shouldStampJavaScriptActor(on: member, enclosedBy: declaration)
|
|
168
|
+
{
|
|
161
169
|
attributes.append("@JavaScriptActor")
|
|
162
170
|
}
|
|
163
171
|
|
|
@@ -172,7 +180,8 @@ extension ExpoModuleMacro: MemberAttributeMacro {
|
|
|
172
180
|
// they already wrote `@ModuleDefinitionBuilder` themselves, which would otherwise be
|
|
173
181
|
// a duplicate attribute.
|
|
174
182
|
if isModuleDefinitionFunction(member),
|
|
175
|
-
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") })
|
|
183
|
+
!memberAttributes(of: member).contains(where: { hasAttributeName($0, "ModuleDefinitionBuilder") })
|
|
184
|
+
{
|
|
176
185
|
attributes.append("@ModuleDefinitionBuilder")
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -184,7 +193,8 @@ private func isModuleDefinitionFunction(_ member: some DeclSyntaxProtocol) -> Bo
|
|
|
184
193
|
guard let funcDecl = member.as(FunctionDeclSyntax.self),
|
|
185
194
|
funcDecl.name.text == "definition",
|
|
186
195
|
funcDecl.signature.parameterClause.parameters.isEmpty,
|
|
187
|
-
let returnType = funcDecl.signature.returnClause?.type
|
|
196
|
+
let returnType = funcDecl.signature.returnClause?.type
|
|
197
|
+
else {
|
|
188
198
|
return false
|
|
189
199
|
}
|
|
190
200
|
return baseIdentifier(of: returnType) == "ModuleDefinition"
|
|
@@ -213,7 +223,8 @@ extension ExpoModuleMacro: ExtensionMacro {
|
|
|
213
223
|
// These base types already supply `AnyModule`; emitting a second conformance here
|
|
214
224
|
// would be redundant and error.
|
|
215
225
|
if let classDecl = declaration.as(ClassDeclSyntax.self),
|
|
216
|
-
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
|
|
226
|
+
inheritsFromAny(classDecl, names: ["Module", "BaseModule", "AnyModule"])
|
|
227
|
+
{
|
|
217
228
|
return []
|
|
218
229
|
}
|
|
219
230
|
let conformance: DeclSyntax = "extension \(type.trimmed): AnyModule {}"
|
|
@@ -230,7 +241,8 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
|
|
|
230
241
|
}
|
|
231
242
|
for binding in varDecl.bindings {
|
|
232
243
|
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
233
|
-
identifier.identifier.text == name
|
|
244
|
+
identifier.identifier.text == name
|
|
245
|
+
{
|
|
234
246
|
return true
|
|
235
247
|
}
|
|
236
248
|
}
|
|
@@ -238,14 +250,12 @@ private func hasStoredProperty(_ classDecl: ClassDeclSyntax, named name: String)
|
|
|
238
250
|
return false
|
|
239
251
|
}
|
|
240
252
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
syntactic match on the spelled name.
|
|
248
|
-
*/
|
|
253
|
+
/// True if the class declares an initializer that satisfies the `init(appContext:)` protocol
|
|
254
|
+
/// requirement: a single parameter labeled `appContext` whose type is written as `AppContext`
|
|
255
|
+
/// (or `….AppContext`). Both must match — the label is part of the requirement, so
|
|
256
|
+
/// `init(c: AppContext)` does *not* satisfy it (we still synthesize ours), and the type guards
|
|
257
|
+
/// against a same-labeled init of an unrelated type. A macro can't resolve types, so this is a
|
|
258
|
+
/// syntactic match on the spelled name.
|
|
249
259
|
private func hasAppContextInitializer(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
250
260
|
for member in classDecl.memberBlock.members {
|
|
251
261
|
guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else {
|
|
@@ -34,7 +34,8 @@ public struct OptimizedFunctionAttachedMacro: PeerMacro {
|
|
|
34
34
|
) throws -> [DeclSyntax] {
|
|
35
35
|
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
36
36
|
throw MacroExpansionErrorMessage(
|
|
37
|
-
"@OptimizedFunction can only be attached to function declarations"
|
|
37
|
+
"@OptimizedFunction can only be attached to function declarations"
|
|
38
|
+
)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
let functionName = funcDecl.name.text
|
|
@@ -178,72 +178,13 @@ internal func isAnyType(_ type: some SyntaxProtocol) -> Bool {
|
|
|
178
178
|
if let member = type.as(MemberTypeSyntax.self),
|
|
179
179
|
member.name.text == "Any",
|
|
180
180
|
member.genericArgumentClause == nil,
|
|
181
|
-
let base = member.baseType.as(IdentifierTypeSyntax.self)
|
|
181
|
+
let base = member.baseType.as(IdentifierTypeSyntax.self)
|
|
182
|
+
{
|
|
182
183
|
return base.name.text == "Swift"
|
|
183
184
|
}
|
|
184
185
|
return false
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
/// The element type of `[T]` or `Array<T>`, or `nil` for any other type.
|
|
188
|
-
private func arrayElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
189
|
-
if let array = type.as(ArrayTypeSyntax.self) {
|
|
190
|
-
return array.element
|
|
191
|
-
}
|
|
192
|
-
guard let arguments = genericArguments(of: type, named: "Array"), arguments.count == 1 else {
|
|
193
|
-
return nil
|
|
194
|
-
}
|
|
195
|
-
return arguments[0]
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/// The value type of `[String: V]` or `Dictionary<String, V>`, or `nil` for any other type, including a
|
|
199
|
-
/// dictionary with a different key type.
|
|
200
|
-
private func stringKeyedDictionaryValueType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
201
|
-
if let dictionary = type.as(DictionaryTypeSyntax.self) {
|
|
202
|
-
return isStringType(dictionary.key) ? dictionary.value : nil
|
|
203
|
-
}
|
|
204
|
-
guard let arguments = genericArguments(of: type, named: "Dictionary"), arguments.count == 2 else {
|
|
205
|
-
return nil
|
|
206
|
-
}
|
|
207
|
-
return isStringType(arguments[0]) ? arguments[1] : nil
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/// The type arguments of a generic type spelled `<name><…>` (or `Swift.<name><…>`), or `nil` when the
|
|
211
|
-
/// type has another name or no type arguments.
|
|
212
|
-
private func genericArguments(of type: TypeSyntax, named name: String) -> [TypeSyntax]? {
|
|
213
|
-
let clause: GenericArgumentClauseSyntax?
|
|
214
|
-
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == name {
|
|
215
|
-
clause = identifier.genericArgumentClause
|
|
216
|
-
} else if let member = type.as(MemberTypeSyntax.self),
|
|
217
|
-
member.name.text == name,
|
|
218
|
-
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift" {
|
|
219
|
-
clause = member.genericArgumentClause
|
|
220
|
-
} else {
|
|
221
|
-
return nil
|
|
222
|
-
}
|
|
223
|
-
guard let clause else {
|
|
224
|
-
return nil
|
|
225
|
-
}
|
|
226
|
-
var types: [TypeSyntax] = []
|
|
227
|
-
for argument in clause.arguments {
|
|
228
|
-
guard case .type(let type) = argument.argument else {
|
|
229
|
-
return nil
|
|
230
|
-
}
|
|
231
|
-
types.append(type)
|
|
232
|
-
}
|
|
233
|
-
return types
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/// True when the type is `String` or `Swift.String`.
|
|
237
|
-
private func isStringType(_ type: TypeSyntax) -> Bool {
|
|
238
|
-
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
239
|
-
return identifier.name.text == "String" && identifier.genericArgumentClause == nil
|
|
240
|
-
}
|
|
241
|
-
if let member = type.as(MemberTypeSyntax.self), member.name.text == "String" {
|
|
242
|
-
return member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
243
|
-
}
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
|
|
247
188
|
/// Walks a type looking for `Any` (or `Swift.Any`) anywhere inside it.
|
|
248
189
|
private final class AnyTypeFinder: SyntaxVisitor {
|
|
249
190
|
var found = false
|
|
@@ -23,7 +23,8 @@ internal struct JSConstructor {
|
|
|
23
23
|
guard arguments.count == \(parameters.count) else {
|
|
24
24
|
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(typeName)", received: arguments.count, required: \(parameters.count), maximum: \(parameters.count)))
|
|
25
25
|
}
|
|
26
|
-
"""
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
27
28
|
|
|
28
29
|
var callArguments: [String] = []
|
|
29
30
|
for (index, parameter) in parameters.enumerated() {
|
|
@@ -32,7 +33,8 @@ internal struct JSConstructor {
|
|
|
32
33
|
type: parameter.type,
|
|
33
34
|
into: "arg\(index)",
|
|
34
35
|
from: "arguments.unownedValue(at: \(index))"
|
|
35
|
-
)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
36
38
|
|
|
37
39
|
let label = parameter.firstName.text
|
|
38
40
|
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
@@ -40,7 +42,8 @@ internal struct JSConstructor {
|
|
|
40
42
|
|
|
41
43
|
lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
|
|
42
44
|
|
|
43
|
-
return
|
|
45
|
+
return
|
|
46
|
+
lines
|
|
44
47
|
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
45
48
|
.map { indent + $0 }
|
|
46
49
|
.joined(separator: "\n")
|
|
@@ -35,6 +35,7 @@ public struct JSMacro: PeerMacro {
|
|
|
35
35
|
diagnoseFreeFormTypes(in: declaration, in: context)
|
|
36
36
|
diagnoseClosureTypes(in: declaration, in: context)
|
|
37
37
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
38
|
+
diagnoseAsyncVariantOption(of: node, on: declaration, in: context)
|
|
38
39
|
|
|
39
40
|
guard let member = boundaryMember(of: declaration),
|
|
40
41
|
let assertion = directionalConformanceAssertion(
|
|
@@ -43,7 +44,8 @@ public struct JSMacro: PeerMacro {
|
|
|
43
44
|
encodableType: member.encodableType,
|
|
44
45
|
callbackArgumentTypes: member.callbackArgumentTypes,
|
|
45
46
|
isStatic: member.isStatic
|
|
46
|
-
)
|
|
47
|
+
)
|
|
48
|
+
else {
|
|
47
49
|
return []
|
|
48
50
|
}
|
|
49
51
|
return [assertion]
|
|
@@ -74,7 +76,9 @@ private func diagnoseConcurrentOption(
|
|
|
74
76
|
id: "js-concurrent-requires-async",
|
|
75
77
|
severity: .error
|
|
76
78
|
),
|
|
77
|
-
fixIts: [insertAsyncFixIt(for: funcDecl)]
|
|
79
|
+
fixIts: [insertAsyncFixIt(for: funcDecl)]
|
|
80
|
+
)
|
|
81
|
+
)
|
|
78
82
|
return
|
|
79
83
|
}
|
|
80
84
|
context.diagnose(
|
|
@@ -84,7 +88,108 @@ private func diagnoseConcurrentOption(
|
|
|
84
88
|
"'.concurrent' applies only to an 'async' @JS function, not to a property or initializer.",
|
|
85
89
|
id: "js-concurrent-requires-function",
|
|
86
90
|
severity: .error
|
|
87
|
-
)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Emits the diagnostics for `@JS(.withAsync("name"))` on a member that can't take it. The option
|
|
97
|
+
/// adds a promise-returning variant that calls the function on the concurrent pool, so it needs a
|
|
98
|
+
/// synchronous function without a global actor, and the variant's JavaScript name as a string literal.
|
|
99
|
+
private func diagnoseAsyncVariantOption(
|
|
100
|
+
of node: AttributeSyntax,
|
|
101
|
+
on declaration: some DeclSyntaxProtocol,
|
|
102
|
+
in context: some MacroExpansionContext
|
|
103
|
+
) {
|
|
104
|
+
guard let option = asyncVariantOption(of: node) else {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
108
|
+
context.diagnose(
|
|
109
|
+
Diagnostic(
|
|
110
|
+
node: node,
|
|
111
|
+
message: JSDiagnosticMessage(
|
|
112
|
+
"'.withAsync' applies only to a synchronous @JS function, not to a property or initializer.",
|
|
113
|
+
id: "js-async-variant-requires-function",
|
|
114
|
+
severity: .error
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil {
|
|
121
|
+
context.diagnose(
|
|
122
|
+
Diagnostic(
|
|
123
|
+
node: node,
|
|
124
|
+
message: JSDiagnosticMessage(
|
|
125
|
+
"'.withAsync' adds a promise-returning variant to a synchronous function, and this function is already 'async'. Remove '.withAsync', and use '.concurrent' to run the function off the JavaScript thread.",
|
|
126
|
+
id: "js-async-variant-on-async-function",
|
|
127
|
+
severity: .error
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
for duplicate in asyncVariantOptions(of: node).dropFirst() {
|
|
134
|
+
context.diagnose(
|
|
135
|
+
Diagnostic(
|
|
136
|
+
node: duplicate,
|
|
137
|
+
message: JSDiagnosticMessage(
|
|
138
|
+
"'.withAsync' can be listed only once: a function gets one promise-returning variant.",
|
|
139
|
+
id: "js-async-variant-duplicate",
|
|
140
|
+
severity: .error
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
if asyncVariantName(of: option) == nil {
|
|
146
|
+
context.diagnose(
|
|
147
|
+
Diagnostic(
|
|
148
|
+
node: option,
|
|
149
|
+
message: JSDiagnosticMessage(
|
|
150
|
+
"'.withAsync' takes the JavaScript name of the variant as a string literal, for example '.withAsync(\"loadAsync\")'.",
|
|
151
|
+
id: "js-async-variant-name",
|
|
152
|
+
severity: .error
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
if let isolation = globalActorIsolation(of: funcDecl, in: context) {
|
|
158
|
+
context.diagnose(
|
|
159
|
+
Diagnostic(
|
|
160
|
+
node: node,
|
|
161
|
+
message: JSDiagnosticMessage(
|
|
162
|
+
isolation.enclosingTypeName.map {
|
|
163
|
+
"'.withAsync' calls the function on the concurrent thread pool, so the function can't inherit '\(isolation.attribute)' from '\($0)'. Mark the function 'nonisolated'."
|
|
164
|
+
}
|
|
165
|
+
?? "'.withAsync' calls the function on the concurrent thread pool, so the function can't be isolated to '\(isolation.attribute)'.",
|
|
166
|
+
id: "js-async-variant-isolated",
|
|
167
|
+
severity: .error
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The global actor a function is isolated to, written on the function itself or inherited from its
|
|
175
|
+
/// enclosing type, or `nil` when it has none or is `nonisolated`. `enclosingTypeName` is set when the
|
|
176
|
+
/// isolation is inherited.
|
|
177
|
+
private func globalActorIsolation(
|
|
178
|
+
of funcDecl: FunctionDeclSyntax,
|
|
179
|
+
in context: some MacroExpansionContext
|
|
180
|
+
) -> (attribute: String, enclosingTypeName: String?)? {
|
|
181
|
+
if let attribute = funcDecl.attributes.first(where: hasGlobalActorShape) {
|
|
182
|
+
return (attribute.trimmedDescription, nil)
|
|
183
|
+
}
|
|
184
|
+
if funcDecl.modifiers.contains(where: { $0.name.text == "nonisolated" }) {
|
|
185
|
+
return nil
|
|
186
|
+
}
|
|
187
|
+
guard let enclosing = context.lexicalContext.first,
|
|
188
|
+
let attribute = enclosing.asProtocol(WithAttributesSyntax.self)?.attributes.first(where: hasGlobalActorShape)
|
|
189
|
+
else {
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
return (attribute.trimmedDescription, enclosing.asProtocol(NamedDeclSyntax.self)?.name.text)
|
|
88
193
|
}
|
|
89
194
|
|
|
90
195
|
/// The fix-it offered alongside the synchronous-function diagnostic: insert `async` into the
|
|
@@ -144,7 +249,9 @@ private func diagnoseParameterSpecifiers(
|
|
|
144
249
|
"A @JS parameter can't be '\(specifier)': its argument is a value decoded from JavaScript, which can't be written back or carry an isolation.",
|
|
145
250
|
id: "js-parameter-specifier",
|
|
146
251
|
severity: .error
|
|
147
|
-
)
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
148
255
|
}
|
|
149
256
|
}
|
|
150
257
|
|
|
@@ -173,14 +280,16 @@ private func diagnoseFreeFormTypes(
|
|
|
173
280
|
// A constructor decodes its arguments exactly like a function and has no return value.
|
|
174
281
|
types = initDecl.signature.parameterClause.parameters.map { SpecifiedType($0.type).type }
|
|
175
282
|
} else if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
176
|
-
let type = varDecl.bindings.first?.typeAnnotation?.type
|
|
283
|
+
let type = varDecl.bindings.first?.typeAnnotation?.type
|
|
284
|
+
{
|
|
177
285
|
types = [type]
|
|
178
286
|
}
|
|
179
287
|
|
|
180
288
|
for type in types
|
|
181
289
|
where mentionsFreeFormAny(type) && !isConvertibleFreeFormType(type) && !containsFunctionType(type) {
|
|
182
290
|
context.diagnose(
|
|
183
|
-
Diagnostic(node: type, message: unconvertibleFreeFormError(for: type.trimmedDescription))
|
|
291
|
+
Diagnostic(node: type, message: unconvertibleFreeFormError(for: type.trimmedDescription))
|
|
292
|
+
)
|
|
184
293
|
}
|
|
185
294
|
}
|
|
186
295
|
|
|
@@ -234,61 +343,49 @@ private struct BoundaryMember {
|
|
|
234
343
|
|
|
235
344
|
/// Reads the boundary member off a `@JS` declaration, splitting its types by conversion direction. A
|
|
236
345
|
/// function contributes its parameter types (decodable) and its return type when non-Void (encodable);
|
|
237
|
-
///
|
|
238
|
-
/// (the setter). Composed types (`[Int]`, `String?`,
|
|
239
|
-
/// conformances transitively constraining the elements. A type
|
|
240
|
-
/// converts through the free-form methods, or is diagnosed by
|
|
241
|
-
/// for declaration kinds `@JS` doesn't read types from, a
|
|
242
|
-
/// syntactic macro can't recover it), or one whose type
|
|
346
|
+
/// an initializer contributes its parameter types; a property contributes its value type as encodable
|
|
347
|
+
/// (the getter) and also as decodable when settable (the setter). Composed types (`[Int]`, `String?`,
|
|
348
|
+
/// …) are kept verbatim, their conditional conformances transitively constraining the elements. A type
|
|
349
|
+
/// that mentions `Any` is left out: it converts through the free-form methods, or is diagnosed by
|
|
350
|
+
/// `diagnoseFreeFormTypes`. Returns `nil` for declaration kinds `@JS` doesn't read types from, a
|
|
351
|
+
/// property whose type isn't spelled out (a syntactic macro can't recover it), or one whose type
|
|
352
|
+
/// mentions `Any`.
|
|
243
353
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
244
354
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
245
|
-
|
|
246
|
-
// A closure in any other position is diagnosed by `diagnoseClosureTypes` and left out here, since
|
|
247
|
-
// its metatype isn't a valid expression in the assertion.
|
|
248
|
-
var decodableTypes: [String] = []
|
|
249
|
-
var callbackArgumentTypes: [String] = []
|
|
250
|
-
for parameter in funcDecl.signature.parameterClause.parameters {
|
|
251
|
-
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
252
|
-
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
253
|
-
let specified = SpecifiedType(parameter.type)
|
|
254
|
-
guard specified.unsupportedSpecifier == nil else {
|
|
255
|
-
continue
|
|
256
|
-
}
|
|
257
|
-
guard let closure = JSClosureType(specified.type) else {
|
|
258
|
-
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
259
|
-
decodableTypes.append(specified.type.trimmedDescription)
|
|
260
|
-
}
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
// A closure type that mentions `Any` is reported by `diagnoseClosureTypes` instead.
|
|
264
|
-
for parameter in closure.parameters
|
|
265
|
-
where !containsFunctionType(parameter.type) && !mentionsFreeFormAny(parameter.type) {
|
|
266
|
-
callbackArgumentTypes.append(parameter.type.trimmedDescription)
|
|
267
|
-
}
|
|
268
|
-
let closureReturnType = closure.functionType.returnClause.type
|
|
269
|
-
if let returnType = closure.returnType,
|
|
270
|
-
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType) {
|
|
271
|
-
decodableTypes.append(returnType)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
355
|
+
let argumentTypes = argumentBoundaryTypes(of: funcDecl.signature.parameterClause.parameters)
|
|
274
356
|
let returnType = funcDecl.signature.returnClause?.type
|
|
275
357
|
let encodableType = returnType.flatMap {
|
|
276
358
|
isVoidType($0) || containsFunctionType($0) || mentionsFreeFormAny($0) ? nil : $0.trimmedDescription
|
|
277
359
|
}
|
|
278
360
|
return BoundaryMember(
|
|
279
361
|
name: funcDecl.name.text,
|
|
280
|
-
decodableTypes:
|
|
362
|
+
decodableTypes: argumentTypes.decodable,
|
|
281
363
|
encodableType: encodableType,
|
|
282
|
-
callbackArgumentTypes:
|
|
364
|
+
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
283
365
|
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
284
366
|
)
|
|
285
367
|
}
|
|
286
368
|
|
|
369
|
+
if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
370
|
+
// A constructor decodes its arguments like a function does, and the instance it returns isn't
|
|
371
|
+
// encoded. The name is escaped because it also names the assertion's nested helper, and `init` is
|
|
372
|
+
// a keyword.
|
|
373
|
+
let argumentTypes = argumentBoundaryTypes(of: initDecl.signature.parameterClause.parameters)
|
|
374
|
+
return BoundaryMember(
|
|
375
|
+
name: "`init`",
|
|
376
|
+
decodableTypes: argumentTypes.decodable,
|
|
377
|
+
encodableType: nil,
|
|
378
|
+
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
379
|
+
isStatic: false
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
287
383
|
if let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
288
384
|
let binding = varDecl.bindings.first,
|
|
289
385
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
290
386
|
let type = binding.typeAnnotation?.type,
|
|
291
|
-
!containsFunctionType(type)
|
|
387
|
+
!containsFunctionType(type)
|
|
388
|
+
{
|
|
292
389
|
// A `let`, or a `var` with no setter, is read-only (encodable only). A settable `var` is also
|
|
293
390
|
// decoded on write, so its value type is asserted in both directions. A type that mentions `Any`
|
|
294
391
|
// converts through the free-form methods instead, so it isn't asserted at all.
|
|
@@ -309,6 +406,45 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
309
406
|
return nil
|
|
310
407
|
}
|
|
311
408
|
|
|
409
|
+
/// The boundary types of a function's or constructor's arguments: an argument is decoded from JS, and
|
|
410
|
+
/// a closure argument instead contributes its parameter types (encoded when native code calls it) and
|
|
411
|
+
/// its return type (decoded).
|
|
412
|
+
private func argumentBoundaryTypes(
|
|
413
|
+
of parameters: FunctionParameterListSyntax
|
|
414
|
+
) -> (decodable: [String], callbackArguments: [String]) {
|
|
415
|
+
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
416
|
+
// A closure in any other position is diagnosed by `diagnoseClosureTypes` and left out here, since
|
|
417
|
+
// its metatype isn't a valid expression in the assertion.
|
|
418
|
+
var decodableTypes: [String] = []
|
|
419
|
+
var callbackArgumentTypes: [String] = []
|
|
420
|
+
for parameter in parameters {
|
|
421
|
+
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
422
|
+
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
423
|
+
let specified = SpecifiedType(parameter.type)
|
|
424
|
+
guard specified.unsupportedSpecifier == nil else {
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
guard let closure = JSClosureType(specified.type) else {
|
|
428
|
+
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
429
|
+
decodableTypes.append(specified.type.trimmedDescription)
|
|
430
|
+
}
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
// A closure type that mentions `Any` is reported by `diagnoseClosureTypes` instead.
|
|
434
|
+
for parameter in closure.parameters
|
|
435
|
+
where !containsFunctionType(parameter.type) && !mentionsFreeFormAny(parameter.type) {
|
|
436
|
+
callbackArgumentTypes.append(parameter.type.trimmedDescription)
|
|
437
|
+
}
|
|
438
|
+
let closureReturnType = closure.functionType.returnClause.type
|
|
439
|
+
if let returnType = closure.returnType,
|
|
440
|
+
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType)
|
|
441
|
+
{
|
|
442
|
+
decodableTypes.append(returnType)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return (decodableTypes, callbackArgumentTypes)
|
|
446
|
+
}
|
|
447
|
+
|
|
312
448
|
/// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
|
|
313
449
|
/// no conformance assertion. (A missing return clause never reaches here: `returnClause` is `nil`.)
|
|
314
450
|
private func isVoidType(_ type: TypeSyntax) -> Bool {
|