expo-modules-macros 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{swift.yml → macos.yml} +12 -3
- package/AGENTS.md +2 -1
- package/README.md +9 -7
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +3 -2
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/MacroExpander.swift +87 -38
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +27 -9
- package/apple/Sources/ExpoModulesCLI/Commands/Fix/RunFix.swift +88 -26
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/ExportedSurface.swift +14 -7
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/SurfaceVisitor.swift +6 -4
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +104 -17
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +13 -4
- package/apple/Sources/ExpoModulesMacrosCore/Macros/JSMacro.swift +124 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/RecordMacro.swift +68 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/SharedObjectMacro.swift +80 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +609 -33
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +15 -16
- package/build/types.d.ts +3 -3
- package/build/types.js +1 -1
- package/package.json +5 -5
|
@@ -35,7 +35,9 @@ import SwiftSyntaxMacros
|
|
|
35
35
|
/// Discrimination is structural and order-dependent: the first case whose payload decodes wins. When
|
|
36
36
|
/// two payload shapes overlap (two records with compatible fields, `Int` and `Double`), the earlier
|
|
37
37
|
/// case matches; the author orders the more specific case first. A case may not repeat another case's
|
|
38
|
-
/// payload type, since it could never be chosen.
|
|
38
|
+
/// payload type, since it could never be chosen. A case after a `JavaScriptValue` case, or with a
|
|
39
|
+
/// number-only payload after a `Double`, `Float` or `CGFloat` case, can't be chosen either, so the macro
|
|
40
|
+
/// warns about it.
|
|
39
41
|
///
|
|
40
42
|
/// The named-union counterpart of core's `Either`: any number of named cases instead of two anonymous
|
|
41
43
|
/// slots, no `Any?` box, and an exhaustive `switch` as the primary way to read it (`as(_:)` covers the
|
|
@@ -48,6 +50,7 @@ public struct UnionMacro: MemberMacro, ExtensionMacro {
|
|
|
48
50
|
in context: some MacroExpansionContext
|
|
49
51
|
) throws -> [DeclSyntax] {
|
|
50
52
|
let union = try validatedUnion(of: declaration)
|
|
53
|
+
diagnoseUnreachableCases(of: declaration, in: context)
|
|
51
54
|
|
|
52
55
|
var members: [DeclSyntax] = []
|
|
53
56
|
|
|
@@ -223,8 +226,8 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
223
226
|
let payloadType = parameter.type.trimmedDescription
|
|
224
227
|
// Alternatives decode in declaration order and the first success wins, so a case whose payload
|
|
225
228
|
// type repeats an earlier case's is unreachable: an exact-spelling duplicate is an error. (An
|
|
226
|
-
// overlap between *different* types, like `Int` and `Double`, is the author's ordering call
|
|
227
|
-
//
|
|
229
|
+
// overlap between *different* types, like `Int` and `Double`, is the author's ordering call;
|
|
230
|
+
// `diagnoseUnreachableCases` warns about the overlaps that make a case unreachable.)
|
|
228
231
|
let normalizedType = payloadType.filter { !$0.isWhitespace }
|
|
229
232
|
if let earlierCase = seenPayloadTypes[normalizedType] {
|
|
230
233
|
diagnostics.append(
|
|
@@ -256,6 +259,88 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
256
259
|
return UnionType(name: enumDecl.name.text, cases: cases)
|
|
257
260
|
}
|
|
258
261
|
|
|
262
|
+
/// The payload types whose decode accepts every JavaScript number: they convert the number with
|
|
263
|
+
/// `asDouble()`, so they never fail on one.
|
|
264
|
+
private let anyNumberPayloadTypes: Set<String> = ["Double", "Float", "CGFloat"]
|
|
265
|
+
|
|
266
|
+
/// The payload types that decode only a JavaScript number, so a case before them with a type of
|
|
267
|
+
/// `anyNumberPayloadTypes` decodes every value they could. `Int`, `Int64`, `UInt` and `UInt64` aren't
|
|
268
|
+
/// listed, since they also decode a `bigint`.
|
|
269
|
+
private let numberOnlyPayloadTypes: Set<String> = [
|
|
270
|
+
"Double", "Float", "CGFloat", "Int8", "Int16", "Int32", "UInt8", "UInt16", "UInt32",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
/// Warns about each case that an earlier case always decodes first, so it is never chosen. Cases are
|
|
274
|
+
/// tried in declaration order and the first that decodes wins. The macro knows that only for some
|
|
275
|
+
/// payload types: a `JavaScriptValue` payload decodes every value, and a floating-point payload every
|
|
276
|
+
/// number, so a later case that decodes only numbers can't be reached. Types with the same spelling
|
|
277
|
+
/// are reported as an error by `validatedUnion`, which runs first.
|
|
278
|
+
private func diagnoseUnreachableCases(of declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) {
|
|
279
|
+
var catchAllCase: (name: TokenSyntax, type: String)?
|
|
280
|
+
var anyNumberCase: (name: TokenSyntax, type: String)?
|
|
281
|
+
|
|
282
|
+
for member in declaration.memberBlock.members {
|
|
283
|
+
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
for element in caseDecl.elements {
|
|
287
|
+
guard let parameter = element.parameterClause?.parameters.first else {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
let type = unqualifiedPayloadTypeName(parameter.type)
|
|
291
|
+
let reason: String?
|
|
292
|
+
let earlier: (name: TokenSyntax, type: String)?
|
|
293
|
+
if let catchAllCase {
|
|
294
|
+
earlier = catchAllCase
|
|
295
|
+
reason = "'\(catchAllCase.type)' decodes every JavaScript value"
|
|
296
|
+
} else if let anyNumberCase, numberOnlyPayloadTypes.contains(type) {
|
|
297
|
+
earlier = anyNumberCase
|
|
298
|
+
reason = "'\(anyNumberCase.type)' decodes every JavaScript number, and '\(type)' decodes only numbers"
|
|
299
|
+
} else {
|
|
300
|
+
earlier = nil
|
|
301
|
+
reason = nil
|
|
302
|
+
}
|
|
303
|
+
if let earlier, let reason {
|
|
304
|
+
let name = element.name.text
|
|
305
|
+
context.diagnose(
|
|
306
|
+
Diagnostic(
|
|
307
|
+
node: element,
|
|
308
|
+
message: UnionDiagnosticMessage(
|
|
309
|
+
"@Union case '\(name)' is never decoded: case '\(earlier.name.text)' is tried first, and \(reason). Move case '\(name)' before case '\(earlier.name.text)'.",
|
|
310
|
+
id: "union-unreachable-case",
|
|
311
|
+
severity: .warning
|
|
312
|
+
),
|
|
313
|
+
notes: [
|
|
314
|
+
Note(
|
|
315
|
+
node: Syntax(earlier.name),
|
|
316
|
+
message: UnionNoteMessage(
|
|
317
|
+
"case '\(earlier.name.text)' is declared here",
|
|
318
|
+
id: "union-unreachable-case-earlier"
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
]
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
if type == "JavaScriptValue" {
|
|
327
|
+
catchAllCase = (element.name, type)
|
|
328
|
+
} else if anyNumberCase == nil, anyNumberPayloadTypes.contains(type) {
|
|
329
|
+
anyNumberCase = (element.name, type)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// The name of a payload type without the module that it may be qualified with, such as `Swift.Double`.
|
|
336
|
+
private func unqualifiedPayloadTypeName(_ type: TypeSyntax) -> String {
|
|
337
|
+
let modules = ["Swift", "CoreGraphics", "CoreFoundation", "Foundation", "ExpoModulesCore", "ExpoModulesJSI"]
|
|
338
|
+
if let memberType = type.as(MemberTypeSyntax.self), modules.contains(memberType.baseType.trimmedDescription) {
|
|
339
|
+
return memberType.name.text
|
|
340
|
+
}
|
|
341
|
+
return type.trimmedDescription
|
|
342
|
+
}
|
|
343
|
+
|
|
259
344
|
// MARK: - Synthesized members
|
|
260
345
|
|
|
261
346
|
/// `decode(_:in:)`: tries each case's payload converter in declaration order and returns the first
|
|
@@ -429,11 +514,22 @@ private func defaultValueDiagnostic(
|
|
|
429
514
|
private struct UnionDiagnosticMessage: DiagnosticMessage {
|
|
430
515
|
let message: String
|
|
431
516
|
let diagnosticID: MessageID
|
|
432
|
-
let severity: DiagnosticSeverity
|
|
517
|
+
let severity: DiagnosticSeverity
|
|
433
518
|
|
|
434
|
-
init(_ message: String, id: String) {
|
|
519
|
+
init(_ message: String, id: String, severity: DiagnosticSeverity = .error) {
|
|
435
520
|
self.message = message
|
|
436
521
|
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
522
|
+
self.severity = severity
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private struct UnionNoteMessage: NoteMessage {
|
|
527
|
+
let message: String
|
|
528
|
+
let noteID: MessageID
|
|
529
|
+
|
|
530
|
+
init(_ message: String, id: String) {
|
|
531
|
+
self.message = message
|
|
532
|
+
self.noteID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
437
533
|
}
|
|
438
534
|
}
|
|
439
535
|
|