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
|
@@ -11,6 +11,17 @@ import SwiftSyntaxMacros
|
|
|
11
11
|
/// name through the view's event emitter. No JS function object is ever decoded into it.
|
|
12
12
|
/// - every other property is a **value prop**, decoded from the raw props Fabric delivers.
|
|
13
13
|
///
|
|
14
|
+
/// An event must be spelled as a function type. The macro reads only the syntax, so a closure type
|
|
15
|
+
/// behind a typealias (`var onTap: TapHandler`) is a value prop.
|
|
16
|
+
///
|
|
17
|
+
/// Core creates the props with `Self()` and resets a prop that React removes (sends as `null`) to its
|
|
18
|
+
/// value there, so every value prop is a `var` with a default. When the type declares its own
|
|
19
|
+
/// `init()`, that initializer sets the defaults, and the compiler checks that it sets every stored
|
|
20
|
+
/// property. Otherwise each stored value prop needs an initial value, or a type written as `T?` or
|
|
21
|
+
/// `T!`, which defaults to `nil` (`Optional<T>` doesn't), and so does each `private` or `fileprivate`
|
|
22
|
+
/// stored property, which isn't a prop. The author's `init()` must be non-failable, non-throwing and
|
|
23
|
+
/// synchronous, so that core can call it.
|
|
24
|
+
///
|
|
14
25
|
/// The macro synthesizes the identity surface the batched reaction model reads:
|
|
15
26
|
///
|
|
16
27
|
/// - `PropName`: a `String`-backed `CaseIterable` enum, one case per value prop, whose raw value is
|
|
@@ -21,6 +32,22 @@ import SwiftSyntaxMacros
|
|
|
21
32
|
/// allocation and no hashing.
|
|
22
33
|
/// - `_eventNames`: the event props' names, verbatim, for core to register at view creation.
|
|
23
34
|
/// - `typealias Diff = PropsDiff<Self>`, the nested spelling of core's one generic diff.
|
|
35
|
+
/// - a public `init()`, which `AnyViewProps` requires, when Swift's own can't satisfy it: when an
|
|
36
|
+
/// event prop has no initial value, when the type is `public` or `package` (Swift's `init()` is
|
|
37
|
+
/// `internal`), also through a `public` or `package` extension that contains it, or when the type
|
|
38
|
+
/// declares other initializers (Swift then synthesizes no `init()`). It gives each
|
|
39
|
+
/// event without an initial value an empty closure (`{}`, `{ _ in }`, ...) and leaves the value
|
|
40
|
+
/// props at their initial values. It is never generated when the type declares its own `init()`.
|
|
41
|
+
/// Like any initializer in the struct body, the generated one removes the memberwise initializer.
|
|
42
|
+
///
|
|
43
|
+
/// An event's function type must return `Void`, because JavaScript can't return a value to it.
|
|
44
|
+
///
|
|
45
|
+
/// The extension that conforms the type to `AnyViewProps` adds:
|
|
46
|
+
///
|
|
47
|
+
/// - `allProps`: every bit set, the changed set on the first application.
|
|
48
|
+
/// - `propSet(for:)`: one name's bit, for folding raw changed keys into the mask.
|
|
49
|
+
/// - `_decode(_:from:in:)`: assigns one value prop from a JavaScript value. `null` and `undefined`
|
|
50
|
+
/// reset it to its value in `Self()`, and any other value decodes through `JavaScriptDecodable`.
|
|
24
51
|
///
|
|
25
52
|
/// Author-facing shape — no conformance to spell out:
|
|
26
53
|
///
|
|
@@ -46,19 +73,23 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
46
73
|
conformingTo protocols: [TypeSyntax],
|
|
47
74
|
in context: some MacroExpansionContext
|
|
48
75
|
) throws -> [DeclSyntax] {
|
|
49
|
-
let props = try validatedViewProps(of: declaration)
|
|
76
|
+
let props = try validatedViewProps(of: declaration, in: context.lexicalContext)
|
|
50
77
|
|
|
51
78
|
var members: [DeclSyntax] = []
|
|
52
79
|
members.append(propNameEnum(valueProps: props.valueProps))
|
|
53
80
|
members.append(propSetStruct(valueProps: props.valueProps))
|
|
54
81
|
members.append(eventNamesConstant(eventProps: props.eventProps))
|
|
55
82
|
members.append(diffTypealias())
|
|
83
|
+
if let initializer = defaultInitializer(props: props) {
|
|
84
|
+
members.append(initializer)
|
|
85
|
+
}
|
|
56
86
|
return members
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
/// Auto-conforms the type to `AnyViewProps`, supplying the
|
|
60
|
-
/// per-props: `allProps` (every bit set, the changed set on the first application)
|
|
61
|
-
/// `propSet(for:)` (one name's bit, for folding raw changed keys into the mask)
|
|
89
|
+
/// Auto-conforms the type to `AnyViewProps`, supplying the requirements that can only be written
|
|
90
|
+
/// per-props: `allProps` (every bit set, the changed set on the first application),
|
|
91
|
+
/// `propSet(for:)` (one name's bit, for folding raw changed keys into the mask) and `_decode`
|
|
92
|
+
/// (one value prop assigned from a JavaScript value).
|
|
62
93
|
///
|
|
63
94
|
/// A conformance the author already spelled out in the inheritance clause is not repeated.
|
|
64
95
|
public static func expansion(
|
|
@@ -75,7 +106,7 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
75
106
|
// expansion runs each role independently, so throwing here too would surface every diagnostic
|
|
76
107
|
// twice. When the model doesn't validate, the member macro has already emitted the error and
|
|
77
108
|
// there is nothing to extend.
|
|
78
|
-
guard let props = try? validatedViewProps(of: declaration) else {
|
|
109
|
+
guard let props = try? validatedViewProps(of: declaration, in: context.lexicalContext) else {
|
|
79
110
|
return []
|
|
80
111
|
}
|
|
81
112
|
let alreadyConforms = inheritsProtocol(named: viewPropsProtocolName, in: declaration)
|
|
@@ -89,8 +120,7 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
89
120
|
? "0"
|
|
90
121
|
: "0b" + String(repeating: "1", count: props.valueProps.count)
|
|
91
122
|
|
|
92
|
-
// With no value prop there's nothing to switch over,
|
|
93
|
-
// enum doesn't compile — return the empty set instead.
|
|
123
|
+
// With no value prop there's nothing to switch over, so return the empty set.
|
|
94
124
|
let propSetBody: String
|
|
95
125
|
if props.valueProps.isEmpty {
|
|
96
126
|
propSetBody = " return []"
|
|
@@ -101,6 +131,8 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
101
131
|
propSetBody = " switch name {\n\(cases)\n }"
|
|
102
132
|
}
|
|
103
133
|
|
|
134
|
+
let decodeBody = decodeFunctionBody(valueProps: props.valueProps)
|
|
135
|
+
|
|
104
136
|
let conformanceClause = alreadyConforms ? "" : ": \(viewPropsProtocolName)"
|
|
105
137
|
let ext: DeclSyntax = """
|
|
106
138
|
extension \(type.trimmed)\(raw: conformanceClause) {
|
|
@@ -112,6 +144,10 @@ public struct ViewPropsMacro: MemberMacro, ExtensionMacro {
|
|
|
112
144
|
public static func propSet(for name: PropName) -> PropSet {
|
|
113
145
|
\(raw: propSetBody)
|
|
114
146
|
}
|
|
147
|
+
|
|
148
|
+
@JavaScriptActor
|
|
149
|
+
public mutating func _decode(_ name: PropName, from value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws {\(raw: decodeBody)
|
|
150
|
+
}
|
|
115
151
|
}
|
|
116
152
|
"""
|
|
117
153
|
guard let extDecl = ext.as(ExtensionDeclSyntax.self) else {
|
|
@@ -134,7 +170,9 @@ private let maxValueProps = 64
|
|
|
134
170
|
/// The error for a leftover `@Field`, attached to the attribute itself and carrying a fix-it that
|
|
135
171
|
/// deletes it. `@ViewProps` treats every stored property as a prop, so the attribute has no meaning
|
|
136
172
|
/// here; left in place it would wrap the value in `Field<T>` and the props type would decode against
|
|
137
|
-
/// the wrong type.
|
|
173
|
+
/// the wrong type. A `@Field` with arguments, such as `@Field(.keyed("borderColor"))`, gets no
|
|
174
|
+
/// fix-it: they can set the prop name, which removing the attribute would change silently, so the
|
|
175
|
+
/// message tells the author to rename the property.
|
|
138
176
|
private func fieldAttributeDiagnostic(
|
|
139
177
|
for attribute: AttributeSyntax,
|
|
140
178
|
on varDecl: VariableDeclSyntax
|
|
@@ -173,11 +211,14 @@ private func fieldAttributeDiagnostic(
|
|
|
173
211
|
)
|
|
174
212
|
]
|
|
175
213
|
)
|
|
214
|
+
let hasArguments = attribute.arguments?.as(LabeledExprListSyntax.self)?.isEmpty == false
|
|
176
215
|
let message = ViewPropsDiagnosticMessage(
|
|
177
|
-
|
|
216
|
+
hasArguments
|
|
217
|
+
? "@Field is no longer used: @ViewProps treats every stored property as a prop, named after the property. Remove the @Field attribute, and rename the property if its arguments set another prop name"
|
|
218
|
+
: "@Field is no longer used: @ViewProps treats every stored property as a prop. Remove the @Field attribute",
|
|
178
219
|
id: "viewprops-field-attribute"
|
|
179
220
|
)
|
|
180
|
-
return Diagnostic(node: attribute, message: message, fixIts: [fixIt])
|
|
221
|
+
return Diagnostic(node: attribute, message: message, fixIts: hasArguments ? [] : [fixIt])
|
|
181
222
|
}
|
|
182
223
|
|
|
183
224
|
/// Replaces the leading trivia of one token.
|
|
@@ -244,6 +285,178 @@ private func innerFunctionType(of type: TypeSyntax) -> TypeSyntax? {
|
|
|
244
285
|
return only.type
|
|
245
286
|
}
|
|
246
287
|
|
|
288
|
+
/// The error for a `let` declaring value props, attached to the `let` keyword and carrying a fix-it
|
|
289
|
+
/// that changes it to `var`. `_decode` assigns each value prop when React sets or removes it, which a
|
|
290
|
+
/// constant doesn't allow. The fix-it changes every binding of the declaration, so the message names
|
|
291
|
+
/// each value prop it declares.
|
|
292
|
+
private func letValuePropDiagnostic(for varDecl: VariableDeclSyntax, names: [String]) -> Diagnostic {
|
|
293
|
+
let specifier = varDecl.bindingSpecifier
|
|
294
|
+
let fixIt = FixIt(
|
|
295
|
+
message: ViewPropsFixItMessage("Replace 'let' with 'var'", id: "viewprops-let-to-var"),
|
|
296
|
+
changes: [
|
|
297
|
+
.replace(
|
|
298
|
+
oldNode: Syntax(specifier),
|
|
299
|
+
newNode: Syntax(specifier.with(\.tokenKind, .keyword(.var)))
|
|
300
|
+
)
|
|
301
|
+
]
|
|
302
|
+
)
|
|
303
|
+
let quoted = names.map { "'\(withoutBackticks($0))'" }
|
|
304
|
+
let text =
|
|
305
|
+
quoted.count == 1
|
|
306
|
+
? "View prop \(quoted[0]) can't be a 'let' constant, because it's assigned whenever React sets or removes the prop. Declare it with 'var'"
|
|
307
|
+
: "View props \(quoted.dropLast().joined(separator: ", ")) and \(quoted[quoted.count - 1]) can't be 'let' constants, because they're assigned whenever React sets or removes the props. Declare them with 'var'"
|
|
308
|
+
return Diagnostic(
|
|
309
|
+
node: specifier,
|
|
310
|
+
message: ViewPropsDiagnosticMessage(text, id: "viewprops-let-prop"),
|
|
311
|
+
fixIts: [fixIt]
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// The error for an event prop whose function type returns something other than `Void`, attached to
|
|
316
|
+
/// the return type and carrying a fix-it that changes it to `Void`. Core dispatches the event to
|
|
317
|
+
/// JavaScript, which can't return a value to the caller, and the generated `init()` couldn't give the
|
|
318
|
+
/// event an empty closure.
|
|
319
|
+
private func eventReturnTypeDiagnostic(for returnType: TypeSyntax, name: String) -> Diagnostic {
|
|
320
|
+
let message = ViewPropsDiagnosticMessage(
|
|
321
|
+
"Event prop '\(withoutBackticks(name))' must return Void, because an event is sent to JavaScript, which can't return a value. Change the return type '\(returnType.trimmedDescription)' to 'Void'",
|
|
322
|
+
id: "viewprops-event-return-type"
|
|
323
|
+
)
|
|
324
|
+
let void = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Void")))
|
|
325
|
+
.with(\.leadingTrivia, returnType.leadingTrivia)
|
|
326
|
+
.with(\.trailingTrivia, returnType.trailingTrivia)
|
|
327
|
+
let fixIt = FixIt(
|
|
328
|
+
message: ViewPropsFixItMessage("Change the return type to 'Void'", id: "viewprops-event-return-void"),
|
|
329
|
+
changes: [.replace(oldNode: Syntax(returnType), newNode: Syntax(void))]
|
|
330
|
+
)
|
|
331
|
+
return Diagnostic(node: returnType, message: message, fixIts: [fixIt])
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// The error for a value prop with no default, attached to the property name. A value prop starts
|
|
335
|
+
/// from its value in `Self()` and returns to it when React removes the prop, so it needs an initial
|
|
336
|
+
/// value, or a type written as `T?`, whose default is `nil`. There is no fix-it, since the default is
|
|
337
|
+
/// the author's choice, except for a type written as `Optional<T>`: rewriting it as `T?` keeps the
|
|
338
|
+
/// type and only adds the implicit `nil`.
|
|
339
|
+
private func missingDefaultDiagnostic(
|
|
340
|
+
for pattern: IdentifierPatternSyntax,
|
|
341
|
+
name: String,
|
|
342
|
+
type: TypeSyntax?
|
|
343
|
+
) -> Diagnostic {
|
|
344
|
+
if let type, let sugared = optionalSugar(for: type) {
|
|
345
|
+
let fixIt = FixIt(
|
|
346
|
+
message: ViewPropsFixItMessage(
|
|
347
|
+
"Write the type as '\(sugared.trimmedDescription)'",
|
|
348
|
+
id: "viewprops-optional-sugar"
|
|
349
|
+
),
|
|
350
|
+
changes: [.replace(oldNode: Syntax(type), newNode: Syntax(sugared))]
|
|
351
|
+
)
|
|
352
|
+
let message = ViewPropsDiagnosticMessage(
|
|
353
|
+
"View prop '\(withoutBackticks(name))' needs an initial value, which is its default when React removes the prop. A type written as '\(type.trimmedDescription)' gets no implicit nil, so write it as '\(sugared.trimmedDescription)'",
|
|
354
|
+
id: "viewprops-missing-default"
|
|
355
|
+
)
|
|
356
|
+
return Diagnostic(node: pattern, message: message, fixIts: [fixIt])
|
|
357
|
+
}
|
|
358
|
+
let message = ViewPropsDiagnosticMessage(
|
|
359
|
+
"View prop '\(withoutBackticks(name))' needs an initial value, which is its default when React removes the prop. Give it one, or make its type optional with '?', which defaults to nil",
|
|
360
|
+
id: "viewprops-missing-default"
|
|
361
|
+
)
|
|
362
|
+
return Diagnostic(node: pattern, message: message)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// `T?` for a type written as `Optional<T>`, with the trivia of the original, or `nil` for any other
|
|
366
|
+
/// type. A wrapped `any P`, `some P` or `P & Q` keeps parentheses, since `any P?` would make only `P`
|
|
367
|
+
/// optional.
|
|
368
|
+
private func optionalSugar(for type: TypeSyntax) -> TypeSyntax? {
|
|
369
|
+
guard let identifier = type.as(IdentifierTypeSyntax.self), identifier.name.text == "Optional",
|
|
370
|
+
let wrapped = optionalWrappedType(type)
|
|
371
|
+
else {
|
|
372
|
+
return nil
|
|
373
|
+
}
|
|
374
|
+
var wrappedType = wrapped.trimmed
|
|
375
|
+
if wrappedType.is(SomeOrAnyTypeSyntax.self) || wrappedType.is(CompositionTypeSyntax.self) {
|
|
376
|
+
wrappedType = TypeSyntax(TupleTypeSyntax(elements: [TupleTypeElementSyntax(type: wrappedType)]))
|
|
377
|
+
}
|
|
378
|
+
return TypeSyntax(OptionalTypeSyntax(wrappedType: wrappedType))
|
|
379
|
+
.with(\.leadingTrivia, type.leadingTrivia)
|
|
380
|
+
.with(\.trailingTrivia, type.trailingTrivia)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// The error for a `private` or `fileprivate` stored property without an initial value, attached to
|
|
384
|
+
/// its name. `@ViewProps` leaves it out of the props, so the `init()` that core calls doesn't set it.
|
|
385
|
+
/// There is no fix-it: the value is the author's choice.
|
|
386
|
+
private func unsetPrivatePropertyDiagnostic(
|
|
387
|
+
for pattern: IdentifierPatternSyntax,
|
|
388
|
+
access: String
|
|
389
|
+
) -> Diagnostic {
|
|
390
|
+
let message = ViewPropsDiagnosticMessage(
|
|
391
|
+
"@ViewProps leaves out the \(access) property '\(withoutBackticks(pattern.identifier.text))', so 'init()' doesn't set it. Give it an initial value, or remove '\(access)' to make it a prop",
|
|
392
|
+
id: "viewprops-unset-private-property"
|
|
393
|
+
)
|
|
394
|
+
return Diagnostic(node: pattern, message: message)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// The errors for an `init()` that core can't call as `Self()`, one for each token that makes it
|
|
398
|
+
/// unusable: the `?` or `!` of a failable initializer, `async`, and the `throws` clause. Each is
|
|
399
|
+
/// attached to its token and carries a fix-it that removes it with its trailing space, so
|
|
400
|
+
/// `init() async throws {` becomes `init() throws {`, and then `init() {`.
|
|
401
|
+
private func unusableInitializerDiagnostics(for initializer: InitializerDeclSyntax) -> [Diagnostic] {
|
|
402
|
+
var diagnostics: [Diagnostic] = []
|
|
403
|
+
if let optionalMark = initializer.optionalMark {
|
|
404
|
+
diagnostics.append(
|
|
405
|
+
unusableInitializerDiagnostic(
|
|
406
|
+
on: Syntax(optionalMark),
|
|
407
|
+
removing: Syntax(optionalMark.with(\.presence, .missing)),
|
|
408
|
+
requirement: "non-failable",
|
|
409
|
+
fixItID: "viewprops-remove-failable"
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
let effectSpecifiers = initializer.signature.effectSpecifiers
|
|
414
|
+
if let asyncSpecifier = effectSpecifiers?.asyncSpecifier {
|
|
415
|
+
diagnostics.append(
|
|
416
|
+
unusableInitializerDiagnostic(
|
|
417
|
+
on: Syntax(asyncSpecifier),
|
|
418
|
+
removing: Syntax(asyncSpecifier.with(\.presence, .missing)),
|
|
419
|
+
requirement: "synchronous",
|
|
420
|
+
fixItID: "viewprops-remove-async"
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
if let throwsClause = effectSpecifiers?.throwsClause {
|
|
425
|
+
let missingClause = ThrowsClauseSyntax(
|
|
426
|
+
throwsSpecifier: throwsClause.throwsSpecifier.with(\.presence, .missing)
|
|
427
|
+
)
|
|
428
|
+
diagnostics.append(
|
|
429
|
+
unusableInitializerDiagnostic(
|
|
430
|
+
on: Syntax(throwsClause),
|
|
431
|
+
removing: Syntax(missingClause),
|
|
432
|
+
requirement: "non-throwing",
|
|
433
|
+
fixItID: "viewprops-remove-throws"
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
return diagnostics
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// One error of `unusableInitializerDiagnostics`. The fix-it replaces the node, together with its
|
|
441
|
+
/// trivia, by a missing node, which prints as nothing.
|
|
442
|
+
private func unusableInitializerDiagnostic(
|
|
443
|
+
on node: Syntax,
|
|
444
|
+
removing missingNode: Syntax,
|
|
445
|
+
requirement: String,
|
|
446
|
+
fixItID: String
|
|
447
|
+
) -> Diagnostic {
|
|
448
|
+
let text = node.trimmedDescription
|
|
449
|
+
let fixIt = FixIt(
|
|
450
|
+
message: ViewPropsFixItMessage("Remove '\(text)'", id: fixItID),
|
|
451
|
+
changes: [.replace(oldNode: node, newNode: missingNode)]
|
|
452
|
+
)
|
|
453
|
+
let message = ViewPropsDiagnosticMessage(
|
|
454
|
+
"@ViewProps needs a \(requirement) 'init()', because core creates the props with 'Self()'. Remove '\(text)'",
|
|
455
|
+
id: "viewprops-unusable-init"
|
|
456
|
+
)
|
|
457
|
+
return Diagnostic(node: node, message: message, fixIts: [fixIt])
|
|
458
|
+
}
|
|
459
|
+
|
|
247
460
|
private struct ViewPropsDiagnosticMessage: DiagnosticMessage {
|
|
248
461
|
let message: String
|
|
249
462
|
let diagnosticID: MessageID
|
|
@@ -272,9 +485,12 @@ private struct ViewProp {
|
|
|
272
485
|
/// The property name as written, backticks included for an escaped name. Every identifier position
|
|
273
486
|
/// in the generated code uses this, since `case default` and `static let default` don't parse.
|
|
274
487
|
let name: String
|
|
275
|
-
/// The property's declared type, verbatim
|
|
276
|
-
///
|
|
488
|
+
/// The property's declared type, verbatim, or the type of its literal initial value. `_decode`
|
|
489
|
+
/// doesn't spell it: the decoded type is inferred from the assignment.
|
|
277
490
|
let type: String
|
|
491
|
+
/// Whether the prop is computed with a `mutating get`, which `_decode` can only read from a
|
|
492
|
+
/// mutable value.
|
|
493
|
+
let hasMutatingGetter: Bool
|
|
278
494
|
|
|
279
495
|
/// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
|
|
280
496
|
/// `_eventNames` both read `"default"`, never `` "`default`" ``.
|
|
@@ -283,20 +499,41 @@ private struct ViewProp {
|
|
|
283
499
|
}
|
|
284
500
|
}
|
|
285
501
|
|
|
502
|
+
/// An event prop declared without an initial value, which the generated `init()` assigns.
|
|
503
|
+
private struct EventDefault {
|
|
504
|
+
/// The property name as written, backticks included.
|
|
505
|
+
let name: String
|
|
506
|
+
/// A closure literal that ignores its arguments and does nothing: `{}`, `{ _ in }`, `{ _, _ in }`
|
|
507
|
+
/// and so on, by the arity of the event's function type.
|
|
508
|
+
let emptyClosure: String
|
|
509
|
+
}
|
|
510
|
+
|
|
286
511
|
private struct ViewPropsModel {
|
|
287
512
|
/// Value props, in declaration order. The order is the bit order, so it is part of the ABI
|
|
288
513
|
/// between a compiled view and the runtime that fills the mask.
|
|
289
514
|
let valueProps: [ViewProp]
|
|
290
515
|
/// Event props, in declaration order.
|
|
291
516
|
let eventProps: [ViewProp]
|
|
517
|
+
/// The event props without an initial value, in declaration order.
|
|
518
|
+
let eventDefaults: [EventDefault]
|
|
519
|
+
/// Whether the macro generates `init()`. See `needsGeneratedInitializer`.
|
|
520
|
+
let generatesInitializer: Bool
|
|
292
521
|
}
|
|
293
522
|
|
|
294
523
|
/// Reads and validates the props type's stored properties, splitting them into value props and events.
|
|
295
524
|
///
|
|
296
525
|
/// Rejects what can't be expressed: a non-`struct` declaration (the class form is the SwiftUI path and
|
|
297
526
|
/// needs the observable protocol, which is deferred), a leftover `@Field` attribute, an optional
|
|
298
|
-
/// function type, a property with no determinable type,
|
|
299
|
-
|
|
527
|
+
/// function type, an event whose function type returns a value, a property with no determinable type,
|
|
528
|
+
/// a value prop declared with `let` or without a default, a `private` stored property without an
|
|
529
|
+
/// initial value, an `init()` that is failable, throwing or `async`, and more than 64 value props.
|
|
530
|
+
///
|
|
531
|
+
/// When the type declares a usable `init()`, that initializer is the default: it must set every
|
|
532
|
+
/// stored property, which the compiler checks, so the props need no initial values.
|
|
533
|
+
private func validatedViewProps(
|
|
534
|
+
of declaration: some DeclGroupSyntax,
|
|
535
|
+
in lexicalContext: [Syntax]
|
|
536
|
+
) throws -> ViewPropsModel {
|
|
300
537
|
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
|
|
301
538
|
throw MacroExpansionErrorMessage(
|
|
302
539
|
"@ViewProps can only be applied to a struct — the class form (for SwiftUI views) is not supported yet"
|
|
@@ -311,23 +548,66 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
311
548
|
)
|
|
312
549
|
}
|
|
313
550
|
|
|
551
|
+
// `@Field` is the v1 property wrapper and has no meaning here — every stored property is already
|
|
552
|
+
// part of the surface. Left in place it would wrap the value in `Field<T>`, so the props type
|
|
553
|
+
// would decode against the wrong type. Flag every one rather than emit code built on them, all at
|
|
554
|
+
// once, so a `@Field` without a fix-it doesn't hide the fix-its of the others.
|
|
555
|
+
let fieldDiagnostics = declaration.memberBlock.members.compactMap { member -> Diagnostic? in
|
|
556
|
+
guard let varDecl = member.decl.as(VariableDeclSyntax.self), !isExcludedByModifier(varDecl.modifiers),
|
|
557
|
+
let fieldAttribute = varDecl.attributes.firstAttribute(named: "Field")
|
|
558
|
+
else {
|
|
559
|
+
return nil
|
|
560
|
+
}
|
|
561
|
+
return fieldAttributeDiagnostic(for: fieldAttribute, on: varDecl)
|
|
562
|
+
}
|
|
563
|
+
if !fieldDiagnostics.isEmpty {
|
|
564
|
+
throw DiagnosticsError(diagnostics: fieldDiagnostics)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// A usable `init()` belongs to the author and sets the defaults. An `init()` that core can't call
|
|
568
|
+
// as `Self()` doesn't count, and is reported only when there is no usable one: Swift allows an
|
|
569
|
+
// `init() async` next to `init()`, and `Self()` in the synchronous `_decode` calls the latter.
|
|
570
|
+
var propDiagnostics: [Diagnostic] = []
|
|
571
|
+
var unusableInitializerDiagnosticsFound: [Diagnostic] = []
|
|
572
|
+
var declaresUsableInitializer = false
|
|
573
|
+
var declaresOtherInitializer = false
|
|
574
|
+
for member in flattenedMembers(declaration.memberBlock.members) {
|
|
575
|
+
guard let initializer = member.decl.as(InitializerDeclSyntax.self) else {
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
guard initializer.signature.parameterClause.parameters.isEmpty else {
|
|
579
|
+
declaresOtherInitializer = true
|
|
580
|
+
continue
|
|
581
|
+
}
|
|
582
|
+
let unusableDiagnostics = unusableInitializerDiagnostics(for: initializer)
|
|
583
|
+
if unusableDiagnostics.isEmpty {
|
|
584
|
+
declaresUsableInitializer = true
|
|
585
|
+
} else {
|
|
586
|
+
unusableInitializerDiagnosticsFound += unusableDiagnostics
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if !declaresUsableInitializer {
|
|
590
|
+
propDiagnostics += unusableInitializerDiagnosticsFound
|
|
591
|
+
}
|
|
592
|
+
|
|
314
593
|
var valueProps: [ViewProp] = []
|
|
315
594
|
var eventProps: [ViewProp] = []
|
|
595
|
+
var eventDefaults: [EventDefault] = []
|
|
316
596
|
|
|
317
597
|
for member in declaration.memberBlock.members {
|
|
318
598
|
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
319
599
|
continue
|
|
320
600
|
}
|
|
321
601
|
if isExcludedByModifier(varDecl.modifiers) {
|
|
602
|
+
if !declaresUsableInitializer {
|
|
603
|
+
propDiagnostics += unsetPrivatePropertyDiagnostics(of: varDecl)
|
|
604
|
+
}
|
|
322
605
|
continue
|
|
323
606
|
}
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
throw DiagnosticsError(diagnostics: [fieldAttributeDiagnostic(for: fieldAttribute, on: varDecl)])
|
|
329
|
-
}
|
|
330
|
-
|
|
607
|
+
// One `let` can declare several props, but its diagnostic and fix-it belong to the keyword, so
|
|
608
|
+
// it comes before the diagnostics of the bindings, in source order.
|
|
609
|
+
var letValuePropNames: [String] = []
|
|
610
|
+
let letDiagnosticIndex = propDiagnostics.count
|
|
331
611
|
for binding in varDecl.bindings {
|
|
332
612
|
// A computed property is never a prop, but an accessor block alone doesn't mean computed:
|
|
333
613
|
// `willSet`/`didSet` observers imply stored storage. `bindingIsSettable` draws exactly that
|
|
@@ -367,12 +647,12 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
367
647
|
// dynamic, but events are registered once at view creation from a static name list, so there
|
|
368
648
|
// is no shape that could express "sometimes emitted".
|
|
369
649
|
if let declaredType, isOptionalType(declaredType), underlyingFunctionType(of: declaredType) != nil {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
])
|
|
650
|
+
propDiagnostics.append(optionalEventDiagnostic(for: declaredType, name: name))
|
|
651
|
+
continue
|
|
373
652
|
}
|
|
374
653
|
|
|
375
|
-
let
|
|
654
|
+
let eventFunctionType = declaredType.flatMap(underlyingFunctionType)
|
|
655
|
+
let isEvent = eventFunctionType != nil
|
|
376
656
|
// `PropSet` stores its mask in `rawValue`, so a value prop of that name would emit a static
|
|
377
657
|
// member shadowing it and the option set would not compile ("circular reference"). The error
|
|
378
658
|
// would point at generated code, so catch it here and name the property.
|
|
@@ -381,21 +661,242 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
381
661
|
"'rawValue' can't be used as a prop name — it collides with the synthesized PropSet's storage. Rename the property"
|
|
382
662
|
)
|
|
383
663
|
}
|
|
384
|
-
|
|
385
|
-
|
|
664
|
+
// Only stored storage takes an initial value; a computed prop with a setter needs none.
|
|
665
|
+
let needsInitialValue =
|
|
666
|
+
binding.initializer == nil && isStoredBinding(binding) && !declaresUsableInitializer
|
|
667
|
+
let prop = ViewProp(name: name, type: resolvedType, hasMutatingGetter: hasMutatingGetter(binding))
|
|
668
|
+
if let eventFunctionType {
|
|
669
|
+
// An event is dispatched to JavaScript, which can't hand a value back to the caller.
|
|
670
|
+
if !returnsVoid(eventFunctionType) {
|
|
671
|
+
propDiagnostics.append(eventReturnTypeDiagnostic(for: eventFunctionType.returnClause.type, name: name))
|
|
672
|
+
}
|
|
673
|
+
if needsInitialValue {
|
|
674
|
+
eventDefaults.append(
|
|
675
|
+
EventDefault(name: name, emptyClosure: emptyClosure(arity: eventFunctionType.parameters.count))
|
|
676
|
+
)
|
|
677
|
+
}
|
|
386
678
|
eventProps.append(prop)
|
|
387
|
-
|
|
388
|
-
|
|
679
|
+
continue
|
|
680
|
+
}
|
|
681
|
+
// `_decode` assigns every value prop, so none can be a constant.
|
|
682
|
+
if varDecl.bindingSpecifier.tokenKind == .keyword(.let) {
|
|
683
|
+
letValuePropNames.append(name)
|
|
389
684
|
}
|
|
685
|
+
// A removed prop is reset to its value in a fresh `Self()`, so every value prop needs a
|
|
686
|
+
// default: its initial value, or the `nil` that Swift gives an optional `var` without one.
|
|
687
|
+
if needsInitialValue && !(declaredType.map(hasImplicitNilDefault) ?? false) {
|
|
688
|
+
propDiagnostics.append(missingDefaultDiagnostic(for: ident, name: name, type: declaredType))
|
|
689
|
+
}
|
|
690
|
+
valueProps.append(prop)
|
|
691
|
+
}
|
|
692
|
+
if !letValuePropNames.isEmpty {
|
|
693
|
+
propDiagnostics.insert(letValuePropDiagnostic(for: varDecl, names: letValuePropNames), at: letDiagnosticIndex)
|
|
390
694
|
}
|
|
391
695
|
}
|
|
696
|
+
// Reported together, so one run names every prop that the generated code can't reset, assign or
|
|
697
|
+
// default.
|
|
698
|
+
if !propDiagnostics.isEmpty {
|
|
699
|
+
throw DiagnosticsError(diagnostics: propDiagnostics)
|
|
700
|
+
}
|
|
392
701
|
|
|
393
702
|
guard valueProps.count <= maxValueProps else {
|
|
394
703
|
throw MacroExpansionErrorMessage(
|
|
395
704
|
"@ViewProps supports at most \(maxValueProps) value props (the changed-props mask is a UInt64), but this type declares \(valueProps.count); '\(valueProps[maxValueProps].name)' is the first over the limit. Event props don't count toward it"
|
|
396
705
|
)
|
|
397
706
|
}
|
|
398
|
-
return ViewPropsModel(
|
|
707
|
+
return ViewPropsModel(
|
|
708
|
+
valueProps: valueProps,
|
|
709
|
+
eventProps: eventProps,
|
|
710
|
+
eventDefaults: eventDefaults,
|
|
711
|
+
generatesInitializer: needsGeneratedInitializer(
|
|
712
|
+
structDecl,
|
|
713
|
+
in: lexicalContext,
|
|
714
|
+
eventDefaults: eventDefaults,
|
|
715
|
+
declaresUsableInitializer: declaresUsableInitializer,
|
|
716
|
+
declaresOtherInitializer: declaresOtherInitializer
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/// The members of a type body in source order, with the members of every `#if` clause in place of
|
|
722
|
+
/// the conditional declaration. The original conditions remain in the source; this is only for
|
|
723
|
+
/// discovering declarations that exist in one or more compilation configurations.
|
|
724
|
+
private func flattenedMembers(_ members: MemberBlockItemListSyntax) -> [MemberBlockItemSyntax] {
|
|
725
|
+
return members.flatMap { member -> [MemberBlockItemSyntax] in
|
|
726
|
+
guard let ifConfig = member.decl.as(IfConfigDeclSyntax.self) else {
|
|
727
|
+
return [member]
|
|
728
|
+
}
|
|
729
|
+
return ifConfig.clauses.flatMap { clause -> [MemberBlockItemSyntax] in
|
|
730
|
+
guard case .decls(let declarations) = clause.elements else {
|
|
731
|
+
return []
|
|
732
|
+
}
|
|
733
|
+
return flattenedMembers(declarations)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/// Whether Swift's own `init()` can't satisfy `AnyViewProps`, so the macro generates a public one.
|
|
739
|
+
/// Swift synthesizes `init()` only when every stored property has a default and the type declares
|
|
740
|
+
/// no initializer, and that `init()` is `internal`, which the conformance of a `public` or `package`
|
|
741
|
+
/// type can't use. The author's usable `init()` always wins, so there is never a second one.
|
|
742
|
+
private func needsGeneratedInitializer(
|
|
743
|
+
_ structDecl: StructDeclSyntax,
|
|
744
|
+
in lexicalContext: [Syntax],
|
|
745
|
+
eventDefaults: [EventDefault],
|
|
746
|
+
declaresUsableInitializer: Bool,
|
|
747
|
+
declaresOtherInitializer: Bool
|
|
748
|
+
) -> Bool {
|
|
749
|
+
if declaresUsableInitializer {
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
return !eventDefaults.isEmpty || isAboveInternal(structDecl, in: lexicalContext) || declaresOtherInitializer
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/// True if the type's access is `public`, `open` or `package`: written on the type, or, when the type
|
|
756
|
+
/// has no access modifier, taken from the extension that directly contains it
|
|
757
|
+
/// (`public extension Outer { struct Props {} }`). Only that extension sets the default; an enclosing
|
|
758
|
+
/// type doesn't, so a type nested in a `public` type without a modifier is `internal`.
|
|
759
|
+
private func isAboveInternal(_ structDecl: StructDeclSyntax, in lexicalContext: [Syntax]) -> Bool {
|
|
760
|
+
if let access = accessLevelModifier(in: structDecl.modifiers) {
|
|
761
|
+
return isAboveInternal(access)
|
|
762
|
+
}
|
|
763
|
+
guard let enclosingExtension = lexicalContext.first?.as(ExtensionDeclSyntax.self),
|
|
764
|
+
let access = accessLevelModifier(in: enclosingExtension.modifiers)
|
|
765
|
+
else {
|
|
766
|
+
return false
|
|
767
|
+
}
|
|
768
|
+
return isAboveInternal(access)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/// The access level modifier of a declaration, without a `(set)` detail, which only limits a setter.
|
|
772
|
+
private func accessLevelModifier(in modifiers: DeclModifierListSyntax) -> TokenKind? {
|
|
773
|
+
return modifiers.first { modifier in
|
|
774
|
+
guard modifier.detail == nil else {
|
|
775
|
+
return false
|
|
776
|
+
}
|
|
777
|
+
switch modifier.name.tokenKind {
|
|
778
|
+
case .keyword(.public), .keyword(.open), .keyword(.package), .keyword(.internal), .keyword(.fileprivate),
|
|
779
|
+
.keyword(.private):
|
|
780
|
+
return true
|
|
781
|
+
default:
|
|
782
|
+
return false
|
|
783
|
+
}
|
|
784
|
+
}?.name.tokenKind
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private func isAboveInternal(_ access: TokenKind) -> Bool {
|
|
788
|
+
return access == .keyword(.public) || access == .keyword(.open) || access == .keyword(.package)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/// True if the binding has storage: no accessor block, or only `willSet`/`didSet` observers.
|
|
792
|
+
private func isStoredBinding(_ binding: PatternBindingSyntax) -> Bool {
|
|
793
|
+
guard let accessorBlock = binding.accessorBlock else {
|
|
794
|
+
return true
|
|
795
|
+
}
|
|
796
|
+
guard case .accessors(let accessors) = accessorBlock.accessors else {
|
|
797
|
+
return false
|
|
798
|
+
}
|
|
799
|
+
return accessors.allSatisfy { accessor in
|
|
800
|
+
accessor.accessorSpecifier.tokenKind == .keyword(.willSet)
|
|
801
|
+
|| accessor.accessorSpecifier.tokenKind == .keyword(.didSet)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/// The errors for the `private` and `fileprivate` stored properties of a declaration that have no
|
|
806
|
+
/// initial value. `static`, `class` and `lazy` properties are exempt, and so is an optional `var`
|
|
807
|
+
/// written as `T?` or `T!`, which defaults to `nil`.
|
|
808
|
+
private func unsetPrivatePropertyDiagnostics(of varDecl: VariableDeclSyntax) -> [Diagnostic] {
|
|
809
|
+
guard
|
|
810
|
+
let access = varDecl.modifiers.first(where: {
|
|
811
|
+
$0.name.tokenKind == .keyword(.private) || $0.name.tokenKind == .keyword(.fileprivate)
|
|
812
|
+
}),
|
|
813
|
+
!varDecl.modifiers.contains(where: {
|
|
814
|
+
$0.name.tokenKind == .keyword(.static) || $0.name.tokenKind == .keyword(.class)
|
|
815
|
+
|| $0.name.tokenKind == .keyword(.lazy)
|
|
816
|
+
})
|
|
817
|
+
else {
|
|
818
|
+
return []
|
|
819
|
+
}
|
|
820
|
+
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
821
|
+
return varDecl.bindings.compactMap { binding in
|
|
822
|
+
guard binding.initializer == nil, isStoredBinding(binding),
|
|
823
|
+
let ident = binding.pattern.as(IdentifierPatternSyntax.self),
|
|
824
|
+
!(isVar && resolvedTypeAnnotation(of: binding, in: varDecl).map { hasImplicitNilDefault($0.type) } == true)
|
|
825
|
+
else {
|
|
826
|
+
return nil
|
|
827
|
+
}
|
|
828
|
+
return unsetPrivatePropertyDiagnostic(for: ident, access: access.name.text)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/// The binding's own type annotation, or the trailing annotation shared with preceding bindings.
|
|
833
|
+
private func resolvedTypeAnnotation(
|
|
834
|
+
of binding: PatternBindingSyntax,
|
|
835
|
+
in varDecl: VariableDeclSyntax
|
|
836
|
+
) -> TypeAnnotationSyntax? {
|
|
837
|
+
if let typeAnnotation = binding.typeAnnotation {
|
|
838
|
+
return typeAnnotation
|
|
839
|
+
}
|
|
840
|
+
var foundBinding = false
|
|
841
|
+
for candidate in varDecl.bindings {
|
|
842
|
+
if foundBinding, let typeAnnotation = candidate.typeAnnotation {
|
|
843
|
+
return typeAnnotation
|
|
844
|
+
}
|
|
845
|
+
foundBinding = candidate.id == binding.id
|
|
846
|
+
}
|
|
847
|
+
return nil
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/// True if the function type returns `Void`, written as `Void`, `Swift.Void` or `()`, in any number
|
|
851
|
+
/// of enclosing parentheses, such as `(Void)` or `((()))`.
|
|
852
|
+
private func returnsVoid(_ functionType: FunctionTypeSyntax) -> Bool {
|
|
853
|
+
return isVoidType(functionType.returnClause.type)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private func isVoidType(_ type: TypeSyntax) -> Bool {
|
|
857
|
+
let type = withoutParentheses(type)
|
|
858
|
+
if let tuple = type.as(TupleTypeSyntax.self) {
|
|
859
|
+
return tuple.elements.isEmpty
|
|
860
|
+
}
|
|
861
|
+
return ["Void", "Swift.Void"].contains(type.trimmedDescription)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/// The type inside any number of unlabeled one-element parentheses: `(T)` and `((T))` are `T`. A
|
|
865
|
+
/// label would make it a tuple, which Swift rejects with a single element anyway.
|
|
866
|
+
private func withoutParentheses(_ type: TypeSyntax) -> TypeSyntax {
|
|
867
|
+
guard let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1, let only = tuple.elements.first,
|
|
868
|
+
only.firstName == nil
|
|
869
|
+
else {
|
|
870
|
+
return type
|
|
871
|
+
}
|
|
872
|
+
return withoutParentheses(only.type)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/// True if the binding is computed with a `mutating get`.
|
|
876
|
+
private func hasMutatingGetter(_ binding: PatternBindingSyntax) -> Bool {
|
|
877
|
+
guard case .accessors(let accessors) = binding.accessorBlock?.accessors else {
|
|
878
|
+
return false
|
|
879
|
+
}
|
|
880
|
+
return accessors.contains { accessor in
|
|
881
|
+
accessor.accessorSpecifier.tokenKind == .keyword(.get)
|
|
882
|
+
&& accessor.modifiers.contains { $0.name.tokenKind == .keyword(.mutating) }
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/// A closure literal that takes `arity` arguments, ignores them and does nothing.
|
|
887
|
+
private func emptyClosure(arity: Int) -> String {
|
|
888
|
+
if arity == 0 {
|
|
889
|
+
return "{}"
|
|
890
|
+
}
|
|
891
|
+
return "{ \(Array(repeating: "_", count: arity).joined(separator: ", ")) in }"
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/// True if Swift gives a `var` of this type an implicit `nil` initial value, so `Self()` can leave it
|
|
895
|
+
/// out. Only the `T?` and `T!` sugar does, also in parentheses (`(T?)`): a property written as
|
|
896
|
+
/// `Optional<T>` has no default.
|
|
897
|
+
private func hasImplicitNilDefault(_ type: TypeSyntax) -> Bool {
|
|
898
|
+
let type = withoutParentheses(type)
|
|
899
|
+
return type.is(OptionalTypeSyntax.self) || type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self)
|
|
399
900
|
}
|
|
400
901
|
|
|
401
902
|
/// The function type a props property declares, unwrapping any number of enclosing parentheses and an
|
|
@@ -433,8 +934,9 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
|
|
|
433
934
|
// MARK: - Synthesized members
|
|
434
935
|
|
|
435
936
|
/// The `PropName` enum: one case per value prop, `String`-backed so the raw value is the wire key.
|
|
436
|
-
/// Emitted even when empty
|
|
437
|
-
///
|
|
937
|
+
/// Emitted even when empty, so a props type with only events still conforms. An enum with no cases
|
|
938
|
+
/// can't declare a raw type, so the empty one conforms to `RawRepresentable` by hand, with `String`
|
|
939
|
+
/// as its `RawValue` like the others.
|
|
438
940
|
private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
439
941
|
// An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
|
|
440
942
|
// the wire key visible in the generated source and independent of that implicit rule.
|
|
@@ -448,7 +950,15 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
448
950
|
.joined(separator: "\n")
|
|
449
951
|
if valueProps.isEmpty {
|
|
450
952
|
return """
|
|
451
|
-
public enum PropName:
|
|
953
|
+
public enum PropName: RawRepresentable, CaseIterable {
|
|
954
|
+
public init?(rawValue: String) {
|
|
955
|
+
return nil
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
public var rawValue: String {
|
|
959
|
+
switch self {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
452
962
|
}
|
|
453
963
|
"""
|
|
454
964
|
}
|
|
@@ -503,6 +1013,72 @@ private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
|
|
|
503
1013
|
"""
|
|
504
1014
|
}
|
|
505
1015
|
|
|
1016
|
+
/// The body of `_decode`, which core calls once per changed prop. React sends a removed prop as
|
|
1017
|
+
/// `null`, so `null` and `undefined` reset the prop to its value in a fresh `Self()`; any other value
|
|
1018
|
+
/// decodes through `JavaScriptDecodable`, with the type inferred from the assignment. The helper is a
|
|
1019
|
+
/// static member of `AnyViewProps`, reached through `Self.`, so a prop named `_decodeViewProp`
|
|
1020
|
+
/// doesn't shadow it. The checks run
|
|
1021
|
+
/// once, before the switch, so each case is a single assignment. Members are reached through `self.`
|
|
1022
|
+
/// so that a prop named like a parameter (`value`, `runtime`) isn't shadowed by it.
|
|
1023
|
+
///
|
|
1024
|
+
/// With no value prop the body is empty: `PropName` is uninhabited, so the function is never called.
|
|
1025
|
+
///
|
|
1026
|
+
/// `defaults` is a `var` only when a prop has a `mutating get`, which can't be read from a constant.
|
|
1027
|
+
/// Otherwise it's a `let`, so the common case gets no "never mutated" warning.
|
|
1028
|
+
private func decodeFunctionBody(valueProps: [ViewProp]) -> String {
|
|
1029
|
+
if valueProps.isEmpty {
|
|
1030
|
+
return ""
|
|
1031
|
+
}
|
|
1032
|
+
let defaultsSpecifier = valueProps.contains(where: \.hasMutatingGetter) ? "var" : "let"
|
|
1033
|
+
let resetCases =
|
|
1034
|
+
valueProps
|
|
1035
|
+
.map { " case .\($0.name):\n self.\($0.name) = defaults.\($0.name)" }
|
|
1036
|
+
.joined(separator: "\n")
|
|
1037
|
+
let decodeCases =
|
|
1038
|
+
valueProps
|
|
1039
|
+
.map { " case .\($0.name):\n self.\($0.name) = try Self._decodeViewProp(value, in: runtime)" }
|
|
1040
|
+
.joined(separator: "\n")
|
|
1041
|
+
// Starts with a line break, so the empty body closes on the next line instead of leaving a blank
|
|
1042
|
+
// one.
|
|
1043
|
+
return """
|
|
1044
|
+
|
|
1045
|
+
if value.isNull() || value.isUndefined() {
|
|
1046
|
+
\(defaultsSpecifier) defaults = Self()
|
|
1047
|
+
switch name {
|
|
1048
|
+
\(resetCases)
|
|
1049
|
+
}
|
|
1050
|
+
return
|
|
1051
|
+
}
|
|
1052
|
+
switch name {
|
|
1053
|
+
\(decodeCases)
|
|
1054
|
+
}
|
|
1055
|
+
"""
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/// The public `init()` that `AnyViewProps` requires, when Swift's own can't satisfy it: it gives each
|
|
1059
|
+
/// event prop without an initial value an empty closure, and leaves the value props at their initial
|
|
1060
|
+
/// values. `nil` when the type declares a usable `init()`, or when Swift's `init()` is enough (see
|
|
1061
|
+
/// `needsGeneratedInitializer`).
|
|
1062
|
+
private func defaultInitializer(props: ViewPropsModel) -> DeclSyntax? {
|
|
1063
|
+
if !props.generatesInitializer {
|
|
1064
|
+
return nil
|
|
1065
|
+
}
|
|
1066
|
+
if props.eventDefaults.isEmpty {
|
|
1067
|
+
return """
|
|
1068
|
+
public init() {
|
|
1069
|
+
}
|
|
1070
|
+
"""
|
|
1071
|
+
}
|
|
1072
|
+
let assignments = props.eventDefaults
|
|
1073
|
+
.map { " self.\($0.name) = \($0.emptyClosure)" }
|
|
1074
|
+
.joined(separator: "\n")
|
|
1075
|
+
return """
|
|
1076
|
+
public init() {
|
|
1077
|
+
\(raw: assignments)
|
|
1078
|
+
}
|
|
1079
|
+
"""
|
|
1080
|
+
}
|
|
1081
|
+
|
|
506
1082
|
/// The nested spelling of core's one generic diff, so a view writes `MyProps.Diff`.
|
|
507
1083
|
private func diffTypealias() -> DeclSyntax {
|
|
508
1084
|
return """
|