expo-modules-macros 0.29.0 → 0.31.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 +2 -2
- package/README.md +37 -4
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/CLI.swift +46 -9
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +38 -1
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/MacroExpander.swift +171 -31
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +43 -15
- package/apple/Sources/ExpoModulesCLI/Commands/Fix/RunFix.swift +290 -0
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +14 -7
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +11 -11
- package/apple/Sources/ExpoModulesCLI/Commands/ScanModules/ScanModules.swift +48 -34
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +92 -15
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +8 -4
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +54 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +9 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +60 -16
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +9 -37
- package/apple/Sources/ExpoModulesMacrosCore/Support/PublicAccess.swift +105 -0
- package/build/types.d.ts +17 -8
- package/build/types.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#if canImport(FoundationEssentials)
|
|
2
|
+
import FoundationEssentials
|
|
3
|
+
#else
|
|
4
|
+
import Foundation
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
/// The options of `fix`, parsed by `ScannerCLI`.
|
|
8
|
+
struct FixOptions: Equatable {
|
|
9
|
+
var paths: [String] = []
|
|
10
|
+
var symbols: [String] = []
|
|
11
|
+
var dryRun = false
|
|
12
|
+
var onlyErrors = false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// A Fix-It that `fix` applied or skipped, at the location of its diagnostic.
|
|
16
|
+
struct FixItReport: Equatable {
|
|
17
|
+
let line: Int
|
|
18
|
+
let column: Int
|
|
19
|
+
let message: String
|
|
20
|
+
|
|
21
|
+
/// Why `fix` skipped the Fix-It, or `nil` when it applied it.
|
|
22
|
+
let skipReason: String?
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The result of applying the Fix-Its of one file.
|
|
26
|
+
struct FixedSource: Equatable {
|
|
27
|
+
/// The source with the applied Fix-Its.
|
|
28
|
+
let source: String
|
|
29
|
+
let fixIts: [FixItReport]
|
|
30
|
+
|
|
31
|
+
/// Whether `fixSource` stopped after `maxFixPasses` passes while Fix-Its were left to apply.
|
|
32
|
+
var reachedPassLimit = false
|
|
33
|
+
|
|
34
|
+
var applied: [FixItReport] {
|
|
35
|
+
fixIts.filter { $0.skipReason == nil }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
extension Scanner {
|
|
40
|
+
/// Runs `fix`: expands the macros in each file like `expand` and applies the Fix-Its of their
|
|
41
|
+
/// diagnostics to the file, in place (see `fixSource`). It doesn't format the file.
|
|
42
|
+
///
|
|
43
|
+
/// Each applied Fix-It is printed to stdout as `<file>:<line>:<column>: fix-it: <message>` after its
|
|
44
|
+
/// file is written, followed by a summary, and each skipped one to stderr as a warning. With
|
|
45
|
+
/// `--dry-run`, the files aren't written. A file that can't be written is reported and the others are
|
|
46
|
+
/// still fixed. The exit code is `0` also when a Fix-It is skipped, `1` when a file can't be written,
|
|
47
|
+
/// and `2` for an input that `expand` doesn't take.
|
|
48
|
+
///
|
|
49
|
+
/// `write` writes a fixed file and returns whether it changed it (see `writeFixedSource`); tests
|
|
50
|
+
/// replace it. A file that another path already fixed, through a hard link, isn't printed or counted
|
|
51
|
+
/// again.
|
|
52
|
+
static func runFix(
|
|
53
|
+
options: FixOptions,
|
|
54
|
+
expander: MacroExpander,
|
|
55
|
+
write: (_ source: String, _ path: String, _ original: Data) throws -> Bool = writeFixedSource
|
|
56
|
+
) -> Int32 {
|
|
57
|
+
let sources: [(source: String, contents: Data, expanded: ExpandedFile)]
|
|
58
|
+
switch expandSources(options: ExpandOptions(paths: options.paths, symbols: options.symbols), expander: expander) {
|
|
59
|
+
case .success(let expanded):
|
|
60
|
+
sources = expanded
|
|
61
|
+
case .failure(let error):
|
|
62
|
+
writeToStandardError("error: \(error.message)\n")
|
|
63
|
+
return 2
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var appliedCount = 0
|
|
67
|
+
var fixedFiles = 0
|
|
68
|
+
var failedFiles = 0
|
|
69
|
+
for (source, contents, expanded) in sources {
|
|
70
|
+
let fixed = fixSource(
|
|
71
|
+
source,
|
|
72
|
+
expanded: expanded,
|
|
73
|
+
symbols: options.symbols,
|
|
74
|
+
onlyErrors: options.onlyErrors,
|
|
75
|
+
expander: expander
|
|
76
|
+
)
|
|
77
|
+
writeToStandardError(renderSkippedFixIts(fixed, file: expanded.file))
|
|
78
|
+
guard !fixed.applied.isEmpty else {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if !options.dryRun {
|
|
82
|
+
do {
|
|
83
|
+
guard try write(fixed.source, expanded.file, contents) else {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
writeToStandardError("error: could not write '\(expanded.file)': \(error)\n")
|
|
88
|
+
failedFiles += 1
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
writeToStandardOutput(renderAppliedFixIts(fixed, file: expanded.file))
|
|
93
|
+
appliedCount += fixed.applied.count
|
|
94
|
+
fixedFiles += 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
writeToStandardOutput(
|
|
98
|
+
renderFixSummary(applied: appliedCount, files: fixedFiles, failed: failedFiles, dryRun: options.dryRun)
|
|
99
|
+
)
|
|
100
|
+
return failedFiles > 0 ? 1 : 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Why `writeFixedSource` didn't write a file.
|
|
105
|
+
struct FixWriteError: Error, CustomStringConvertible {
|
|
106
|
+
let description: String
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Writes `source`, the fixed source of the file at `path`, whose contents were `original`, and
|
|
110
|
+
/// returns whether it changed the file.
|
|
111
|
+
///
|
|
112
|
+
/// The file must still have the contents that `fix` read, so a change that an editor saved meanwhile
|
|
113
|
+
/// isn't lost. A file that already has the fixed contents, because `fix` wrote it through another
|
|
114
|
+
/// hard link, is left as it is. It keeps the UTF-8 byte order mark of the file, which `readSource`
|
|
115
|
+
/// removes from the source.
|
|
116
|
+
///
|
|
117
|
+
/// The file is written in place, so on macOS and Linux its links, permissions, owner, extended
|
|
118
|
+
/// attributes and ACLs stay, and a read-only file isn't written. When the write fails, the original contents are written back,
|
|
119
|
+
/// and the error tells whether the file is unchanged. When that fails too, the original contents are
|
|
120
|
+
/// saved to a file in the temporary directory, which the error names.
|
|
121
|
+
@discardableResult
|
|
122
|
+
func writeFixedSource(_ source: String, toFile path: String, original: Data) throws -> Bool {
|
|
123
|
+
let url = URL(fileURLWithPath: path)
|
|
124
|
+
let contents = Data((original.starts(with: utf8ByteOrderMark) ? utf8ByteOrderMark : []) + Array(source.utf8))
|
|
125
|
+
guard let current = try? Data(contentsOf: url) else {
|
|
126
|
+
throw FixWriteError(description: "the file can't be read anymore")
|
|
127
|
+
}
|
|
128
|
+
guard current != contents else {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
guard current == original else {
|
|
132
|
+
throw FixWriteError(description: "the file changed after fix read it")
|
|
133
|
+
}
|
|
134
|
+
do {
|
|
135
|
+
try contents.write(to: url)
|
|
136
|
+
} catch {
|
|
137
|
+
if (try? Data(contentsOf: url)) != original {
|
|
138
|
+
try? original.write(to: url)
|
|
139
|
+
}
|
|
140
|
+
if (try? Data(contentsOf: url)) == original {
|
|
141
|
+
throw FixWriteError(description: "\(error); the file is unchanged")
|
|
142
|
+
}
|
|
143
|
+
let backup = FileManager.default.temporaryDirectory.appendingPathComponent(
|
|
144
|
+
"\(url.lastPathComponent).\(UUID().uuidString).original"
|
|
145
|
+
)
|
|
146
|
+
let saved = (try? original.write(to: backup)) != nil
|
|
147
|
+
throw FixWriteError(
|
|
148
|
+
description: "\(error); the file can be incomplete"
|
|
149
|
+
+ (saved ? ", and its original contents are in '\(backup.path)'" : "")
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The most times that `fixSource` expands a file again after applying Fix-Its.
|
|
156
|
+
let maxFixPasses = 10
|
|
157
|
+
|
|
158
|
+
/// Applies the Fix-Its of `expanded`, the expansion of `source`, to `source`. A macro can stop at its
|
|
159
|
+
/// first error, so after applying Fix-Its, this expands the result again and applies the new Fix-Its,
|
|
160
|
+
/// until none applies or after `maxFixPasses` passes. The location of an applied Fix-It is in the
|
|
161
|
+
/// source of its pass. The skipped Fix-Its are those of the last pass. With `onlyErrors`, the Fix-Its
|
|
162
|
+
/// of warnings and other diagnostics that aren't errors are left out.
|
|
163
|
+
func fixSource(
|
|
164
|
+
_ source: String,
|
|
165
|
+
expanded: ExpandedFile,
|
|
166
|
+
symbols: [String],
|
|
167
|
+
onlyErrors: Bool = false,
|
|
168
|
+
expander: MacroExpander
|
|
169
|
+
) -> FixedSource {
|
|
170
|
+
let selected = { (diagnostics: [ExpansionDiagnostic]) in
|
|
171
|
+
onlyErrors ? diagnostics.filter { $0.severity == .error } : diagnostics
|
|
172
|
+
}
|
|
173
|
+
var fixed = applyFixIts(to: source, diagnostics: selected(expanded.diagnostics))
|
|
174
|
+
var applied = fixed.applied
|
|
175
|
+
for _ in 1..<maxFixPasses where !fixed.applied.isEmpty {
|
|
176
|
+
let diagnostics = expander.expand(fixed.source, expanded.file, symbols).diagnostics
|
|
177
|
+
fixed = applyFixIts(to: fixed.source, diagnostics: selected(diagnostics))
|
|
178
|
+
applied += fixed.applied
|
|
179
|
+
}
|
|
180
|
+
// After the last pass, expand once more to tell whether Fix-Its are left.
|
|
181
|
+
let reachedPassLimit =
|
|
182
|
+
!fixed.applied.isEmpty
|
|
183
|
+
&& !applyFixIts(
|
|
184
|
+
to: fixed.source,
|
|
185
|
+
diagnostics: selected(expander.expand(fixed.source, expanded.file, symbols).diagnostics)
|
|
186
|
+
).applied.isEmpty
|
|
187
|
+
return FixedSource(
|
|
188
|
+
source: fixed.source,
|
|
189
|
+
fixIts: applied + fixed.fixIts.filter { $0.skipReason != nil },
|
|
190
|
+
reachedPassLimit: reachedPassLimit
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Applies the Fix-Its of `diagnostics` to `source`, which is the file that they were reported for.
|
|
195
|
+
///
|
|
196
|
+
/// A diagnostic with more than one Fix-It is skipped, because they are alternatives. So is a Fix-It
|
|
197
|
+
/// whose edits aren't in the file, or whose edits overlap the edits of a Fix-It before it. A Fix-It
|
|
198
|
+
/// with the same edits as one before it, which a macro can report twice, is left out. A diagnostic
|
|
199
|
+
/// without `fixItEdits`, such as a syntax error, is left out too.
|
|
200
|
+
func applyFixIts(to source: String, diagnostics: [ExpansionDiagnostic]) -> FixedSource {
|
|
201
|
+
let text = Array(source.utf8)
|
|
202
|
+
var accepted: [ExpansionEdit] = []
|
|
203
|
+
var reports: [FixItReport] = []
|
|
204
|
+
|
|
205
|
+
for diagnostic in diagnostics
|
|
206
|
+
where !diagnostic.fixIts.isEmpty && diagnostic.fixItEdits.count == diagnostic.fixIts.count {
|
|
207
|
+
let report = { (message: String, reason: String?) in
|
|
208
|
+
reports.append(
|
|
209
|
+
FixItReport(line: diagnostic.line, column: diagnostic.column, message: message, skipReason: reason)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
guard diagnostic.fixIts.count == 1, let message = diagnostic.fixIts.first, let edits = diagnostic.fixItEdits.first
|
|
213
|
+
else {
|
|
214
|
+
for message in diagnostic.fixIts {
|
|
215
|
+
report(message, "the diagnostic has \(diagnostic.fixIts.count) Fix-Its to choose from")
|
|
216
|
+
}
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
if !edits.isEmpty && edits.allSatisfy(accepted.contains) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
guard !edits.isEmpty, edits.allSatisfy({ matches($0, in: text) }) else {
|
|
223
|
+
report(message, "its edits aren't in the file")
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
let overlaps = edits.enumerated().contains { index, edit in
|
|
227
|
+
(accepted + edits.prefix(index)).contains { overlap(edit, $0) }
|
|
228
|
+
}
|
|
229
|
+
guard !overlaps else {
|
|
230
|
+
report(message, "it overlaps another Fix-It")
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
accepted += edits
|
|
234
|
+
report(message, nil)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var fixed = text
|
|
238
|
+
for edit in accepted.sorted(by: { $0.offset > $1.offset }) {
|
|
239
|
+
fixed.replaceSubrange(edit.offset..<(edit.offset + edit.original.utf8.count), with: Array(edit.replacement.utf8))
|
|
240
|
+
}
|
|
241
|
+
return FixedSource(source: String(decoding: fixed, as: UTF8.self), fixIts: reports)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Whether `text` has the original text of `edit` at its offset.
|
|
245
|
+
private func matches(_ edit: ExpansionEdit, in text: [UInt8]) -> Bool {
|
|
246
|
+
let original = Array(edit.original.utf8)
|
|
247
|
+
return edit.offset >= 0 && edit.offset + original.count <= text.count
|
|
248
|
+
&& text[edit.offset..<(edit.offset + original.count)].elementsEqual(original)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Whether two edits change the same bytes, or insert at the same offset, where their order would
|
|
252
|
+
/// matter.
|
|
253
|
+
private func overlap(_ a: ExpansionEdit, _ b: ExpansionEdit) -> Bool {
|
|
254
|
+
let aEnd = a.offset + a.original.utf8.count
|
|
255
|
+
let bEnd = b.offset + b.original.utf8.count
|
|
256
|
+
return a.offset == b.offset || (a.offset < bEnd && b.offset < aEnd)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// The applied Fix-Its of `fixed`, one line each.
|
|
260
|
+
func renderAppliedFixIts(_ fixed: FixedSource, file: String) -> String {
|
|
261
|
+
fixed.applied.map { "\(file):\($0.line):\($0.column): fix-it: \($0.message)\n" }.joined()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// The warnings for the skipped Fix-Its of `fixed`, and for reaching the pass limit.
|
|
265
|
+
func renderSkippedFixIts(_ fixed: FixedSource, file: String) -> String {
|
|
266
|
+
var text = ""
|
|
267
|
+
for fixIt in fixed.fixIts {
|
|
268
|
+
if let reason = fixIt.skipReason {
|
|
269
|
+
text += "\(file):\(fixIt.line):\(fixIt.column): warning: skipped fix-it '\(fixIt.message)': \(reason)\n"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if fixed.reachedPassLimit {
|
|
273
|
+
text += "\(file): warning: stopped after \(maxFixPasses) passes; run fix again for the Fix-Its left\n"
|
|
274
|
+
}
|
|
275
|
+
return text
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// The summary line of `fix`, for example `Applied 3 Fix-Its in 2 files.` or, when files couldn't be
|
|
279
|
+
/// written, `Applied 1 Fix-It in 1 file. Could not write 1 file.`
|
|
280
|
+
func renderFixSummary(applied: Int, files: Int, failed: Int = 0, dryRun: Bool) -> String {
|
|
281
|
+
let count = { (n: Int, noun: String) in "\(n) \(noun)\(n == 1 ? "" : "s")" }
|
|
282
|
+
var parts: [String] = []
|
|
283
|
+
if applied > 0 {
|
|
284
|
+
parts.append("\(dryRun ? "Would apply" : "Applied") \(count(applied, "Fix-It")) in \(count(files, "file")).")
|
|
285
|
+
}
|
|
286
|
+
if failed > 0 {
|
|
287
|
+
parts.append("Could not write \(count(failed, "file")).")
|
|
288
|
+
}
|
|
289
|
+
return (parts.isEmpty ? "No Fix-Its to apply." : parts.joined(separator: " ")) + "\n"
|
|
290
|
+
}
|
|
@@ -207,8 +207,8 @@ struct ExportedEvent: Encodable, Equatable {
|
|
|
207
207
|
/// The single payload parameter's type, or `nil` for a no-payload `() -> Void` event.
|
|
208
208
|
var payload: TypeNode?
|
|
209
209
|
|
|
210
|
-
///
|
|
211
|
-
let
|
|
210
|
+
/// How the event is dispatched: the default `scheduled`, or the `.inline` or `.blocking` option.
|
|
211
|
+
let dispatch: ExportedEventDispatch
|
|
212
212
|
|
|
213
213
|
/// The spelled access modifier, or `nil` when none is written.
|
|
214
214
|
let accessLevel: String?
|
|
@@ -216,8 +216,7 @@ struct ExportedEvent: Encodable, Equatable {
|
|
|
216
216
|
let platforms: [ExportedPlatform]
|
|
217
217
|
|
|
218
218
|
private enum CodingKeys: String, CodingKey {
|
|
219
|
-
case name, jsName, payload, accessLevel, platforms
|
|
220
|
-
case isSync = "sync"
|
|
219
|
+
case name, jsName, payload, dispatch, accessLevel, platforms
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
func encode(to encoder: Encoder) throws {
|
|
@@ -225,7 +224,7 @@ struct ExportedEvent: Encodable, Equatable {
|
|
|
225
224
|
try container.encode(name, forKey: .name)
|
|
226
225
|
try container.encode(jsName, forKey: .jsName)
|
|
227
226
|
try container.encodeIfPresent(payload, forKey: .payload)
|
|
228
|
-
try container.encode(
|
|
227
|
+
try container.encode(dispatch, forKey: .dispatch)
|
|
229
228
|
try container.encodeIfPresent(accessLevel, forKey: .accessLevel)
|
|
230
229
|
try container.encode(platforms, forKey: .platforms)
|
|
231
230
|
}
|
|
@@ -471,6 +470,13 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
471
470
|
let unions: [ExportedUnion]
|
|
472
471
|
}
|
|
473
472
|
|
|
473
|
+
/// How an `@Event` is dispatched to JavaScript, from its option. Mirrors `EventDispatch` in the macros.
|
|
474
|
+
enum ExportedEventDispatch: String, Encodable, Equatable {
|
|
475
|
+
case scheduled
|
|
476
|
+
case inline
|
|
477
|
+
case blocking
|
|
478
|
+
}
|
|
479
|
+
|
|
474
480
|
/// Version of the `scan-exports` output shape. Bumped on any breaking change to the envelope or to
|
|
475
481
|
/// anything under `exports`, so a consumer can verify it understands the output before trusting it.
|
|
476
482
|
/// Versioned independently of `scanModulesSchemaVersion`: the two commands serve different consumers
|
|
@@ -481,8 +487,9 @@ struct ExportedSurface: Encodable, Equatable {
|
|
|
481
487
|
/// declarations in `#if` blocks, adds `platforms` (with deprecations), `accessLevel`, and
|
|
482
488
|
/// `defaultValue`, and replaces `constructorParameters` with `initializer`; version 9 adds the
|
|
483
489
|
/// `sharedRef` type kind and the `sharedRef` of a shared object; version 10 adds the `tuple` and `object` type kinds (named after a tuple alias)
|
|
484
|
-
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes
|
|
485
|
-
|
|
490
|
+
/// and resolves the tuple type aliases declared in `@ExpoModule` and `@SharedObject` classes; version 11
|
|
491
|
+
/// replaces the `sync` flag of an event with `dispatch`.
|
|
492
|
+
let scanExportsSchemaVersion = 11
|
|
486
493
|
|
|
487
494
|
/// The `scan-exports` result: the surface plus the run's stats. A distinct envelope from
|
|
488
495
|
/// `ScanModulesResult` (different consumer: TS generation vs. autolinking).
|
|
@@ -285,7 +285,9 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
285
285
|
return []
|
|
286
286
|
}
|
|
287
287
|
let override = stringArgument(of: attribute)
|
|
288
|
-
let
|
|
288
|
+
let dispatch: ExportedEventDispatch =
|
|
289
|
+
hasEventOption(attribute, named: "inline")
|
|
290
|
+
? .inline : hasEventOption(attribute, named: "blocking") ? .blocking : .scheduled
|
|
289
291
|
var result: [ExportedEvent] = []
|
|
290
292
|
|
|
291
293
|
for binding in varDecl.bindings {
|
|
@@ -306,7 +308,7 @@ final class SurfaceVisitor: SyntaxVisitor {
|
|
|
306
308
|
name: name,
|
|
307
309
|
jsName: override ?? defaultEventName(for: name),
|
|
308
310
|
payload: functionType.parameters.first.map { typeNode(from: $0.type) },
|
|
309
|
-
|
|
311
|
+
dispatch: dispatch,
|
|
310
312
|
accessLevel: spelledAccessLevel(of: varDecl.modifiers),
|
|
311
313
|
platforms: platforms
|
|
312
314
|
)
|
|
@@ -704,18 +706,16 @@ private func isVoidEventReturn(_ type: TypeSyntax) -> Bool {
|
|
|
704
706
|
return text == "Void" || text == "()" || text == "Swift.Void"
|
|
705
707
|
}
|
|
706
708
|
|
|
707
|
-
///
|
|
708
|
-
|
|
709
|
+
/// True if an `@Event` attribute lists the given option member (`@Event(.inline)` or
|
|
710
|
+
/// `@Event("name", EventOptions.inline)`). Like `hasOption` in the macros, but without the array-literal
|
|
711
|
+
/// branch, which `EventOptions...` doesn't accept.
|
|
712
|
+
private func hasEventOption(_ attribute: AttributeSyntax, named option: String) -> Bool {
|
|
709
713
|
guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else {
|
|
710
|
-
return
|
|
714
|
+
return false
|
|
711
715
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return nil
|
|
715
|
-
}
|
|
716
|
-
return literal.literal.tokenKind == .keyword(.true)
|
|
716
|
+
return arguments.contains { argument in
|
|
717
|
+
argument.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text == option
|
|
717
718
|
}
|
|
718
|
-
return nil
|
|
719
719
|
}
|
|
720
720
|
|
|
721
721
|
/// The name in a `@JS` attribute's `.withAsync("name")` option, in the arguments or inside an array
|
|
@@ -34,19 +34,19 @@ public enum Scanner {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/// One
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
/// per-member detail instead.
|
|
43
|
-
struct
|
|
44
|
-
/// The Swift class name
|
|
37
|
+
/// One class in the `scan-modules` output: an `@ExpoModule` class in `modules`, or a `@SharedObject`
|
|
38
|
+
/// class in `sharedObjects`. Trimmed to what `expo-modules-autolinking` needs to register the class:
|
|
39
|
+
/// the Swift class name, the JS name it registers under, the platforms that include it, and the file
|
|
40
|
+
/// it's in. The richer fields the visitor captures (declaration kind, raw macro arguments,
|
|
41
|
+
/// line/column) are dropped here, because this command doesn't need them (the macro is always on a
|
|
42
|
+
/// class) and the deep `scan-exports` surface carries the richer per-member detail instead.
|
|
43
|
+
struct ScannedClass: Codable, Equatable {
|
|
44
|
+
/// The Swift class name.
|
|
45
45
|
let name: String
|
|
46
46
|
|
|
47
|
-
/// The fully-resolved JS
|
|
48
|
-
/// class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
|
-
/// and the consumer never has to apply the fallback itself.
|
|
47
|
+
/// The fully-resolved JS name: the `@ExpoModule("Foo")` or `@SharedObject("Foo")` override when
|
|
48
|
+
/// present, otherwise the class name. Resolved here (rather than left `nil`) so it matches how the
|
|
49
|
+
/// macro derives the name and the consumer never has to apply the fallback itself.
|
|
50
50
|
let jsName: String
|
|
51
51
|
|
|
52
52
|
/// The class's spelled access modifier (`open`, `public`, `package`, `fileprivate`, `private`),
|
|
@@ -56,20 +56,21 @@ struct ScannedModule: Codable, Equatable {
|
|
|
56
56
|
let accessLevel: String
|
|
57
57
|
|
|
58
58
|
/// The Apple OSes whose builds include this class, spelled as `os(...)` spells them and compared
|
|
59
|
-
/// case-sensitively. An unconditional
|
|
59
|
+
/// case-sensitively. An unconditional class lists every OS. Empty means no build is known to
|
|
60
60
|
/// include it, because the enclosing conditions depend on flags not asserted with `--define` or
|
|
61
61
|
/// on conditions a static scan cannot answer (those are reported in `warnings`). The consumer
|
|
62
62
|
/// must not assume such a class exists.
|
|
63
63
|
let platforms: [String]
|
|
64
64
|
|
|
65
|
-
/// Source file the
|
|
65
|
+
/// Source file the class was found in, relative to the path the scanner was invoked with.
|
|
66
66
|
let file: String
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/// Version of the `scan-modules` output shape. Bumped on any breaking change to the envelope or to
|
|
70
|
-
/// `
|
|
70
|
+
/// `ScannedClass`, so `expo-modules-autolinking` can verify it understands the output before
|
|
71
71
|
/// trusting it (and fall back to config-declared modules when it doesn't). Version 2 added the
|
|
72
|
-
/// per-module `platforms` list and made the module list platform-agnostic.
|
|
72
|
+
/// per-module `platforms` list and made the module list platform-agnostic. A new field that older
|
|
73
|
+
/// consumers can ignore, such as `sharedObjects`, doesn't change the version.
|
|
73
74
|
let scanModulesSchemaVersion = 2
|
|
74
75
|
|
|
75
76
|
/// The `scan-modules` result: the detected modules plus the stats describing the run. Encoded as the
|
|
@@ -77,7 +78,11 @@ let scanModulesSchemaVersion = 2
|
|
|
77
78
|
/// serve different consumers and don't share an envelope.)
|
|
78
79
|
struct ScanModulesResult: Codable, Equatable {
|
|
79
80
|
let schemaVersion: Int
|
|
80
|
-
let modules: [
|
|
81
|
+
let modules: [ScannedClass]
|
|
82
|
+
|
|
83
|
+
/// The top-level `@SharedObject` classes, in the same order and with the same platform rules as
|
|
84
|
+
/// `modules`.
|
|
85
|
+
let sharedObjects: [ScannedClass]
|
|
81
86
|
|
|
82
87
|
/// Warnings for `#if` conditions the scan couldn't answer statically (see `ScanWarning`). Carried
|
|
83
88
|
/// in the report rather than on stderr so the consumer can attach them to its own output.
|
|
@@ -97,25 +102,28 @@ private let platformUniverse: [(condition: String, reported: String)] = [
|
|
|
97
102
|
(condition: "visionOS", reported: "visionOS"),
|
|
98
103
|
]
|
|
99
104
|
|
|
100
|
-
/// Scans the given paths for top-level `@ExpoModule` types and returns every
|
|
105
|
+
/// Scans the given paths for top-level `@ExpoModule` and `@SharedObject` types and returns every one
|
|
106
|
+
/// found in any
|
|
101
107
|
/// `#if` branch (in file then source order), each with the platforms whose builds include it, plus
|
|
102
108
|
/// the stats for the run — the `scan-modules` command. Kept separate from the public entry (and
|
|
103
109
|
/// `internal`) so tests can drive it without going through argv/stdout.
|
|
104
110
|
///
|
|
105
111
|
/// Each file is parsed once and walked once per platform (plus once unconditionally to enumerate
|
|
106
|
-
/// every
|
|
112
|
+
/// every type): a type's `platforms` are the OSes whose evaluated walk reached it, given the
|
|
107
113
|
/// asserted `defines`. The scanner reports the facts; filtering to the platform being linked is the
|
|
108
114
|
/// consumer's call.
|
|
109
115
|
func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResult {
|
|
110
|
-
var modules: [
|
|
116
|
+
var modules: [ScannedClass] = []
|
|
117
|
+
var sharedObjects: [ScannedClass] = []
|
|
111
118
|
var warnings: [ScanWarning] = []
|
|
112
119
|
var seenWarnings = Set<ScanWarning>()
|
|
113
120
|
|
|
114
|
-
let
|
|
121
|
+
let macros: Set<DetectedMacro> = [.expoModule, .sharedObject]
|
|
122
|
+
let stats = scanFiles(paths: paths, macros: macros) { source, file in
|
|
115
123
|
let tree = Parser.parse(source: source)
|
|
116
124
|
|
|
117
|
-
// The unconditional walk enumerates every
|
|
118
|
-
let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros:
|
|
125
|
+
// The unconditional walk enumerates every type in the file, in source order.
|
|
126
|
+
let allModules = DetectionVisitor(file: file, tree: tree, detectedMacros: macros, configuration: nil)
|
|
119
127
|
allModules.walk(tree)
|
|
120
128
|
|
|
121
129
|
// One evaluated walk per OS attributes each module to the platforms that include it. The walks
|
|
@@ -126,7 +134,7 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
126
134
|
let visitor = DetectionVisitor(
|
|
127
135
|
file: file,
|
|
128
136
|
tree: tree,
|
|
129
|
-
detectedMacros:
|
|
137
|
+
detectedMacros: macros,
|
|
130
138
|
configuration: configuration
|
|
131
139
|
)
|
|
132
140
|
visitor.walk(tree)
|
|
@@ -140,24 +148,30 @@ func scanModules(paths: [String], defines: Set<String> = []) -> ScanModulesResul
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// the
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
151
|
+
// The macros reject anything but a class, so a struct with one of them is never registered.
|
|
152
|
+
for detection in allModules.detections where detection.declarationKind == "class" {
|
|
153
|
+
// Resolve the JS name the way the macro does: explicit `@ExpoModule("Foo")` or
|
|
154
|
+
// `@SharedObject("Foo")` override, else the class name.
|
|
155
|
+
let scanned = ScannedClass(
|
|
156
|
+
name: detection.name,
|
|
157
|
+
jsName: detection.jsName ?? detection.name,
|
|
158
|
+
accessLevel: detection.accessLevel,
|
|
159
|
+
platforms: platformsByDetection[detectionKey(detection)] ?? [],
|
|
160
|
+
file: detection.file
|
|
154
161
|
)
|
|
162
|
+
switch detection.macro {
|
|
163
|
+
case .sharedObject:
|
|
164
|
+
sharedObjects.append(scanned)
|
|
165
|
+
default:
|
|
166
|
+
modules.append(scanned)
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
return ScanModulesResult(
|
|
159
172
|
schemaVersion: scanModulesSchemaVersion,
|
|
160
173
|
modules: modules,
|
|
174
|
+
sharedObjects: sharedObjects,
|
|
161
175
|
warnings: warnings,
|
|
162
176
|
stats: stats
|
|
163
177
|
)
|