expo-modules-macros 0.0.1 → 0.15.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/resources/expo-modules-macros.svg +23 -0
- package/.github/workflows/publish.yml +122 -0
- package/.github/workflows/swift.yml +68 -0
- package/AGENTS.md +59 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.resolved +15 -0
- package/apple/Package.swift +61 -0
- package/apple/PodTests/StubTest.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +444 -0
- package/apple/Sources/ExpoModulesMacros/EventMacro.swift +330 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +263 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +117 -0
- package/apple/Sources/ExpoModulesMacros/ExpoViewMacro.swift +259 -0
- package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +214 -0
- package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +294 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +433 -0
- package/apple/Sources/ExpoModulesMacros/OptimizedFunctionHelpers.swift +55 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +34 -0
- package/apple/Sources/ExpoModulesMacros/Receiver.swift +79 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +590 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +162 -0
- package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +190 -0
- package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +389 -0
- package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +477 -0
- package/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift +19 -0
- package/apple/Sources/ExpoModulesScanner/CLI.swift +114 -0
- package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +79 -0
- package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +181 -0
- package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +115 -0
- package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +193 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +319 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +199 -0
- package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +66 -0
- package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +616 -0
- package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +284 -0
- package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +161 -0
- package/apple/build.js +138 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +153 -0
- package/build/types.d.ts +264 -0
- package/build/types.js +15 -0
- package/package.json +22 -5
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxBuilder
|
|
4
|
+
import SwiftSyntaxMacros
|
|
5
|
+
|
|
6
|
+
// MARK: - ExpoModulesOptimizedMacro
|
|
7
|
+
|
|
8
|
+
/// Implementation of the `@OptimizedFunction` attached macro.
|
|
9
|
+
/// Generates a peer function that returns `OptimizedFunctionDescriptor` for use with
|
|
10
|
+
/// the `Function("name", descriptor)` or `AsyncFunction("name", descriptor)` overload.
|
|
11
|
+
///
|
|
12
|
+
/// Usage:
|
|
13
|
+
///
|
|
14
|
+
/// @OptimizedFunction
|
|
15
|
+
/// private func addNumbers(a: Double, b: Double) -> Double {
|
|
16
|
+
/// return a + b
|
|
17
|
+
/// }
|
|
18
|
+
///
|
|
19
|
+
/// generates a peer function:
|
|
20
|
+
///
|
|
21
|
+
/// private func addNumbers() -> OptimizedFunctionDescriptor {
|
|
22
|
+
/// return OptimizedSyncFunctionDefinition.createDescriptor(
|
|
23
|
+
/// typeEncoding: "d@?dd",
|
|
24
|
+
/// argsCount: 2,
|
|
25
|
+
/// block: (addNumbers as @convention(block) (Double, Double) -> Double) as AnyObject
|
|
26
|
+
/// )
|
|
27
|
+
/// }
|
|
28
|
+
///
|
|
29
|
+
/// Then use in definition(): `Function("addNumbers", addNumbers())`
|
|
30
|
+
public struct OptimizedFunctionAttachedMacro: PeerMacro {
|
|
31
|
+
public static func expansion(
|
|
32
|
+
of node: AttributeSyntax,
|
|
33
|
+
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
34
|
+
in context: some MacroExpansionContext
|
|
35
|
+
) throws -> [DeclSyntax] {
|
|
36
|
+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
37
|
+
throw MacroExpansionErrorMessage(
|
|
38
|
+
"@OptimizedFunction can only be attached to function declarations")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let functionName = funcDecl.name.text
|
|
42
|
+
|
|
43
|
+
var paramTypes: [String] = []
|
|
44
|
+
for param in funcDecl.signature.parameterClause.parameters {
|
|
45
|
+
paramTypes.append(param.type.trimmedDescription)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let returnType = funcDecl.signature.returnClause?.type.trimmedDescription ?? "Void"
|
|
49
|
+
let functionThrows = funcDecl.signature.effectSpecifiers?.throwsClause?.throwsSpecifier != nil
|
|
50
|
+
let typeEncoding = try generateTypeEncoding(returnType: returnType, paramTypes: paramTypes)
|
|
51
|
+
let implFuncName = funcDecl.name.text
|
|
52
|
+
let blockParamTypes = paramTypes.joined(separator: ", ")
|
|
53
|
+
let argNames = (0..<paramTypes.count).map { "arg\($0)" }.joined(separator: ", ")
|
|
54
|
+
|
|
55
|
+
let peerFunc: DeclSyntax
|
|
56
|
+
|
|
57
|
+
if functionThrows {
|
|
58
|
+
let isVoid = returnType == "Void" || returnType == "()"
|
|
59
|
+
let implType =
|
|
60
|
+
paramTypes.isEmpty
|
|
61
|
+
? "() throws -> \(returnType)" : "(\(blockParamTypes)) throws -> \(returnType)"
|
|
62
|
+
let wrapperType =
|
|
63
|
+
paramTypes.isEmpty
|
|
64
|
+
? "@convention(block) () -> \(returnType)"
|
|
65
|
+
: "@convention(block) (\(blockParamTypes)) -> \(returnType)"
|
|
66
|
+
let closureArgs = paramTypes.isEmpty ? "" : " \(argNames) in"
|
|
67
|
+
let implCall = paramTypes.isEmpty ? "impl()" : "impl(\(argNames))"
|
|
68
|
+
let tryExpr = isVoid ? "try \(implCall)" : "return try \(implCall)"
|
|
69
|
+
let unreachable = isVoid ? "" : "\n fatalError(\"Unreachable\")"
|
|
70
|
+
|
|
71
|
+
peerFunc = """
|
|
72
|
+
private func \(raw: functionName)() -> OptimizedFunctionDescriptor {
|
|
73
|
+
let impl: \(raw: implType) = \(raw: implFuncName)
|
|
74
|
+
let wrapper: \(raw: wrapperType) = {\(raw: closureArgs)
|
|
75
|
+
do {
|
|
76
|
+
\(raw: tryExpr)
|
|
77
|
+
} catch {
|
|
78
|
+
let nsError: NSError
|
|
79
|
+
if let expoError = error as? Exception {
|
|
80
|
+
nsError = NSError(domain: "dev.expo.modules", code: 0, userInfo: [
|
|
81
|
+
"name": expoError.name,
|
|
82
|
+
"code": expoError.code,
|
|
83
|
+
"message": expoError.debugDescription,
|
|
84
|
+
])
|
|
85
|
+
} else {
|
|
86
|
+
nsError = error as NSError
|
|
87
|
+
}
|
|
88
|
+
let exception = NSException(
|
|
89
|
+
name: NSExceptionName(nsError.userInfo["name"] as? String ?? "SwiftError"),
|
|
90
|
+
reason: nsError.userInfo["message"] as? String ?? nsError.localizedDescription,
|
|
91
|
+
userInfo: nsError.userInfo
|
|
92
|
+
)
|
|
93
|
+
exception.raise()\(raw: unreachable)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return OptimizedSyncFunctionDefinition.createDescriptor(
|
|
97
|
+
typeEncoding: "\(raw: typeEncoding)",
|
|
98
|
+
argsCount: \(raw: String(paramTypes.count)),
|
|
99
|
+
block: wrapper as AnyObject
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
"""
|
|
103
|
+
} else {
|
|
104
|
+
peerFunc = """
|
|
105
|
+
private func \(raw: functionName)() -> OptimizedFunctionDescriptor {
|
|
106
|
+
return OptimizedSyncFunctionDefinition.createDescriptor(
|
|
107
|
+
typeEncoding: "\(raw: typeEncoding)",
|
|
108
|
+
argsCount: \(raw: String(paramTypes.count)),
|
|
109
|
+
block: (\(raw: implFuncName) as @convention(block) (\(raw: blockParamTypes)) -> \(raw: returnType)) as AnyObject
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
"""
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [peerFunc]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxBuilder
|
|
4
|
+
import SwiftSyntaxMacros
|
|
5
|
+
|
|
6
|
+
/// Member macro applied to an `ExpoView` subclass, binding it to its props type:
|
|
7
|
+
///
|
|
8
|
+
/// @ExpoView<CardProps>
|
|
9
|
+
/// final class CardView: ExpoView {
|
|
10
|
+
/// override func didUpdateProps(_ diff: CardProps.Diff) {
|
|
11
|
+
/// if diff.changed(.color) {
|
|
12
|
+
/// backgroundColor = props.color
|
|
13
|
+
/// }
|
|
14
|
+
/// }
|
|
15
|
+
/// }
|
|
16
|
+
///
|
|
17
|
+
/// The props type is the attribute's **generic argument**, so it sits in type position: no
|
|
18
|
+
/// `.self` metatype to write, and the macro reads it straight off the attribute's
|
|
19
|
+
/// `genericArgumentClause`. Core's declaration constrains the parameter
|
|
20
|
+
/// (`macro ExpoView<Props: AnyViewProps>()`), so passing a type that isn't a props type is a
|
|
21
|
+
/// compile error at the author's own line ("requires that 'X' conform to 'AnyViewProps'")
|
|
22
|
+
/// rather than a macro diagnostic. This macro therefore validates nothing about the props type.
|
|
23
|
+
///
|
|
24
|
+
/// The whole expansion is one line:
|
|
25
|
+
///
|
|
26
|
+
/// public typealias Props = CardProps
|
|
27
|
+
///
|
|
28
|
+
/// That is deliberately all of it. There is **no view definition**: no `ViewDefinition`, no
|
|
29
|
+
/// `View(…) { }` builder, no `Props(_:)` or `Events(_:)` element. `_synthesizedViewDefinition()`
|
|
30
|
+
/// existed only to hand core a DSL value the old registration path could consume, and modules and
|
|
31
|
+
/// shared objects already moved to direct hooks. Core reads the props type from this typealias and
|
|
32
|
+
/// the event names from the props type's own `_eventNames` (synthesized by `@ViewProps`), both
|
|
33
|
+
/// static, neither needing a builder to be evaluated.
|
|
34
|
+
///
|
|
35
|
+
/// Views also get no `_decorateView` hook in its place: `_decorateModule` and `_decorateSharedObject`
|
|
36
|
+
/// exist to bind members into a JS object, and a view has no such object. Props reach a view through
|
|
37
|
+
/// Fabric's mounting layer, not a JS call, so a view's runtime entry point is the props update hook.
|
|
38
|
+
///
|
|
39
|
+
/// `didUpdateProps` is a core-called override, not synthesized here.
|
|
40
|
+
public struct ExpoViewMacro: MemberMacro {
|
|
41
|
+
public static func expansion(
|
|
42
|
+
of node: AttributeSyntax,
|
|
43
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
44
|
+
conformingTo protocols: [TypeSyntax],
|
|
45
|
+
in context: some MacroExpansionContext
|
|
46
|
+
) throws -> [DeclSyntax] {
|
|
47
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
48
|
+
throw MacroExpansionErrorMessage("@ExpoView can only be applied to a class")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
guard inheritsFromAny(classDecl, names: expoViewBaseClassNames) else {
|
|
52
|
+
throw DiagnosticsError(diagnostics: [missingBaseClassDiagnostic(for: classDecl)])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A generic view can't be registered: `@ExpoModule(views: [CardView.self])` on a generic type
|
|
56
|
+
// fails with "generic parameter 'T' could not be inferred", and the props type ignores the
|
|
57
|
+
// parameter anyway. Reject it here, at the author's line, rather than at the registration site.
|
|
58
|
+
// `@ViewProps` rejects generic props types for the same reason.
|
|
59
|
+
if classDecl.genericParameterClause != nil {
|
|
60
|
+
throw MacroExpansionErrorMessage(
|
|
61
|
+
"@ExpoView does not support generic classes — a view registered with @ExpoModule(views:) must be concrete"
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The expansion is a `Props` typealias, so a member of that name already on the class collides.
|
|
66
|
+
// Left alone, the compiler reports "invalid redeclaration of 'Props'" positioned inside the
|
|
67
|
+
// macro expansion, which is code the author never wrote. Name it here instead.
|
|
68
|
+
if let existing = existingPropsMemberKind(in: classDecl) {
|
|
69
|
+
throw MacroExpansionErrorMessage(
|
|
70
|
+
"@ExpoView synthesizes a `Props` typealias, but this class already declares a \(existing) named 'Props'. Rename it"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The props type is the attribute's generic argument. Core's declaration is generic, so a bare
|
|
75
|
+
// `@ExpoView` never reaches expansion: it fails type-checking first with "generic parameter
|
|
76
|
+
// 'Props' could not be inferred". This diagnostic covers the case where the macro is declared
|
|
77
|
+
// differently than core declares it, so the author still gets a message naming the attribute.
|
|
78
|
+
guard let propsType = genericArgument(of: node) else {
|
|
79
|
+
throw MacroExpansionErrorMessage(
|
|
80
|
+
"@ExpoView requires its props type as a generic argument, as in `@ExpoView<MyViewProps>`"
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
"""
|
|
86
|
+
public typealias Props = \(raw: propsType)
|
|
87
|
+
"""
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// MARK: - Diagnostics
|
|
93
|
+
|
|
94
|
+
/// The error for a class that doesn't name `ExpoView` in its inheritance clause, carrying a fix-it
|
|
95
|
+
/// that adds it.
|
|
96
|
+
///
|
|
97
|
+
/// The fix-it only applies where the edit is unambiguous. With no inheritance clause at all it
|
|
98
|
+
/// inserts `: ExpoView`; with an existing clause it prepends `ExpoView` as the first entry, since
|
|
99
|
+
/// Swift requires the superclass to come before any protocol. A class that already names some other
|
|
100
|
+
/// superclass gets the diagnostic without a fix-it: replacing that superclass is a decision the macro
|
|
101
|
+
/// can't make (see the indirect-inheritance note on `expoViewBaseClassNames`).
|
|
102
|
+
private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagnostic {
|
|
103
|
+
let message = ExpoViewDiagnosticMessage(
|
|
104
|
+
"@ExpoView class must inherit from ExpoView. Add `: ExpoView` to the class declaration.",
|
|
105
|
+
id: "expoview-missing-base-class"
|
|
106
|
+
)
|
|
107
|
+
var fixIts: [FixIt] = []
|
|
108
|
+
|
|
109
|
+
if classDecl.inheritanceClause == nil {
|
|
110
|
+
// `class CardView {` → `class CardView: ExpoView {`. The name carries the space before `{` as
|
|
111
|
+
// trailing trivia, so move it onto the clause to keep the spacing.
|
|
112
|
+
let name = classDecl.name
|
|
113
|
+
let clause = InheritanceClauseSyntax(
|
|
114
|
+
colon: .colonToken(trailingTrivia: .space),
|
|
115
|
+
inheritedTypes: InheritedTypeListSyntax([
|
|
116
|
+
InheritedTypeSyntax(type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoView"))))
|
|
117
|
+
])
|
|
118
|
+
)
|
|
119
|
+
fixIts.append(
|
|
120
|
+
FixIt(
|
|
121
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoView'", id: "expoview-add-base-class"),
|
|
122
|
+
changes: [
|
|
123
|
+
.replace(
|
|
124
|
+
oldNode: Syntax(classDecl),
|
|
125
|
+
newNode: Syntax(
|
|
126
|
+
classDecl
|
|
127
|
+
.with(\.name, name.with(\.trailingTrivia, []))
|
|
128
|
+
.with(\.inheritanceClause, clause.with(\.trailingTrivia, name.trailingTrivia))
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
} else if let clause = classDecl.inheritanceClause, inheritsOnlyProtocolsByConvention(clause) {
|
|
135
|
+
// `class CardView: Identifiable {` → `class CardView: ExpoView, Identifiable {`.
|
|
136
|
+
var inherited = clause.inheritedTypes
|
|
137
|
+
if var first = inherited.first {
|
|
138
|
+
first.trailingComma = .commaToken(trailingTrivia: .space)
|
|
139
|
+
first.type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("ExpoView")))
|
|
140
|
+
inherited.insert(first, at: inherited.startIndex)
|
|
141
|
+
}
|
|
142
|
+
fixIts.append(
|
|
143
|
+
FixIt(
|
|
144
|
+
message: ExpoViewFixItMessage("Inherit from 'ExpoView'", id: "expoview-add-base-class"),
|
|
145
|
+
changes: [
|
|
146
|
+
.replace(
|
|
147
|
+
oldNode: Syntax(clause),
|
|
148
|
+
newNode: Syntax(clause.with(\.inheritedTypes, inherited))
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Diagnostic(node: classDecl.name, message: message, fixIts: fixIts)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Whether every entry in the clause looks like a protocol rather than a superclass, by the
|
|
159
|
+
/// capitalization-free heuristic that a Swift superclass must come first: if the first entry is one
|
|
160
|
+
/// the macro would be replacing, prepending `ExpoView` would produce two superclasses. A macro can't
|
|
161
|
+
/// resolve names, so this only reports true when the clause is empty of anything that could be a
|
|
162
|
+
/// base class the author chose deliberately. Conservative by design: a wrong guess here would emit a
|
|
163
|
+
/// fix-it that doesn't compile.
|
|
164
|
+
private func inheritsOnlyProtocolsByConvention(_ clause: InheritanceClauseSyntax) -> Bool {
|
|
165
|
+
// Any entry at all could be a superclass, so only offer the prepend when the author wrote
|
|
166
|
+
// something that is definitely not one: a known protocol-shaped name from the standard library.
|
|
167
|
+
let knownProtocols: Set<String> = [
|
|
168
|
+
"Identifiable", "Equatable", "Hashable", "Codable", "Encodable", "Decodable",
|
|
169
|
+
"Sendable", "CustomStringConvertible", "ObservableObject",
|
|
170
|
+
]
|
|
171
|
+
return clause.inheritedTypes.allSatisfy { entry in
|
|
172
|
+
guard let name = baseIdentifier(of: entry.type) else {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
return knownProtocols.contains(name)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private struct ExpoViewDiagnosticMessage: DiagnosticMessage {
|
|
180
|
+
let message: String
|
|
181
|
+
let diagnosticID: MessageID
|
|
182
|
+
let severity: DiagnosticSeverity = .error
|
|
183
|
+
|
|
184
|
+
init(_ message: String, id: String) {
|
|
185
|
+
self.message = message
|
|
186
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private struct ExpoViewFixItMessage: FixItMessage {
|
|
191
|
+
let message: String
|
|
192
|
+
let fixItID: MessageID
|
|
193
|
+
|
|
194
|
+
init(_ message: String, id: String) {
|
|
195
|
+
self.message = message
|
|
196
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Describes an existing `Props` member on the class, for the collision diagnostic, or `nil` when the
|
|
201
|
+
/// name is free. Covers the shapes that would clash with the synthesized typealias: another typealias,
|
|
202
|
+
/// and a nested type of any kind.
|
|
203
|
+
private func existingPropsMemberKind(in classDecl: ClassDeclSyntax) -> String? {
|
|
204
|
+
for member in classDecl.memberBlock.members {
|
|
205
|
+
let decl = member.decl
|
|
206
|
+
if let typealiasDecl = decl.as(TypeAliasDeclSyntax.self), typealiasDecl.name.text == "Props" {
|
|
207
|
+
return "typealias"
|
|
208
|
+
}
|
|
209
|
+
if let structDecl = decl.as(StructDeclSyntax.self), structDecl.name.text == "Props" {
|
|
210
|
+
return "struct"
|
|
211
|
+
}
|
|
212
|
+
if let nestedClass = decl.as(ClassDeclSyntax.self), nestedClass.name.text == "Props" {
|
|
213
|
+
return "class"
|
|
214
|
+
}
|
|
215
|
+
if let enumDecl = decl.as(EnumDeclSyntax.self), enumDecl.name.text == "Props" {
|
|
216
|
+
return "enum"
|
|
217
|
+
}
|
|
218
|
+
if let actorDecl = decl.as(ActorDeclSyntax.self), actorDecl.name.text == "Props" {
|
|
219
|
+
return "actor"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return nil
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Base classes an `@ExpoView` may inherit from. The check is syntactic, matching the name in the
|
|
226
|
+
/// class's own inheritance clause: both `: ExpoView` and a qualified `: ExpoModulesCore.ExpoView`
|
|
227
|
+
/// pass, since `baseIdentifier` reads the trailing name.
|
|
228
|
+
///
|
|
229
|
+
/// Indirect inheritance does not: a view whose superclass is itself an `ExpoView` subclass
|
|
230
|
+
/// (`class Base: ExpoView` then `class CardView: Base`) is rejected, because a macro can't resolve
|
|
231
|
+
/// the superclass to check. `@SharedObject` has the same limitation, through the same helper. The
|
|
232
|
+
/// workaround is to apply `@ExpoView` to the class that names `ExpoView` directly.
|
|
233
|
+
private let expoViewBaseClassNames: Set<String> = ["ExpoView"]
|
|
234
|
+
|
|
235
|
+
/// The attribute's single generic argument, verbatim (`@ExpoView<CardProps>` → `"CardProps"`), or
|
|
236
|
+
/// `nil` when the attribute carries no generic argument clause.
|
|
237
|
+
///
|
|
238
|
+
/// The type is available syntactically, with no type resolution. Two spellings carry it, mirroring
|
|
239
|
+
/// `baseIdentifier`'s handling of the inheritance clause: a bare `@ExpoView<CardProps>` parses as an
|
|
240
|
+
/// `IdentifierTypeSyntax`, and a qualified `@ExpoModulesCore.ExpoView<CardProps>` as a
|
|
241
|
+
/// `MemberTypeSyntax`. Both have a `genericArgumentClause`, and both are legal at the use site, so
|
|
242
|
+
/// reading only the bare form would reject valid code while claiming the author omitted the argument.
|
|
243
|
+
///
|
|
244
|
+
/// Only the first argument is read: core's declaration has exactly one parameter, so a second is
|
|
245
|
+
/// already a compile error at the use site ("specialized with too many type parameters").
|
|
246
|
+
private func genericArgument(of attribute: AttributeSyntax) -> String? {
|
|
247
|
+
let arguments: GenericArgumentListSyntax?
|
|
248
|
+
if let identifier = attribute.attributeName.as(IdentifierTypeSyntax.self) {
|
|
249
|
+
arguments = identifier.genericArgumentClause?.arguments
|
|
250
|
+
} else if let member = attribute.attributeName.as(MemberTypeSyntax.self) {
|
|
251
|
+
arguments = member.genericArgumentClause?.arguments
|
|
252
|
+
} else {
|
|
253
|
+
arguments = nil
|
|
254
|
+
}
|
|
255
|
+
guard let first = arguments?.first, case .type(let type) = first.argument else {
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
return type.trimmedDescription
|
|
259
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// Recognition of the free-form (`Any`-bearing) boundary types and the spellings the macros emit or
|
|
4
|
+
/// suggest for them. None of these types can conform to the JS codable protocols (`Any` can't conform
|
|
5
|
+
/// to a protocol, and the container conditional conformances require the element/value to conform), so
|
|
6
|
+
/// they can't cross the boundary through the usual `T.decode` / `T.encode` path. Instead they're
|
|
7
|
+
/// accepted **only** in decode position (function/constructor arguments), decoded through a dedicated
|
|
8
|
+
/// `JavaScriptValue.decodeAny…` entry point; a free-form return or getter is rejected, since there's
|
|
9
|
+
/// no free-form encode.
|
|
10
|
+
///
|
|
11
|
+
/// Shared across the macro pipeline: `TypeConformanceAssertion` skips them, `MacroHelpers.decodeCall`
|
|
12
|
+
/// reroutes their decode, and `JSMacro` uses them for the steering diagnostics.
|
|
13
|
+
///
|
|
14
|
+
/// `@Record` accepts a property whose type mentions `Any` anywhere (`Any?`, `[String: Any]?`,
|
|
15
|
+
/// `[String: [Any]]`, …) and converts it on the JS-value paths without `decode`/`encode`: the three
|
|
16
|
+
/// free-form shapes, optionally wrapped in one optional, use the dedicated `JavaScriptValue.decodeAny…`
|
|
17
|
+
/// and `encodeAny…` methods, and every other type uses the generic `decodeAny(_:as:in:)` and
|
|
18
|
+
/// `encodeAny(_:in:)`.
|
|
19
|
+
|
|
20
|
+
/// The free-form boundary types: an untyped value (`Any`), an untyped array (`[Any]`), and an untyped
|
|
21
|
+
/// string-keyed dictionary (`[String: Any]`). Matched by whitespace-normalized spelling so
|
|
22
|
+
/// `[String: Any]` and `[String : Any]` both count.
|
|
23
|
+
private let freeFormBoundaryTypes: Set<String> = ["Any", "[Any]", "[String:Any]"]
|
|
24
|
+
|
|
25
|
+
/// The `JavaScriptValue.decodeAny…` method the binding calls to decode a free-form argument, keyed by
|
|
26
|
+
/// the free-form type's normalized spelling. `nil` for any non-free-form type.
|
|
27
|
+
private let freeFormDecodeMethods: [String: String] = [
|
|
28
|
+
"Any": "decodeAny",
|
|
29
|
+
"[Any]": "decodeAnyArray",
|
|
30
|
+
"[String:Any]": "decodeAnyDictionary",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
/// The type-safe alternative a free-form type's diagnostic steers to: the same shape with the `Any`
|
|
34
|
+
/// element replaced by `JavaScriptValue`, which conforms to the codable protocols. Keyed by the
|
|
35
|
+
/// free-form type's normalized spelling; spelled with conventional spacing for the message.
|
|
36
|
+
private let typedFreeFormReplacements: [String: String] = [
|
|
37
|
+
"Any": "JavaScriptValue",
|
|
38
|
+
"[Any]": "[JavaScriptValue]",
|
|
39
|
+
"[String:Any]": "[String: JavaScriptValue]",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/// True when a boundary type (as written) is one of the free-form spellings, ignoring internal
|
|
43
|
+
/// whitespace so `[String: Any]` and `[String :Any]` both match. Optionals are not free-form here: a
|
|
44
|
+
/// trailing `?` would route through `Optional.decode`, which free-form can't satisfy, so an optional
|
|
45
|
+
/// free-form type isn't recognized and stays a normal (failing) assertion.
|
|
46
|
+
internal func isFreeFormBoundaryType(_ type: String) -> Bool {
|
|
47
|
+
return freeFormBoundaryTypes.contains(normalizedTypeSpelling(type))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The `JavaScriptValue.decodeAny…` method name for a free-form boundary type, or `nil` when the type
|
|
51
|
+
/// isn't free-form. The binding emits `JavaScriptValue.<method>(arguments.unownedValue(at:), in:)` in
|
|
52
|
+
/// place of the type's own `.decode`.
|
|
53
|
+
internal func freeFormDecodeMethod(for type: String) -> String? {
|
|
54
|
+
return freeFormDecodeMethods[normalizedTypeSpelling(type)]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The type-safe alternative to suggest in place of a free-form type (its `Any` element replaced by
|
|
58
|
+
/// `JavaScriptValue`), or `nil` when the type isn't free-form.
|
|
59
|
+
internal func typedFreeFormReplacement(for type: String) -> String? {
|
|
60
|
+
return typedFreeFormReplacements[normalizedTypeSpelling(type)]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// A type spelling with all whitespace removed, so spelling variations of the same type
|
|
64
|
+
/// (`[String: Any]` vs `[String :Any]`) compare equal.
|
|
65
|
+
private func normalizedTypeSpelling(_ type: String) -> String {
|
|
66
|
+
return type.filter { !$0.isWhitespace }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// True when a type mentions `Any` anywhere in its spelling, at any nesting depth: `Any`, `[Any]?`,
|
|
70
|
+
/// `[String: Any]`, `Dictionary<String, [Any]>`, `[Int: Any]`, and so on. Such a type can't conform to
|
|
71
|
+
/// the JS codable protocols.
|
|
72
|
+
internal func mentionsFreeFormAny(_ type: TypeSyntax) -> Bool {
|
|
73
|
+
let finder = AnyTypeFinder(viewMode: .sourceAccurate)
|
|
74
|
+
finder.walk(type)
|
|
75
|
+
return finder.found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// True when a type mentioning `Any` is built only from `Any`, arrays, `String`-keyed dictionaries and
|
|
79
|
+
/// optionals, in any nesting (`[String: [Any]]?`, `[[String: Any]]`, …). Those are the only shapes the
|
|
80
|
+
/// free-form conversions produce and accept, so any other type holding `Any` (`[Int: Any]`,
|
|
81
|
+
/// `Set<[String: Any]>`, a user generic `Box<Any>`) could never convert.
|
|
82
|
+
internal func isConvertibleFreeFormType(_ type: TypeSyntax) -> Bool {
|
|
83
|
+
if isAnyType(type) {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
if let wrapped = optionalWrappedType(type) {
|
|
87
|
+
return isConvertibleFreeFormType(wrapped)
|
|
88
|
+
}
|
|
89
|
+
if let element = arrayElementType(type) {
|
|
90
|
+
return isConvertibleFreeFormType(element)
|
|
91
|
+
}
|
|
92
|
+
if let value = stringKeyedDictionaryValueType(type) {
|
|
93
|
+
return isConvertibleFreeFormType(value)
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The three free-form shapes that have dedicated conversions.
|
|
99
|
+
internal enum FreeFormShape {
|
|
100
|
+
case any
|
|
101
|
+
case array
|
|
102
|
+
case dictionary
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// The free-form shape a type has exactly (`Any`, `[Any]` or `[String: Any]`, in any spelling), or
|
|
106
|
+
/// `nil` for every other type, including nested and optional ones.
|
|
107
|
+
internal func freeFormShape(of type: TypeSyntax) -> FreeFormShape? {
|
|
108
|
+
if isAnyType(type) {
|
|
109
|
+
return .any
|
|
110
|
+
}
|
|
111
|
+
if let element = arrayElementType(type), isAnyType(element) {
|
|
112
|
+
return .array
|
|
113
|
+
}
|
|
114
|
+
if let value = stringKeyedDictionaryValueType(type), isAnyType(value) {
|
|
115
|
+
return .dictionary
|
|
116
|
+
}
|
|
117
|
+
return nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// True when the type is `Any` itself, spelled `Any` or `Swift.Any` (not `AnyObject`, `AnyHashable` or
|
|
121
|
+
/// a generic).
|
|
122
|
+
internal func isAnyType(_ type: some SyntaxProtocol) -> Bool {
|
|
123
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
124
|
+
return identifier.name.text == "Any" && identifier.genericArgumentClause == nil
|
|
125
|
+
}
|
|
126
|
+
if let member = type.as(MemberTypeSyntax.self),
|
|
127
|
+
member.name.text == "Any",
|
|
128
|
+
member.genericArgumentClause == nil,
|
|
129
|
+
let base = member.baseType.as(IdentifierTypeSyntax.self) {
|
|
130
|
+
return base.name.text == "Swift"
|
|
131
|
+
}
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The element type of `[T]` or `Array<T>`, or `nil` for any other type.
|
|
136
|
+
private func arrayElementType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
137
|
+
if let array = type.as(ArrayTypeSyntax.self) {
|
|
138
|
+
return array.element
|
|
139
|
+
}
|
|
140
|
+
guard let arguments = genericArguments(of: type, named: "Array"), arguments.count == 1 else {
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
return arguments[0]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The value type of `[String: V]` or `Dictionary<String, V>`, or `nil` for any other type, including a
|
|
147
|
+
/// dictionary with a different key type.
|
|
148
|
+
private func stringKeyedDictionaryValueType(_ type: TypeSyntax) -> TypeSyntax? {
|
|
149
|
+
if let dictionary = type.as(DictionaryTypeSyntax.self) {
|
|
150
|
+
return isStringType(dictionary.key) ? dictionary.value : nil
|
|
151
|
+
}
|
|
152
|
+
guard let arguments = genericArguments(of: type, named: "Dictionary"), arguments.count == 2 else {
|
|
153
|
+
return nil
|
|
154
|
+
}
|
|
155
|
+
return isStringType(arguments[0]) ? arguments[1] : nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// The type arguments of a generic type spelled `<name><…>` (or `Swift.<name><…>`), or `nil` when the
|
|
159
|
+
/// type has another name or no type arguments.
|
|
160
|
+
private func genericArguments(of type: TypeSyntax, named name: String) -> [TypeSyntax]? {
|
|
161
|
+
let clause: GenericArgumentClauseSyntax?
|
|
162
|
+
if let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == name {
|
|
163
|
+
clause = identifier.genericArgumentClause
|
|
164
|
+
} else if let member = type.as(MemberTypeSyntax.self),
|
|
165
|
+
member.name.text == name,
|
|
166
|
+
member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift" {
|
|
167
|
+
clause = member.genericArgumentClause
|
|
168
|
+
} else {
|
|
169
|
+
return nil
|
|
170
|
+
}
|
|
171
|
+
guard let clause else {
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
var types: [TypeSyntax] = []
|
|
175
|
+
for argument in clause.arguments {
|
|
176
|
+
guard case .type(let type) = argument.argument else {
|
|
177
|
+
return nil
|
|
178
|
+
}
|
|
179
|
+
types.append(type)
|
|
180
|
+
}
|
|
181
|
+
return types
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// True when the type is `String` or `Swift.String`.
|
|
185
|
+
private func isStringType(_ type: TypeSyntax) -> Bool {
|
|
186
|
+
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
187
|
+
return identifier.name.text == "String" && identifier.genericArgumentClause == nil
|
|
188
|
+
}
|
|
189
|
+
if let member = type.as(MemberTypeSyntax.self), member.name.text == "String" {
|
|
190
|
+
return member.baseType.as(IdentifierTypeSyntax.self)?.name.text == "Swift"
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Walks a type looking for `Any` (or `Swift.Any`) anywhere inside it.
|
|
196
|
+
private final class AnyTypeFinder: SyntaxVisitor {
|
|
197
|
+
var found = false
|
|
198
|
+
|
|
199
|
+
override func visit(_ node: IdentifierTypeSyntax) -> SyntaxVisitorContinueKind {
|
|
200
|
+
if isAnyType(node) {
|
|
201
|
+
found = true
|
|
202
|
+
return .skipChildren
|
|
203
|
+
}
|
|
204
|
+
return .visitChildren
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override func visit(_ node: MemberTypeSyntax) -> SyntaxVisitorContinueKind {
|
|
208
|
+
if isAnyType(node) {
|
|
209
|
+
found = true
|
|
210
|
+
return .skipChildren
|
|
211
|
+
}
|
|
212
|
+
return .visitChildren
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
|
|
3
|
+
/// A `@JS init` collected for direct JSI binding. A shared-object type has at most one (JS classes
|
|
4
|
+
/// have a single constructor). Instead of a `Constructor { … }` DSL entry, the macro synthesizes an
|
|
5
|
+
/// override of `SharedObject._constructSharedObject(...)` that decodes the JS arguments and returns a
|
|
6
|
+
/// fresh instance; unlike the method/property bindings it produces the native instance rather than
|
|
7
|
+
/// recovering one.
|
|
8
|
+
internal struct JSConstructor {
|
|
9
|
+
let parameters: [FunctionParameterSyntax]
|
|
10
|
+
|
|
11
|
+
init(initDecl: InitializerDeclSyntax) {
|
|
12
|
+
self.parameters = Array(initDecl.signature.parameterClause.parameters)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// The body statements, indented with `indent`: arity guard, per-argument decode through
|
|
16
|
+
/// `JavaScriptDecodable.decode` on a zero-copy `arguments.unownedValue(at:)` (the arity guard proves
|
|
17
|
+
/// each index is in bounds), then `return <Type>(label: arg0, …)`.
|
|
18
|
+
private func bodyStatements(typeName: String, indent: String) -> String {
|
|
19
|
+
var lines: [String] = []
|
|
20
|
+
|
|
21
|
+
lines.append(
|
|
22
|
+
"""
|
|
23
|
+
guard arguments.count == \(parameters.count) else {
|
|
24
|
+
throw Exceptions.ArgumentsRangeMismatch((functionName: "\(typeName)", received: arguments.count, required: \(parameters.count), maximum: \(parameters.count)))
|
|
25
|
+
}
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
var callArguments: [String] = []
|
|
29
|
+
for (index, parameter) in parameters.enumerated() {
|
|
30
|
+
let type = parameter.type.trimmedDescription
|
|
31
|
+
lines.append("let arg\(index) = try \(decodeCall(type, from: "arguments.unownedValue(at: \(index))"))")
|
|
32
|
+
|
|
33
|
+
let label = parameter.firstName.text
|
|
34
|
+
callArguments.append(label == "_" ? "arg\(index)" : "\(label): arg\(index)")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.append("return \(typeName)(\(callArguments.joined(separator: ", ")))")
|
|
38
|
+
|
|
39
|
+
return lines
|
|
40
|
+
.flatMap { $0.split(separator: "\n", omittingEmptySubsequences: false) }
|
|
41
|
+
.map { indent + $0 }
|
|
42
|
+
.joined(separator: "\n")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Builds an instance from the JS arguments. Overrides the base `SharedObject` class method; returns
|
|
46
|
+
/// the concrete instance, which promotes to the base `SharedObject?` return type.
|
|
47
|
+
func buildConstructor(typeName: String) -> DeclSyntax {
|
|
48
|
+
return """
|
|
49
|
+
@JavaScriptActor
|
|
50
|
+
public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
|
|
51
|
+
\(raw: bodyStatements(typeName: typeName, indent: " "))
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
}
|
|
55
|
+
}
|