expo-modules-macros 0.31.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 +5 -5
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacrosCore/Macros/EventMacro.swift +12 -2
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ExpoModuleMacro.swift +12 -0
- 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 +79 -1
- package/apple/Sources/ExpoModulesMacrosCore/Macros/UnionMacro.swift +101 -5
- package/apple/Sources/ExpoModulesMacrosCore/Macros/ViewPropsMacro.swift +585 -23
- package/apple/Sources/ExpoModulesMacrosCore/Support/JSNameCollisions.swift +111 -2
- package/apple/Sources/ExpoModulesMacrosCore/Support/MacroHelpers.swift +13 -0
- package/package.json +5 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: macOS
|
|
2
|
+
|
|
3
|
+
# Builds and tests the package on macOS, and checks the universal release tool there.
|
|
2
4
|
|
|
3
5
|
on:
|
|
4
6
|
push:
|
|
@@ -12,8 +14,8 @@ concurrency:
|
|
|
12
14
|
cancel-in-progress: true
|
|
13
15
|
|
|
14
16
|
jobs:
|
|
15
|
-
|
|
16
|
-
name: Build & test
|
|
17
|
+
macos:
|
|
18
|
+
name: Build & test (macOS)
|
|
17
19
|
# The package declares swift-tools-version 6.2, which requires Xcode 26+.
|
|
18
20
|
runs-on: macos-26
|
|
19
21
|
steps:
|
|
@@ -63,3 +65,10 @@ jobs:
|
|
|
63
65
|
run: |
|
|
64
66
|
pnpm install
|
|
65
67
|
pnpm run typecheck
|
|
68
|
+
|
|
69
|
+
- name: Scan through the TypeScript wrapper
|
|
70
|
+
# The wrapper must find the release tool in `apple/`, and `scanExports` must report the
|
|
71
|
+
# surface in `scripts/check-scan-exports.json`, which the Windows and Linux workflows also check.
|
|
72
|
+
run: |
|
|
73
|
+
pnpm run build:ts
|
|
74
|
+
node scripts/check-scan.js .
|
package/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ pnpm run swift:format # format the Swift sources in place
|
|
|
31
31
|
pnpm run swift:lint # check the formatting, as CI does
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
CI runs on macOS (`.github/workflows/
|
|
34
|
+
CI runs on macOS (`.github/workflows/macos.yml`), on Windows x64 and arm64 (`.github/workflows/windows.yml`), and on Linux x64 and arm64 (`.github/workflows/linux.yml`). On macOS it runs the release build, checks both binary slices, runs `swift test` and `pnpm run typecheck`, then scans through the TypeScript wrapper with `scripts/check-scan.js`. On Windows it builds and tests the package, builds the release binary, checks that it starts without the Swift runtime, packs it into its `platforms/` package (checking the files and the `os`/`cpu` fields), installs that package, and scans through the TypeScript wrapper with `scripts/check-scan.js`, which must find the executable there. On Linux it does the same in the `swift:6.4.0-rhel-ubi9` image, and also checks that no Swift library is linked dynamically, the newest glibc symbol version that the binary needs, and that the package keeps the executable bit. On every platform, `check-scan.js` also requires `scanExports` to report the surface in `scripts/check-scan-exports.json` for its fixture, so the generated types are the same on each OS. The Swift Format workflow (`.github/workflows/swift-format.yml`) builds the pinned swift-format version and runs `pnpm run swift:lint`. Code in `Sources` and `Tests` must not assume Apple platforms or POSIX paths.
|
|
35
35
|
|
|
36
36
|
## Windows
|
|
37
37
|
|
|
@@ -57,6 +57,7 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
|
|
|
57
57
|
## Changing macros
|
|
58
58
|
|
|
59
59
|
- The scanner re-implements the macros' naming and member rules on the syntax tree; it does not call the macro code. When a macro changes which members it binds or how it names them (the `@JS` name override, the `on` prefix `@Event` strips, which properties `@Record` treats as fields), change `scan-exports` to match, or generated TypeScript types drift from the runtime.
|
|
60
|
+
- When the `scan-exports` output changes on purpose, update `scripts/check-scan-exports.json`: run `swift build`, then `node scripts/check-scan.js . --update .build/debug/ExpoModulesMacrosTool`, and review the diff. CI compares it with the output on macOS, Windows and Linux.
|
|
60
61
|
- The scanner only parses files that pass the pre-filter, `MacroPrefilter` in `ExpoModulesCLI/Core/SourceScan.swift`. Each command builds it from the `macros` and `conformances` arguments of its `scanFiles` call. A new attribute or conformance the scanner must detect has to be added to those arguments (an attribute also needs a `DetectedMacro` case), or files that use it are skipped without an error.
|
|
61
62
|
- A new macro needs: the implementation in `apple/Sources/ExpoModulesMacrosCore/Macros`, an entry in `ProvidedMacro.all` in `ProvidedMacros.swift` (with the attribute name and the extension conformances of its declaration), an expansion test file, the `#externalMacro` declaration in `expo-modules-core`, and an entry in the macro list in `README.md`.
|
|
62
63
|
|
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ This package lists them as optional dependencies, and their `os`, `cpu` and (on
|
|
|
25
25
|
# Macros
|
|
26
26
|
|
|
27
27
|
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none. It adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet (see [Sendable conformance](#sendable-conformance)).
|
|
28
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels.
|
|
29
|
-
- **`@Event(_ name: String? = nil, _ options: EventOptions...)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. By default the event is scheduled on the JavaScript thread. The `.inline` option runs the listeners before the call returns when it is made on the JavaScript thread, and schedules the event otherwise; it never blocks. The `.blocking` option runs the listeners before the call returns on any thread, so a call from another thread blocks until the JavaScript thread runs them. An event property is callable from any thread with each option.
|
|
30
|
-
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses. A subclass of another `@SharedObject` class also gets the `@JS` members of that class, and its own members replace inherited members with the same JS name. A subclass that isn't `final` can't be constructed from JavaScript through the `@JS init` of its superclass, so give each subclass that JavaScript constructs its own `@JS init`. Like `@ExpoModule`, it adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
|
|
31
|
-
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. A record converts only from and to JavaScript values: its `from(dictionary:appContext:)` throws `RecordDictionaryConversionException`, so a record can't be created from a native `[String: Any]`. A `@Union` can be a record property. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
32
|
-
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first.
|
|
28
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. The class macro reads only the class body, so `@JS` on a member of an extension, or of a type that is neither a module nor a shared object, is an error. So is `@JS` on an initializer or a `static` member of a module, since JavaScript doesn't construct a module and gets only its instance. `@JS` on any other declaration, such as a subscript, is an error too. A `@JS` function or initializer can't be generic, since the binding converts each value by its static type. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. A name that JavaScript already gives a meaning on the object is an error too: `__proto__` on any object, `constructor` on a shared object's prototype, and `prototype` on its constructor. The macro also checks that every type crossing the boundary is convertible in the direction it travels. For a top-level declaration, a bad type is reported on the author's own declaration. In an `@ExpoModule` or `@SharedObject` class, the class macro makes the check, since only it sees the type aliases in the class body, so the error is reported on the class and names the member and its argument labels, such as `local function 'move(_:by:returning:)' requires that 'Point' conform to 'JavaScriptEncodable'`. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. Because of that, a `.concurrent` function and a function with `.withAsync` can't take a JavaScript value such as `JavaScriptValue` or `JavaScriptObject`, which is valid only on the JavaScript thread; the macro reports such a parameter. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol, the error points into the generated code, so mark the function `nonisolated`. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
29
|
+
- **`@Event(_ name: String? = nil, _ options: EventOptions...)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `@ExpoModule` and `@SharedObject` warn when two events in the class body have the same JS name. By default the event is scheduled on the JavaScript thread. The `.inline` option runs the listeners before the call returns when it is made on the JavaScript thread, and schedules the event otherwise; it never blocks. The `.blocking` option runs the listeners before the call returns on any thread, so a call from another thread blocks until the JavaScript thread runs them. An event property is callable from any thread with each option.
|
|
30
|
+
- **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A class without an inheritance clause is an error, with a Fix-It that adds `: SharedObject`. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses. A subclass of another `@SharedObject` class also gets the `@JS` members of that class, and its own members replace inherited members with the same JS name. A subclass that isn't `final` can't be constructed from JavaScript through the `@JS init` of its superclass, so give each subclass that JavaScript constructs its own `@JS init`. Like `@ExpoModule`, it adds an `@unchecked Sendable` conformance when the class isn't `Sendable` yet.
|
|
31
|
+
- **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. A record converts only from and to JavaScript values: its `from(dictionary:appContext:)` throws `RecordDictionaryConversionException`, so a record can't be created from a native `[String: Any]`. A `@Union` can be a record property. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional. The memberwise initializer sets every field, so a field that is a `let` with a default value is an error. It doesn't set a `private` or `fileprivate` stored property, so such a property needs a default value, unless it's an optional `var` or the type declares its own memberwise initializer.
|
|
32
|
+
- **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first. The macro warns about a case that an earlier case always decodes first: any case after a `JavaScriptValue` case, and a case with a payload that decodes only numbers, such as `Float` or `Int32`, after a `Double`, `Float` or `CGFloat` case.
|
|
33
33
|
|
|
34
34
|
The members and conformances that `@Record` and `@Union` generate are `nonisolated`, so they also work in a module that builds with a default isolation, such as `-default-isolation MainActor`. The stored properties of a record still take that isolation. A class record, or a struct record with a property whose type isn't `Sendable`, then needs `nonisolated` on its declaration, for example `@Record nonisolated final class Options`.
|
|
35
35
|
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -29,8 +29,8 @@ import SwiftSyntaxMacros
|
|
|
29
29
|
/// The synthesized property is deliberately **not** isolated to `@JavaScriptActor`, unlike `@JS`
|
|
30
30
|
/// members: `emit` is itself non-isolated and schedules the dispatch onto the JS thread internally,
|
|
31
31
|
/// so the event is callable from any thread or isolation with no actor hop at the call site. It is
|
|
32
|
-
/// also self-contained: `@ExpoModule`/`@SharedObject`
|
|
33
|
-
///
|
|
32
|
+
/// also self-contained: `@ExpoModule`/`@SharedObject` don't register `@Event` members anywhere. They
|
|
33
|
+
/// read the event names only to warn when two events share one (see `diagnoseEventNameCollisions`).
|
|
34
34
|
///
|
|
35
35
|
/// Two options change how the event is dispatched, and each one calls its own `EventEmitter` method,
|
|
36
36
|
/// which is callable from any thread as well:
|
|
@@ -293,6 +293,16 @@ private func isDispatchOption(_ expression: ExprSyntax) -> Bool {
|
|
|
293
293
|
return name == "inline" || name == "blocking"
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/// The JS name of an `@Event` property: the `@Event("…")` override, or the default name derived from
|
|
297
|
+
/// the property name. `nil` when the declaration isn't a single named property, which the macro reports.
|
|
298
|
+
internal func eventJSName(of varDecl: VariableDeclSyntax, attribute: AttributeSyntax) -> String? {
|
|
299
|
+
guard varDecl.bindings.count == 1, let identifier = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)
|
|
300
|
+
else {
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
return jsNameArgument(of: attribute) ?? defaultEventName(for: withoutBackticks(identifier.identifier.text))
|
|
304
|
+
}
|
|
305
|
+
|
|
296
306
|
// MARK: - Default event name
|
|
297
307
|
|
|
298
308
|
/// The default JS event name for a property: the Swift name with the conventional `on` prefix
|
|
@@ -63,6 +63,16 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
63
63
|
for member in classDecl.memberBlock.members {
|
|
64
64
|
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
65
65
|
let decl = aliases.resolving(member.decl)
|
|
66
|
+
// A generic member is reported by `@JS` and left out, so the build has no errors in its binding.
|
|
67
|
+
if memberHasJSAttribute(decl) && jsGenericParameterClause(of: decl) != nil {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
// So is a `static` or `class` member: the module object binds only instance members.
|
|
71
|
+
if memberHasJSAttribute(decl),
|
|
72
|
+
let modifiers = decl.asProtocol(WithModifiersSyntax.self)?.modifiers, isTypeLevel(modifiers)
|
|
73
|
+
{
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
66
76
|
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
67
77
|
assertions.append(assertion)
|
|
68
78
|
}
|
|
@@ -86,6 +96,8 @@ public struct ExpoModuleMacro: MemberMacro {
|
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
diagnoseJSNameCollisions(functions: functions, properties: properties, in: context)
|
|
99
|
+
diagnoseReservedJSNames(functions: functions, properties: properties, on: .module, in: context)
|
|
100
|
+
diagnoseEventNameCollisions(in: classDecl.memberBlock, in: context)
|
|
89
101
|
|
|
90
102
|
var emitted: [DeclSyntax] = []
|
|
91
103
|
|
|
@@ -31,6 +31,20 @@ public struct JSMacro: PeerMacro {
|
|
|
31
31
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
32
32
|
in context: some MacroExpansionContext
|
|
33
33
|
) throws -> [DeclSyntax] {
|
|
34
|
+
diagnosePlacement(of: node, on: declaration, in: context)
|
|
35
|
+
if let genericParameterClause = jsGenericParameterClause(of: declaration) {
|
|
36
|
+
context.diagnose(
|
|
37
|
+
Diagnostic(
|
|
38
|
+
node: genericParameterClause,
|
|
39
|
+
message: JSDiagnosticMessage(
|
|
40
|
+
"A @JS function or initializer can't be generic: the binding decodes each argument and encodes the result by its static type, which a generic parameter doesn't have. Use a concrete type, a @Union of the types, or 'JavaScriptValue'.",
|
|
41
|
+
id: "js-generic-member",
|
|
42
|
+
severity: .error
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
34
48
|
diagnoseParameterSpecifiers(in: declaration, in: context)
|
|
35
49
|
diagnoseFreeFormTypes(in: declaration, in: context)
|
|
36
50
|
diagnoseClosureTypes(in: declaration, in: context)
|
|
@@ -75,6 +89,116 @@ private func isInBindingClass(_ context: some MacroExpansionContext) -> Bool {
|
|
|
75
89
|
|| classDecl.attributes.firstAttribute(named: "SharedObject") != nil
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
/// Reports a `@JS` attribute that no macro binds, so the member would be missing in JavaScript without
|
|
93
|
+
/// an error:
|
|
94
|
+
/// - on a declaration other than a function, property or initializer, such as a subscript;
|
|
95
|
+
/// - on a member of an extension, since `@ExpoModule` and `@SharedObject` read only the class body;
|
|
96
|
+
/// - on a member of a type that isn't an `@ExpoModule` or `@SharedObject` class;
|
|
97
|
+
/// - on a `static` or `class` member of an `@ExpoModule` class, since JavaScript gets only the instance;
|
|
98
|
+
/// - on an initializer of an `@ExpoModule` class, since JavaScript doesn't construct a module.
|
|
99
|
+
private func diagnosePlacement(
|
|
100
|
+
of node: AttributeSyntax,
|
|
101
|
+
on declaration: some DeclSyntaxProtocol,
|
|
102
|
+
in context: some MacroExpansionContext
|
|
103
|
+
) {
|
|
104
|
+
guard
|
|
105
|
+
declaration.is(FunctionDeclSyntax.self) || declaration.is(VariableDeclSyntax.self)
|
|
106
|
+
|| declaration.is(InitializerDeclSyntax.self)
|
|
107
|
+
else {
|
|
108
|
+
let kind = declaration.is(SubscriptDeclSyntax.self) ? "a subscript" : "this declaration"
|
|
109
|
+
context.diagnose(
|
|
110
|
+
Diagnostic(
|
|
111
|
+
node: node,
|
|
112
|
+
message: JSDiagnosticMessage(
|
|
113
|
+
"@JS applies to a function, property or initializer, so \(kind) isn't bound to JavaScript. Expose a @JS function instead.",
|
|
114
|
+
id: "js-unsupported-declaration",
|
|
115
|
+
severity: .error
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if let extensionDecl = context.lexicalContext.first?.as(ExtensionDeclSyntax.self) {
|
|
122
|
+
let typeName = extensionDecl.extendedType.trimmedDescription
|
|
123
|
+
context.diagnose(
|
|
124
|
+
Diagnostic(
|
|
125
|
+
node: node,
|
|
126
|
+
message: JSDiagnosticMessage(
|
|
127
|
+
"A @JS member of an extension isn't bound to JavaScript: @ExpoModule and @SharedObject bind only the @JS members declared in the class body. Move the member into the declaration of '\(typeName)'.",
|
|
128
|
+
id: "js-in-extension",
|
|
129
|
+
severity: .error
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if let enclosingType = context.lexicalContext.first, enclosingType.isProtocol(DeclGroupSyntax.self),
|
|
136
|
+
!isInBindingClass(context)
|
|
137
|
+
{
|
|
138
|
+
let typeName = enclosingType.asProtocol(NamedDeclSyntax.self).map { "'\($0.name.text)'" } ?? "this type"
|
|
139
|
+
context.diagnose(
|
|
140
|
+
Diagnostic(
|
|
141
|
+
node: node,
|
|
142
|
+
message: JSDiagnosticMessage(
|
|
143
|
+
"A @JS member of \(typeName) isn't bound to JavaScript: only an @ExpoModule or @SharedObject class binds its @JS members. Add one of these macros to \(typeName), or move the member into such a class.",
|
|
144
|
+
id: "js-outside-binding-class",
|
|
145
|
+
severity: .error
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if let modifiers = declaration.asProtocol(WithModifiersSyntax.self)?.modifiers, isTypeLevel(modifiers),
|
|
152
|
+
let classDecl = context.lexicalContext.first?.as(ClassDeclSyntax.self),
|
|
153
|
+
classDecl.attributes.firstAttribute(named: "ExpoModule") != nil
|
|
154
|
+
{
|
|
155
|
+
context.diagnose(
|
|
156
|
+
Diagnostic(
|
|
157
|
+
node: node,
|
|
158
|
+
message: JSDiagnosticMessage(
|
|
159
|
+
"A @JS member of an @ExpoModule class can't be 'static' or 'class': JavaScript gets one module object, and @ExpoModule binds the members of the instance to it. Make the member an instance member.",
|
|
160
|
+
id: "js-module-static-member",
|
|
161
|
+
severity: .error
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if let initDecl = declaration.as(InitializerDeclSyntax.self),
|
|
168
|
+
let classDecl = context.lexicalContext.first?.as(ClassDeclSyntax.self),
|
|
169
|
+
classDecl.attributes.firstAttribute(named: "ExpoModule") != nil
|
|
170
|
+
{
|
|
171
|
+
context.diagnose(
|
|
172
|
+
Diagnostic(
|
|
173
|
+
node: node,
|
|
174
|
+
message: JSDiagnosticMessage(
|
|
175
|
+
"@JS on an initializer of an @ExpoModule class isn't bound to JavaScript: JavaScript doesn't construct a module, it gets the instance that the app context creates. Remove @JS, or construct a @SharedObject class from JavaScript instead.",
|
|
176
|
+
id: "js-module-initializer",
|
|
177
|
+
severity: .error
|
|
178
|
+
),
|
|
179
|
+
fixIts: removeAttributeFixIts(node, before: initDecl.initKeyword)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// The fix-it that removes a `@JS` attribute written on its own line, with the line break and
|
|
186
|
+
/// indentation before it. The macro gets the attribute without the trivia after it, so when the
|
|
187
|
+
/// declaration starts on the line of the attribute (`@JS init()`), the fix-it can't tell how much
|
|
188
|
+
/// space to remove, and there is none. It edits only the text of the attribute, since the declaration
|
|
189
|
+
/// that a peer macro gets can carry attributes that other macros added.
|
|
190
|
+
private func removeAttributeFixIts(_ node: AttributeSyntax, before keyword: TokenSyntax) -> [FixIt] {
|
|
191
|
+
guard keyword.leadingTrivia.contains(where: \.isNewline) else {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
FixIt(
|
|
196
|
+
message: JSFixItMessage("Remove '@JS'", id: "js-remove-attribute"),
|
|
197
|
+
changes: [.replaceText(range: node.position..<node.endPosition, with: "", in: Syntax(node))]
|
|
198
|
+
)
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
|
|
78
202
|
/// Emits the diagnostic for `@JS(.concurrent)` on a member that can't take it. The option maps to
|
|
79
203
|
/// Swift's `@concurrent`, which requires an `async` function: a synchronous member has nowhere to
|
|
80
204
|
/// suspend, and a property or initializer can't be async at all. Diagnosing here points at the
|
|
@@ -81,6 +81,12 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
81
81
|
// the author's version stand — the factories only need *an* `init(<properties>)` and `Record` only
|
|
82
82
|
// needs *an* `init()`, regardless of who wrote them.
|
|
83
83
|
let existingInitLabels = initializerParameterLabels(of: declaration)
|
|
84
|
+
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
85
|
+
let diagnostics = unsetPrivatePropertyDiagnostics(of: declaration)
|
|
86
|
+
guard diagnostics.isEmpty else {
|
|
87
|
+
throw DiagnosticsError(diagnostics: diagnostics)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
var members: [DeclSyntax] = []
|
|
86
92
|
|
|
@@ -227,6 +233,10 @@ private func recordProperties(
|
|
|
227
233
|
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
228
234
|
continue
|
|
229
235
|
}
|
|
236
|
+
// The memberwise init sets every property, and a `let` with a default value can't be set again.
|
|
237
|
+
if varDecl.bindingSpecifier.tokenKind == .keyword(.let), binding.initializer != nil {
|
|
238
|
+
throw DiagnosticsError(diagnostics: [letWithDefaultDiagnostic(for: varDecl, name: ident.identifier.text)])
|
|
239
|
+
}
|
|
230
240
|
// Prefer the explicit annotation. When it's omitted, recover the type from a literal default
|
|
231
241
|
// (Swift's own default-literal type) — covers the common `var name = "foo"` case. Anything
|
|
232
242
|
// whose type a syntactic macro can't determine (calls, collections, member access) still needs
|
|
@@ -637,6 +647,64 @@ private func unsupportedClassDiagnostics(of declaration: some DeclGroupSyntax) -
|
|
|
637
647
|
return diagnostics
|
|
638
648
|
}
|
|
639
649
|
|
|
650
|
+
/// The errors for the `private` and `fileprivate` stored properties that the synthesized memberwise
|
|
651
|
+
/// initializer would leave without a value. Such a property isn't a record property, so the initializer
|
|
652
|
+
/// doesn't set it, and it needs a default value. An optional `var` starts as `nil`, so it needs none.
|
|
653
|
+
private func unsetPrivatePropertyDiagnostics(of declaration: some DeclGroupSyntax) -> [Diagnostic] {
|
|
654
|
+
var diagnostics: [Diagnostic] = []
|
|
655
|
+
for member in declaration.memberBlock.members {
|
|
656
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self),
|
|
657
|
+
let access = varDecl.modifiers.first(where: {
|
|
658
|
+
$0.name.tokenKind == .keyword(.private) || $0.name.tokenKind == .keyword(.fileprivate)
|
|
659
|
+
}),
|
|
660
|
+
!varDecl.modifiers.contains(where: {
|
|
661
|
+
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
662
|
+
|| $0.name.tokenKind == .keyword(.lazy)
|
|
663
|
+
})
|
|
664
|
+
else {
|
|
665
|
+
continue
|
|
666
|
+
}
|
|
667
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
668
|
+
for binding in varDecl.bindings where binding.accessorBlock == nil && binding.initializer == nil {
|
|
669
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
670
|
+
!(isVar && binding.typeAnnotation.map { isOptionalType($0.type) } == true)
|
|
671
|
+
else {
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
diagnostics.append(
|
|
675
|
+
Diagnostic(
|
|
676
|
+
node: ident,
|
|
677
|
+
message: RecordDiagnosticMessage(
|
|
678
|
+
"@Record leaves out the \(access.name.text) property '\(ident.identifier.text)', so the synthesized initializer doesn't set it. Give it a default value, or remove '\(access.name.text)' to make it a record property",
|
|
679
|
+
id: "record-unset-private-property"
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return diagnostics
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/// The error for a `let` property with a default value, attached to the `let` keyword and carrying a
|
|
689
|
+
/// fix-it that replaces it with `var`. The default still applies when JavaScript omits the property.
|
|
690
|
+
private func letWithDefaultDiagnostic(for varDecl: VariableDeclSyntax, name: String) -> Diagnostic {
|
|
691
|
+
let specifier = varDecl.bindingSpecifier
|
|
692
|
+
let fixIt = FixIt(
|
|
693
|
+
message: RecordFixItMessage("Replace 'let' with 'var'", id: "record-let-to-var"),
|
|
694
|
+
changes: [
|
|
695
|
+
.replace(oldNode: Syntax(specifier), newNode: Syntax(specifier.with(\.tokenKind, .keyword(.var))))
|
|
696
|
+
]
|
|
697
|
+
)
|
|
698
|
+
return Diagnostic(
|
|
699
|
+
node: specifier,
|
|
700
|
+
message: RecordDiagnosticMessage(
|
|
701
|
+
"@Record property '\(name)' is a 'let' with a default value, so the synthesized initializer can't set it from JavaScript. Make it a 'var', and the default applies when JavaScript omits the property, or make it 'static' or 'private' to leave it out of the record",
|
|
702
|
+
id: "record-let-with-default"
|
|
703
|
+
),
|
|
704
|
+
fixIts: [fixIt]
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
|
|
640
708
|
/// The fix-it for a record class that isn't `final`: add the modifier right before `class`.
|
|
641
709
|
private func markFinalFixIt(for classDecl: ClassDeclSyntax) -> FixIt {
|
|
642
710
|
// The new modifier takes over the leading trivia of `class` (the line break after the attribute
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
1
2
|
import SwiftSyntax
|
|
2
3
|
import SwiftSyntaxBuilder
|
|
3
4
|
import SwiftSyntaxMacros
|
|
@@ -42,9 +43,13 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
42
43
|
in: context
|
|
43
44
|
)
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
+
// A class without an inheritance clause has no superclass, so it can't be a `SharedObject`. Any
|
|
47
|
+
// other superclass isn't checked: the macro sees only this declaration, so it can't tell an
|
|
46
48
|
// indirect subclass (`class Thumbnail: ImageRef`) from an unrelated class. The compiler reports a
|
|
47
49
|
// class that isn't a `SharedObject` subclass on the `_jsName` override, which is always emitted.
|
|
50
|
+
if classDecl.inheritanceClause == nil {
|
|
51
|
+
throw DiagnosticsError(diagnostics: [missingBaseClassDiagnostic(for: classDecl)])
|
|
52
|
+
}
|
|
48
53
|
let typeName = classDecl.name.text
|
|
49
54
|
let jsName = jsNameArgument(of: node) ?? withoutBackticks(typeName)
|
|
50
55
|
|
|
@@ -71,6 +76,10 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
71
76
|
for member in classDecl.memberBlock.members {
|
|
72
77
|
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
73
78
|
let decl = aliases.resolving(member.decl)
|
|
79
|
+
// A generic member is reported by `@JS` and left out, so the build has no errors in its binding.
|
|
80
|
+
if memberHasJSAttribute(decl) && jsGenericParameterClause(of: decl) != nil {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
74
83
|
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
75
84
|
assertions.append(assertion)
|
|
76
85
|
}
|
|
@@ -114,6 +123,19 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
114
123
|
|
|
115
124
|
diagnoseJSNameCollisions(functions: instanceFunctions, properties: instanceProperties, in: context)
|
|
116
125
|
diagnoseJSNameCollisions(functions: staticFunctions, properties: staticProperties, in: context)
|
|
126
|
+
diagnoseReservedJSNames(
|
|
127
|
+
functions: instanceFunctions,
|
|
128
|
+
properties: instanceProperties,
|
|
129
|
+
on: .prototype,
|
|
130
|
+
in: context
|
|
131
|
+
)
|
|
132
|
+
diagnoseReservedJSNames(
|
|
133
|
+
functions: staticFunctions,
|
|
134
|
+
properties: staticProperties,
|
|
135
|
+
on: .constructor,
|
|
136
|
+
in: context
|
|
137
|
+
)
|
|
138
|
+
diagnoseEventNameCollisions(in: classDecl.memberBlock, in: context)
|
|
117
139
|
|
|
118
140
|
// The resolved JS class name, read by core when it builds the class. An `override` of the
|
|
119
141
|
// `SharedObject` class property, so core reads it through the concrete type's metatype.
|
|
@@ -199,3 +221,59 @@ extension SharedObjectMacro: ExtensionMacro {
|
|
|
199
221
|
return [sendable]
|
|
200
222
|
}
|
|
201
223
|
}
|
|
224
|
+
|
|
225
|
+
/// The error for a `@SharedObject` class without an inheritance clause, with a fix-it that makes it
|
|
226
|
+
/// inherit from `SharedObject`: `class Cache {` becomes `class Cache: SharedObject {`.
|
|
227
|
+
private func missingBaseClassDiagnostic(for classDecl: ClassDeclSyntax) -> Diagnostic {
|
|
228
|
+
// The name carries the space before `{` as trailing trivia, so move it onto the clause to keep the
|
|
229
|
+
// spacing.
|
|
230
|
+
let name = classDecl.name
|
|
231
|
+
let clause = InheritanceClauseSyntax(
|
|
232
|
+
colon: .colonToken(trailingTrivia: .space),
|
|
233
|
+
inheritedTypes: InheritedTypeListSyntax([
|
|
234
|
+
InheritedTypeSyntax(type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("SharedObject"))))
|
|
235
|
+
])
|
|
236
|
+
)
|
|
237
|
+
let fixIt = FixIt(
|
|
238
|
+
message: SharedObjectFixItMessage("Inherit from 'SharedObject'", id: "sharedobject-add-base-class"),
|
|
239
|
+
changes: [
|
|
240
|
+
.replace(
|
|
241
|
+
oldNode: Syntax(classDecl),
|
|
242
|
+
newNode: Syntax(
|
|
243
|
+
classDecl
|
|
244
|
+
.with(\.name, name.with(\.trailingTrivia, []))
|
|
245
|
+
.with(\.inheritanceClause, clause.with(\.trailingTrivia, name.trailingTrivia))
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
]
|
|
249
|
+
)
|
|
250
|
+
return Diagnostic(
|
|
251
|
+
node: name,
|
|
252
|
+
message: SharedObjectDiagnosticMessage(
|
|
253
|
+
"@SharedObject class '\(name.text)' must inherit from 'SharedObject', directly or through another class such as 'SharedRef'. Add ': SharedObject' to the class declaration.",
|
|
254
|
+
id: "sharedobject-missing-base-class"
|
|
255
|
+
),
|
|
256
|
+
fixIts: [fixIt]
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private struct SharedObjectDiagnosticMessage: DiagnosticMessage {
|
|
261
|
+
let message: String
|
|
262
|
+
let diagnosticID: MessageID
|
|
263
|
+
let severity: DiagnosticSeverity = .error
|
|
264
|
+
|
|
265
|
+
init(_ message: String, id: String) {
|
|
266
|
+
self.message = message
|
|
267
|
+
self.diagnosticID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private struct SharedObjectFixItMessage: FixItMessage {
|
|
272
|
+
let message: String
|
|
273
|
+
let fixItID: MessageID
|
|
274
|
+
|
|
275
|
+
init(_ message: String, id: String) {
|
|
276
|
+
self.message = message
|
|
277
|
+
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -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
|
|