expo-modules-macros 0.25.0 → 0.27.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/AGENTS.md +8 -8
- package/README.md +40 -3
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.swift +10 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/CLI.swift +45 -2
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +120 -0
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +183 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ExportedSurface.swift +3 -2
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ResolveRefs.swift +9 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/TypeNode.swift +47 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/SourceScan.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/Expand/MacroExpander.swift +484 -0
- package/apple/Sources/ExpoModulesMacros/{EventMacro.swift → Macros/EventMacro.swift} +23 -10
- package/apple/Sources/ExpoModulesMacros/{ExpoModuleMacro.swift → Macros/ExpoModuleMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{JSMacro.swift → Macros/JSMacro.swift} +71 -54
- package/apple/Sources/ExpoModulesMacros/{RecordMacro.swift → Macros/RecordMacro.swift} +18 -5
- package/apple/Sources/ExpoModulesMacros/{SharedObjectMacro.swift → Macros/SharedObjectMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{UnionMacro.swift → Macros/UnionMacro.swift} +16 -5
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +9 -14
- package/apple/Sources/ExpoModulesMacros/ProvidedMacros.swift +31 -0
- package/apple/Sources/ExpoModulesMacros/{ClosureArguments.swift → Support/ClosureArguments.swift} +30 -4
- package/apple/Sources/ExpoModulesMacros/{DecorateModuleBuilder.swift → Support/DecorateModuleBuilder.swift} +5 -1
- package/apple/Sources/ExpoModulesMacros/{MacroHelpers.swift → Support/MacroHelpers.swift} +11 -4
- package/apple/Sources/ExpoModulesMacros/Support/TupleTypes.swift +272 -0
- package/apple/Sources/ExpoModulesMacros/Support/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/{TypeConformanceAssertion.swift → Support/TypeConformanceAssertion.swift} +104 -41
- package/build/index.d.ts +31 -1
- package/build/index.js +34 -0
- package/build/types.d.ts +77 -2
- package/build/types.js +4 -3
- package/package.json +5 -5
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/Availability.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ScanExports.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Modules → ExpoModulesCLI/Commands/ScanModules}/ScanModules.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/Detection.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/DetectionVisitor.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/ScanBuildConfiguration.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/StandardStreams.swift +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoModulesOptimizedMacro.swift → Macros/ExpoModulesOptimizedMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoViewMacro.swift → Macros/ExpoViewMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ViewPropsMacro.swift → Macros/ViewPropsMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{FreeFormTypes.swift → Support/FreeFormTypes.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSConstructor.swift → Support/JSConstructor.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSNameCollisions.swift → Support/JSNameCollisions.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{OptimizedFunctionHelpers.swift → Support/OptimizedFunctionHelpers.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{Receiver.swift → Support/Receiver.swift} +0 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import ExpoModulesCLI
|
|
2
|
+
import SwiftBasicFormat
|
|
3
|
+
import SwiftDiagnostics
|
|
4
|
+
import SwiftParser
|
|
5
|
+
import SwiftParserDiagnostics
|
|
6
|
+
import SwiftSyntax
|
|
7
|
+
import SwiftSyntaxMacroExpansion
|
|
8
|
+
import SwiftSyntaxMacros
|
|
9
|
+
|
|
10
|
+
/// Expands the macros of the plugin in one source file in process, with the same swift-syntax
|
|
11
|
+
/// machinery as `assertMacroExpansion`. The macros only read the syntax tree, so the generated code
|
|
12
|
+
/// is what the compiler gets, except for the names from `makeUniqueName`. The compiler also skips the
|
|
13
|
+
/// extension conformances that a type already has; the macros check the inheritance clause for that
|
|
14
|
+
/// too, so the result differs only when a conformance is inherited from somewhere else.
|
|
15
|
+
///
|
|
16
|
+
/// Without `symbols`, it returns one expansion of the whole file. Otherwise it returns one expansion
|
|
17
|
+
/// for each symbol declared in the file, and only the diagnostics inside those declarations:
|
|
18
|
+
///
|
|
19
|
+
/// - A type (`MyModule`, `MyModule.Options`): the expanded type and the extensions that its macros
|
|
20
|
+
/// add.
|
|
21
|
+
/// - A member (`MyModule.greet`, `MyModule.init`): the expanded type that declares it, with only the
|
|
22
|
+
/// declarations of that name (all overloads). The member macros of the type then generate only the
|
|
23
|
+
/// code for that member. A macro that reads other members (for example to find a name collision)
|
|
24
|
+
/// can generate a little differently than for the whole type.
|
|
25
|
+
///
|
|
26
|
+
/// A symbol that the file doesn't declare is left out of the expansions.
|
|
27
|
+
public func expandMacros(source: String, file: String, symbols: [String]) -> ExpandedFile {
|
|
28
|
+
let tree = Parser.parse(source: source)
|
|
29
|
+
let converter = SourceLocationConverter(fileName: file, tree: tree)
|
|
30
|
+
let (expanded, context) = expand(tree, file: file)
|
|
31
|
+
|
|
32
|
+
var diagnostics = ParseDiagnosticsGenerator.diagnostics(for: tree).map { diagnostic in
|
|
33
|
+
expansionDiagnostic(diagnostic) { position, _ in converter.location(for: position) }
|
|
34
|
+
}
|
|
35
|
+
let originalDeclarations = OriginalDeclarations(tree)
|
|
36
|
+
diagnostics += context.diagnostics.map { diagnostic in
|
|
37
|
+
expansionDiagnostic(diagnostic) { position, node in
|
|
38
|
+
let location = context.location(for: position, anchoredAt: node, fileName: file)
|
|
39
|
+
if let original = originalDeclarations.position(of: position, in: node, shiftedOffset: location.offset) {
|
|
40
|
+
return converter.location(for: original)
|
|
41
|
+
}
|
|
42
|
+
return location
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
diagnostics.sort { ($0.line, $0.column) < ($1.line, $1.column) }
|
|
46
|
+
|
|
47
|
+
guard !symbols.isEmpty else {
|
|
48
|
+
return ExpandedFile(
|
|
49
|
+
file: file,
|
|
50
|
+
expansions: [Expansion(symbol: nil, source: expanded.description)],
|
|
51
|
+
diagnostics: diagnostics
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let declarations = SymbolIndex(tree).declarations
|
|
56
|
+
var expansions: [Expansion] = []
|
|
57
|
+
var lineRanges: [ClosedRange<Int>] = []
|
|
58
|
+
for symbol in symbols {
|
|
59
|
+
let types = declarations.filter { $0.path == symbol && $0.kind == .type }
|
|
60
|
+
let members = declarations.filter { $0.path == symbol && $0.kind == .member }
|
|
61
|
+
|
|
62
|
+
if !types.isEmpty {
|
|
63
|
+
let source = render(typePath: symbol, in: expanded, original: tree) { index in
|
|
64
|
+
index.filter { $0.path == symbol && $0.kind == .type }.map(\.node)
|
|
65
|
+
}
|
|
66
|
+
expansions.append(Expansion(symbol: symbol, source: source))
|
|
67
|
+
lineRanges += types.map { lineRange(of: $0.node, converter: converter) }
|
|
68
|
+
} else if !members.isEmpty {
|
|
69
|
+
let pruned = MemberPruner(
|
|
70
|
+
parents: Set(members.compactMap(\.parent?.id)),
|
|
71
|
+
kept: Set(members.map(\.node.id))
|
|
72
|
+
)
|
|
73
|
+
.rewrite(tree)
|
|
74
|
+
guard let prunedTree = pruned.as(SourceFileSyntax.self) else {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
let parentPath = String(symbol[..<(symbol.lastIndex(of: ".") ?? symbol.startIndex)])
|
|
78
|
+
let (prunedExpansion, _) = expand(prunedTree, file: file)
|
|
79
|
+
// Only the types or extensions that declare the member: another extension of the same type
|
|
80
|
+
// isn't pruned.
|
|
81
|
+
let source = render(typePath: parentPath, in: prunedExpansion, original: prunedTree) { index in
|
|
82
|
+
let parents = Set(index.filter { $0.path == symbol && $0.kind == .member }.compactMap(\.parent?.id))
|
|
83
|
+
return index.filter { parents.contains($0.node.id) }.map(\.node)
|
|
84
|
+
}
|
|
85
|
+
expansions.append(Expansion(symbol: symbol, source: source))
|
|
86
|
+
lineRanges += members.map { lineRange(of: $0.node, converter: converter) }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ExpandedFile(
|
|
91
|
+
file: file,
|
|
92
|
+
expansions: expansions,
|
|
93
|
+
diagnostics: diagnostics.filter { diagnostic in
|
|
94
|
+
lineRanges.contains { $0.contains(diagnostic.line) }
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Expands every macro of the plugin in `tree`.
|
|
100
|
+
private func expand(
|
|
101
|
+
_ tree: SourceFileSyntax,
|
|
102
|
+
file: String
|
|
103
|
+
) -> (expanded: Syntax, context: BasicMacroExpansionContext) {
|
|
104
|
+
let specs = Dictionary(
|
|
105
|
+
uniqueKeysWithValues: ProvidedMacro.all.map {
|
|
106
|
+
($0.name, MacroSpec(type: $0.type, conformances: $0.conformances))
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
let context = BasicMacroExpansionContext(
|
|
110
|
+
sourceFiles: [tree: .init(moduleName: "ExpandedModule", fullFilePath: file)]
|
|
111
|
+
)
|
|
112
|
+
let expanded = tree.expand(
|
|
113
|
+
macroSpecs: specs,
|
|
114
|
+
contextGenerator: { node in
|
|
115
|
+
BasicMacroExpansionContext(sharingWith: context, lexicalContext: node.allMacroLexicalContexts())
|
|
116
|
+
},
|
|
117
|
+
// The generated code follows the indentation of the file.
|
|
118
|
+
indentationWidth: BasicFormat.inferIndentation(of: tree) ?? .spaces(2)
|
|
119
|
+
)
|
|
120
|
+
return (expanded, context)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// The expanded declarations that `select` picks from the symbols of `expanded`, each without the
|
|
124
|
+
/// indentation of its nesting, followed by the extensions that the extension macros add for the type
|
|
125
|
+
/// at `typePath`.
|
|
126
|
+
private func render(
|
|
127
|
+
typePath: String,
|
|
128
|
+
in expanded: Syntax,
|
|
129
|
+
original: SourceFileSyntax,
|
|
130
|
+
select: ([SymbolDeclaration]) -> [Syntax]
|
|
131
|
+
) -> String {
|
|
132
|
+
let declarations = select(SymbolIndex(expanded).declarations)
|
|
133
|
+
// The macro system adds the extensions after the expanded statements of the file, with the type
|
|
134
|
+
// name as the macro got it (qualified for a nested type). So they are the last statements, after
|
|
135
|
+
// the extensions that already end the file.
|
|
136
|
+
let statements = expanded.as(SourceFileSyntax.self)?.statements ?? []
|
|
137
|
+
let addedCount =
|
|
138
|
+
trailingExtensions(of: statements).count - trailingExtensions(of: original.statements).count
|
|
139
|
+
let addedExtensions = trailingExtensions(of: statements).suffix(max(addedCount, 0)).filter {
|
|
140
|
+
$0.extendedType.trimmedDescription == typePath
|
|
141
|
+
}
|
|
142
|
+
let nodes = declarations + addedExtensions.map(Syntax.init)
|
|
143
|
+
return nodes.map(withoutNestingIndentation).joined(separator: "\n\n") + "\n"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The extensions at the end of `statements`, up to the last statement that isn't one.
|
|
147
|
+
private func trailingExtensions(of statements: CodeBlockItemListSyntax) -> [ExtensionDeclSyntax] {
|
|
148
|
+
var extensions: [ExtensionDeclSyntax] = []
|
|
149
|
+
for item in statements.reversed() {
|
|
150
|
+
guard let extensionDecl = item.item.as(ExtensionDeclSyntax.self) else {
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
extensions.insert(extensionDecl, at: 0)
|
|
154
|
+
}
|
|
155
|
+
return extensions
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// The source of `node` without its leading trivia, and with the indentation of its first line
|
|
159
|
+
/// removed from all the lines.
|
|
160
|
+
private func withoutNestingIndentation(_ node: Syntax) -> String {
|
|
161
|
+
var indentation = ""
|
|
162
|
+
for piece in node.leadingTrivia.pieces {
|
|
163
|
+
switch piece {
|
|
164
|
+
case .newlines, .carriageReturns, .carriageReturnLineFeeds:
|
|
165
|
+
indentation = ""
|
|
166
|
+
case .spaces(let count):
|
|
167
|
+
indentation += String(repeating: " ", count: count)
|
|
168
|
+
case .tabs(let count):
|
|
169
|
+
indentation += String(repeating: "\t", count: count)
|
|
170
|
+
default:
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let lines = node.trimmedDescription.split(separator: "\n", omittingEmptySubsequences: false)
|
|
175
|
+
return lines.enumerated().map { index, line in
|
|
176
|
+
index > 0 && line.hasPrefix(indentation) ? String(line.dropFirst(indentation.count)) : String(line)
|
|
177
|
+
}
|
|
178
|
+
.joined(separator: "\n")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// The lines of `node` in the original file, from its attributes to its closing brace.
|
|
182
|
+
private func lineRange(of node: Syntax, converter: SourceLocationConverter) -> ClosedRange<Int> {
|
|
183
|
+
let start = converter.location(for: node.positionAfterSkippingLeadingTrivia).line
|
|
184
|
+
let end = converter.location(for: node.endPositionBeforeTrailingTrivia).line
|
|
185
|
+
return start...end
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func expansionDiagnostic(
|
|
189
|
+
_ diagnostic: Diagnostic,
|
|
190
|
+
location: (AbsolutePosition, Syntax) -> SourceLocation
|
|
191
|
+
) -> ExpansionDiagnostic {
|
|
192
|
+
let start = location(diagnostic.position, diagnostic.node)
|
|
193
|
+
return ExpansionDiagnostic(
|
|
194
|
+
severity: severity(diagnostic.diagMessage.severity),
|
|
195
|
+
message: diagnostic.message,
|
|
196
|
+
line: start.line,
|
|
197
|
+
column: start.column,
|
|
198
|
+
notes: diagnostic.notes.map { note in
|
|
199
|
+
let noteStart = location(note.position, note.node)
|
|
200
|
+
return ExpansionNote(message: note.message, line: noteStart.line, column: noteStart.column)
|
|
201
|
+
},
|
|
202
|
+
fixIts: diagnostic.fixIts.map(\.message.message)
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func severity(_ severity: DiagnosticSeverity) -> ExpansionDiagnostic.Severity {
|
|
207
|
+
switch severity {
|
|
208
|
+
case .error: .error
|
|
209
|
+
case .warning: .warning
|
|
210
|
+
case .note: .note
|
|
211
|
+
case .remark: .remark
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MARK: - Diagnostic locations
|
|
216
|
+
|
|
217
|
+
/// Maps a position in a declaration that a macro got to the same position in the original file.
|
|
218
|
+
///
|
|
219
|
+
/// The macro system detaches the declaration that it passes to an attached macro and maps positions
|
|
220
|
+
/// in it back to the declaration it detached it from. But a peer macro gets the declaration with the
|
|
221
|
+
/// attributes that the member attribute macros of its type added, for example the `@JavaScriptActor`
|
|
222
|
+
/// that `@ExpoModule` adds to a `@JS` function, and that declaration isn't in the original file. So
|
|
223
|
+
/// every position after the added attributes is off by their length. A diagnostic in a parameter
|
|
224
|
+
/// type of a `@JS` function would be on the next line.
|
|
225
|
+
///
|
|
226
|
+
/// The text from the keyword of the declaration (`func`, `var`, `init`) to its end is the same in
|
|
227
|
+
/// both, so this finds the original declaration by that text and moves the position by the offset of
|
|
228
|
+
/// the keyword. A position before the keyword, in the attributes or the modifiers, isn't mapped.
|
|
229
|
+
private struct OriginalDeclarations {
|
|
230
|
+
/// The positions of the keywords of the declarations in the original file, in source order, by the
|
|
231
|
+
/// text from the keyword to the end of the declaration.
|
|
232
|
+
private let keywordPositions: [String: [AbsolutePosition]]
|
|
233
|
+
|
|
234
|
+
init(_ tree: SourceFileSyntax) {
|
|
235
|
+
let collector = DeclarationCollector(viewMode: .sourceAccurate)
|
|
236
|
+
collector.walk(tree)
|
|
237
|
+
var keywordPositions: [String: [AbsolutePosition]] = [:]
|
|
238
|
+
for declaration in collector.declarations {
|
|
239
|
+
guard let keyword = keywordToken(of: declaration) else {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
keywordPositions[textFromKeyword(keyword, of: declaration), default: []].append(
|
|
243
|
+
keyword.positionAfterSkippingLeadingTrivia
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
self.keywordPositions = keywordPositions
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// The position in the original file of `position` in `node`, or `nil` when `node` is in the
|
|
250
|
+
/// original file, isn't in a detached declaration, or is before its keyword.
|
|
251
|
+
///
|
|
252
|
+
/// `shiftedOffset` is the offset that the macro system maps `position` to. The added attributes
|
|
253
|
+
/// only move it forward, so when declarations with the same text are in the file, the one it is
|
|
254
|
+
/// in is the last one that starts at or before that offset.
|
|
255
|
+
func position(of position: AbsolutePosition, in node: Syntax, shiftedOffset: Int) -> AbsolutePosition? {
|
|
256
|
+
guard !node.root.is(SourceFileSyntax.self),
|
|
257
|
+
let declaration = node.root.as(DeclSyntax.self),
|
|
258
|
+
let keyword = keywordToken(of: declaration),
|
|
259
|
+
position >= keyword.positionAfterSkippingLeadingTrivia,
|
|
260
|
+
let candidates = keywordPositions[textFromKeyword(keyword, of: declaration)]
|
|
261
|
+
else {
|
|
262
|
+
return nil
|
|
263
|
+
}
|
|
264
|
+
let offset = position.utf8Offset - keyword.positionAfterSkippingLeadingTrivia.utf8Offset
|
|
265
|
+
let shiftedKeyword = shiftedOffset - offset
|
|
266
|
+
guard let originalKeyword = candidates.last(where: { $0.utf8Offset <= shiftedKeyword }) ?? candidates.first
|
|
267
|
+
else {
|
|
268
|
+
return nil
|
|
269
|
+
}
|
|
270
|
+
return AbsolutePosition(utf8Offset: originalKeyword.utf8Offset + offset)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// The first token of `declaration` after its attributes and modifiers, such as `func` or `var`.
|
|
275
|
+
private func keywordToken(of declaration: DeclSyntax) -> TokenSyntax? {
|
|
276
|
+
for child in declaration.children(viewMode: .sourceAccurate)
|
|
277
|
+
where !child.is(AttributeListSyntax.self) && !child.is(DeclModifierListSyntax.self) {
|
|
278
|
+
if let token = child.firstToken(viewMode: .sourceAccurate) {
|
|
279
|
+
return token
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return nil
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// The source text of `declaration` from `keyword` to its end, without the trailing trivia.
|
|
286
|
+
private func textFromKeyword(_ keyword: TokenSyntax, of declaration: DeclSyntax) -> String {
|
|
287
|
+
let text = Array(declaration.description.utf8)
|
|
288
|
+
let start = keyword.positionAfterSkippingLeadingTrivia.utf8Offset - declaration.position.utf8Offset
|
|
289
|
+
let end = declaration.endPositionBeforeTrailingTrivia.utf8Offset - declaration.position.utf8Offset
|
|
290
|
+
return String(decoding: text[start..<end], as: UTF8.self)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Collects every declaration in a tree.
|
|
294
|
+
private final class DeclarationCollector: SyntaxAnyVisitor {
|
|
295
|
+
private(set) var declarations: [DeclSyntax] = []
|
|
296
|
+
|
|
297
|
+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
|
|
298
|
+
if let declaration = node.as(DeclSyntax.self) {
|
|
299
|
+
declarations.append(declaration)
|
|
300
|
+
}
|
|
301
|
+
return .visitChildren
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// MARK: - Symbols
|
|
306
|
+
|
|
307
|
+
/// A declaration that `--symbol` can name.
|
|
308
|
+
private struct SymbolDeclaration {
|
|
309
|
+
enum Kind {
|
|
310
|
+
case type
|
|
311
|
+
case `extension`
|
|
312
|
+
case member
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// The names from the outermost type to the declaration, joined with dots: `MyModule.greet`. An
|
|
316
|
+
/// extension has the path of the type it extends.
|
|
317
|
+
let path: String
|
|
318
|
+
let kind: Kind
|
|
319
|
+
let node: Syntax
|
|
320
|
+
|
|
321
|
+
/// For a member, the type or extension that declares it.
|
|
322
|
+
let parent: Syntax?
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Collects the types, extensions and members in a tree, also those inside `#if` blocks. Function
|
|
326
|
+
/// bodies are skipped, so a local type isn't a symbol.
|
|
327
|
+
private final class SymbolIndex: SyntaxVisitor {
|
|
328
|
+
private(set) var declarations: [SymbolDeclaration] = []
|
|
329
|
+
private var scopes: [(path: String, node: Syntax)] = []
|
|
330
|
+
|
|
331
|
+
init(_ tree: some SyntaxProtocol) {
|
|
332
|
+
super.init(viewMode: .sourceAccurate)
|
|
333
|
+
walk(tree)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
337
|
+
enterType(node, name: node.name)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
override func visitPost(_ node: ClassDeclSyntax) {
|
|
341
|
+
scopes.removeLast()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
345
|
+
enterType(node, name: node.name)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
override func visitPost(_ node: StructDeclSyntax) {
|
|
349
|
+
scopes.removeLast()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
353
|
+
enterType(node, name: node.name)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
override func visitPost(_ node: EnumDeclSyntax) {
|
|
357
|
+
scopes.removeLast()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
361
|
+
enterType(node, name: node.name)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
override func visitPost(_ node: ActorDeclSyntax) {
|
|
365
|
+
scopes.removeLast()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
369
|
+
enterType(node, name: node.name)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
override func visitPost(_ node: ProtocolDeclSyntax) {
|
|
373
|
+
scopes.removeLast()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
377
|
+
let path = qualified(node.extendedType.trimmedDescription)
|
|
378
|
+
declarations.append(SymbolDeclaration(path: path, kind: .extension, node: Syntax(node), parent: nil))
|
|
379
|
+
scopes.append((path, Syntax(node)))
|
|
380
|
+
return .visitChildren
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
override func visitPost(_ node: ExtensionDeclSyntax) {
|
|
384
|
+
scopes.removeLast()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
388
|
+
addMember(node, names: [node.name.text])
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
392
|
+
addMember(
|
|
393
|
+
node,
|
|
394
|
+
names: node.bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text }
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
399
|
+
addMember(node, names: ["init"])
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
403
|
+
addMember(node, names: ["subscript"])
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
407
|
+
addMember(node, names: node.elements.map(\.name.text))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private func enterType(_ node: some DeclSyntaxProtocol, name: TokenSyntax) -> SyntaxVisitorContinueKind {
|
|
411
|
+
let path = qualified(name.text)
|
|
412
|
+
declarations.append(SymbolDeclaration(path: path, kind: .type, node: Syntax(node), parent: nil))
|
|
413
|
+
scopes.append((path, Syntax(node)))
|
|
414
|
+
return .visitChildren
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// Records a member of the innermost type. Doesn't visit the children: a declaration in a body or
|
|
418
|
+
/// an accessor isn't a member.
|
|
419
|
+
private func addMember(_ node: some DeclSyntaxProtocol, names: [String]) -> SyntaxVisitorContinueKind {
|
|
420
|
+
if let scope = scopes.last {
|
|
421
|
+
for name in names.map(withoutBackticks) {
|
|
422
|
+
declarations.append(
|
|
423
|
+
SymbolDeclaration(path: "\(scope.path).\(name)", kind: .member, node: Syntax(node), parent: scope.node)
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return .skipChildren
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// `name` inside the innermost type, without backticks: `` `default` `` is `default`.
|
|
431
|
+
private func qualified(_ name: String) -> String {
|
|
432
|
+
let name = withoutBackticks(name)
|
|
433
|
+
return scopes.last.map { "\($0.path).\(name)" } ?? name
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Removes from the member blocks of `parents` every member that isn't in `kept`, except the type
|
|
438
|
+
/// aliases. An `#if` block stays with the kept members inside it.
|
|
439
|
+
private final class MemberPruner: SyntaxRewriter {
|
|
440
|
+
private let parents: Set<SyntaxIdentifier>
|
|
441
|
+
private let kept: Set<SyntaxIdentifier>
|
|
442
|
+
|
|
443
|
+
init(parents: Set<SyntaxIdentifier>, kept: Set<SyntaxIdentifier>) {
|
|
444
|
+
self.parents = parents
|
|
445
|
+
self.kept = kept
|
|
446
|
+
super.init(viewMode: .sourceAccurate)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
override func visit(_ node: MemberBlockSyntax) -> MemberBlockSyntax {
|
|
450
|
+
guard let parent = node.parent, parents.contains(parent.id) else {
|
|
451
|
+
return super.visit(node)
|
|
452
|
+
}
|
|
453
|
+
return node.with(\.members, prune(node.members))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private func prune(_ members: MemberBlockItemListSyntax) -> MemberBlockItemListSyntax {
|
|
457
|
+
MemberBlockItemListSyntax(
|
|
458
|
+
members.compactMap { item -> MemberBlockItemSyntax? in
|
|
459
|
+
// The macros resolve the type aliases of the type in the types of its members.
|
|
460
|
+
if kept.contains(item.decl.id) || item.decl.is(TypeAliasDeclSyntax.self) {
|
|
461
|
+
return item
|
|
462
|
+
}
|
|
463
|
+
guard let ifConfig = item.decl.as(IfConfigDeclSyntax.self) else {
|
|
464
|
+
return nil
|
|
465
|
+
}
|
|
466
|
+
let clauses = ifConfig.clauses.map { clause -> IfConfigClauseSyntax in
|
|
467
|
+
guard case .decls(let decls) = clause.elements else {
|
|
468
|
+
return clause
|
|
469
|
+
}
|
|
470
|
+
return clause.with(\.elements, .decls(prune(decls)))
|
|
471
|
+
}
|
|
472
|
+
let keepsMember = clauses.contains { clause in
|
|
473
|
+
guard case .decls(let decls) = clause.elements else {
|
|
474
|
+
return false
|
|
475
|
+
}
|
|
476
|
+
// A clause with only type aliases left keeps no member.
|
|
477
|
+
return decls.contains { !$0.decl.is(TypeAliasDeclSyntax.self) }
|
|
478
|
+
}
|
|
479
|
+
return keepsMember
|
|
480
|
+
? item.with(\.decl, DeclSyntax(ifConfig.with(\.clauses, IfConfigClauseListSyntax(clauses)))) : nil
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -53,11 +53,19 @@ public struct EventMacro: AccessorMacro {
|
|
|
53
53
|
// The closure's parameter and return types are inferred from the property's declared type
|
|
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
|
+
// A payload that holds a tuple can't conform to the payload protocols, so it's passed with a
|
|
57
|
+
// closure that encodes it.
|
|
56
58
|
let emitMethod = event.isSync ? "emitSync" : "emit"
|
|
57
|
-
let closure
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
let closure: String
|
|
60
|
+
if let tuple = event.payloadType.flatMap({ tupleConversion(of: $0) }) {
|
|
61
|
+
let encode = encoderClosure(encoding: tuple.encodeCall(of: "value", into: "slot"))
|
|
62
|
+
closure =
|
|
63
|
+
"{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload, encode: \(encode)) }"
|
|
64
|
+
} else if event.hasPayload {
|
|
65
|
+
closure = "{ [weak self] payload in self?.\(emitMethod)(event: \"\(event.jsName)\", payload: payload) }"
|
|
66
|
+
} else {
|
|
67
|
+
closure = "{ [weak self] in self?.\(emitMethod)(event: \"\(event.jsName)\") }"
|
|
68
|
+
}
|
|
61
69
|
return [
|
|
62
70
|
"""
|
|
63
71
|
get {
|
|
@@ -85,15 +93,20 @@ extension EventMacro: PeerMacro {
|
|
|
85
93
|
// through `self.emit`). Named after the member so the member shows up in either diagnostic,
|
|
86
94
|
// and asserting the enclosing type by its spelled name so the conformance error names the
|
|
87
95
|
// user's type rather than 'Self'.
|
|
96
|
+
//
|
|
97
|
+
// A payload that holds a tuple is encoded element by element (see `TupleConversion`), so the types
|
|
98
|
+
// in it are asserted `JavaScriptEncodable`, which the encode closure needs, in its place.
|
|
88
99
|
let name = event.swiftName
|
|
89
|
-
let
|
|
100
|
+
let tupleElementTypes = event.payloadType.flatMap { tupleConversion(of: $0)?.conformingTypes }
|
|
101
|
+
let payloadConstraint = tupleElementTypes == nil ? jsConvertibleProtocolName : javaScriptEncodableProtocolName
|
|
102
|
+
let payloads =
|
|
103
|
+
tupleElementTypes.map { distinctAssertableTypes($0, constraint: payloadConstraint) }
|
|
104
|
+
?? event.payloadType.flatMap { assertableBoundaryType($0, constraint: payloadConstraint) }.map { [$0] } ?? []
|
|
90
105
|
let owner = enclosingTypeName(in: context) ?? "Self"
|
|
91
106
|
let emitterParameter = opaqueMetatypeParameter(constraint: eventEmitterProtocolName)
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: "func \(name)(\(emitterParameter)) {}"
|
|
96
|
-
let call = payload.map { "\(name)(\($0).self, \(owner).self)" } ?? "\(name)(\(owner).self)"
|
|
107
|
+
let payloadParameters = payloads.map { _ in opaqueMetatypeParameter(constraint: payloadConstraint) }
|
|
108
|
+
let helper = "func \(name)(\((payloadParameters + [emitterParameter]).joined(separator: ", "))) {}"
|
|
109
|
+
let call = "\(name)(\((payloads + [owner]).map { "\($0).self" }.joined(separator: ", ")))"
|
|
97
110
|
let peerName = derivedIdentifier(from: name, prefix: "_assertTypesConformance_")
|
|
98
111
|
return [
|
|
99
112
|
"""
|
package/apple/Sources/ExpoModulesMacros/{ExpoModuleMacro.swift → Macros/ExpoModuleMacro.swift}
RENAMED
|
@@ -49,9 +49,15 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
49
49
|
// DSL entry.
|
|
50
50
|
var functions: [JSFunction] = []
|
|
51
51
|
var properties: [JSProperty] = []
|
|
52
|
+
var assertions: [MemberConformanceAssertion] = []
|
|
53
|
+
let aliases = TupleTypeAliases(in: classDecl.memberBlock)
|
|
52
54
|
|
|
53
55
|
for member in classDecl.memberBlock.members {
|
|
54
|
-
|
|
56
|
+
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
57
|
+
let decl = aliases.resolving(member.decl)
|
|
58
|
+
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
59
|
+
assertions.append(assertion)
|
|
60
|
+
}
|
|
55
61
|
|
|
56
62
|
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
57
63
|
let attribute = funcDecl.attributes.firstAttribute(named: "JS")
|
|
@@ -126,6 +132,12 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
126
132
|
emitted.append(buildDecorateJavaScriptObject(functions: functions, properties: properties))
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
// The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
|
|
136
|
+
// `jsConformanceAssertionBody`).
|
|
137
|
+
if let assertion = classConformanceAssertions(assertions) {
|
|
138
|
+
emitted.append(assertion)
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
return emitted
|
|
130
142
|
}
|
|
131
143
|
}
|