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
|
@@ -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 {
|
|
@@ -249,6 +285,178 @@ private func innerFunctionType(of type: TypeSyntax) -> TypeSyntax? {
|
|
|
249
285
|
return only.type
|
|
250
286
|
}
|
|
251
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
|
+
|
|
252
460
|
private struct ViewPropsDiagnosticMessage: DiagnosticMessage {
|
|
253
461
|
let message: String
|
|
254
462
|
let diagnosticID: MessageID
|
|
@@ -277,9 +485,12 @@ private struct ViewProp {
|
|
|
277
485
|
/// The property name as written, backticks included for an escaped name. Every identifier position
|
|
278
486
|
/// in the generated code uses this, since `case default` and `static let default` don't parse.
|
|
279
487
|
let name: String
|
|
280
|
-
/// The property's declared type, verbatim
|
|
281
|
-
///
|
|
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.
|
|
282
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
|
|
283
494
|
|
|
284
495
|
/// The name with any escaping backticks removed: the JS-visible key, so the enum's raw value and
|
|
285
496
|
/// `_eventNames` both read `"default"`, never `` "`default`" ``.
|
|
@@ -288,20 +499,41 @@ private struct ViewProp {
|
|
|
288
499
|
}
|
|
289
500
|
}
|
|
290
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
|
+
|
|
291
511
|
private struct ViewPropsModel {
|
|
292
512
|
/// Value props, in declaration order. The order is the bit order, so it is part of the ABI
|
|
293
513
|
/// between a compiled view and the runtime that fills the mask.
|
|
294
514
|
let valueProps: [ViewProp]
|
|
295
515
|
/// Event props, in declaration order.
|
|
296
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
|
|
297
521
|
}
|
|
298
522
|
|
|
299
523
|
/// Reads and validates the props type's stored properties, splitting them into value props and events.
|
|
300
524
|
///
|
|
301
525
|
/// Rejects what can't be expressed: a non-`struct` declaration (the class form is the SwiftUI path and
|
|
302
526
|
/// needs the observable protocol, which is deferred), a leftover `@Field` attribute, an optional
|
|
303
|
-
/// function type, a property with no determinable type,
|
|
304
|
-
|
|
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 {
|
|
305
537
|
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
|
|
306
538
|
throw MacroExpansionErrorMessage(
|
|
307
539
|
"@ViewProps can only be applied to a struct — the class form (for SwiftUI views) is not supported yet"
|
|
@@ -332,16 +564,50 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
332
564
|
throw DiagnosticsError(diagnostics: fieldDiagnostics)
|
|
333
565
|
}
|
|
334
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
|
+
|
|
335
593
|
var valueProps: [ViewProp] = []
|
|
336
594
|
var eventProps: [ViewProp] = []
|
|
595
|
+
var eventDefaults: [EventDefault] = []
|
|
337
596
|
|
|
338
597
|
for member in declaration.memberBlock.members {
|
|
339
598
|
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else {
|
|
340
599
|
continue
|
|
341
600
|
}
|
|
342
601
|
if isExcludedByModifier(varDecl.modifiers) {
|
|
602
|
+
if !declaresUsableInitializer {
|
|
603
|
+
propDiagnostics += unsetPrivatePropertyDiagnostics(of: varDecl)
|
|
604
|
+
}
|
|
343
605
|
continue
|
|
344
606
|
}
|
|
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
|
|
345
611
|
for binding in varDecl.bindings {
|
|
346
612
|
// A computed property is never a prop, but an accessor block alone doesn't mean computed:
|
|
347
613
|
// `willSet`/`didSet` observers imply stored storage. `bindingIsSettable` draws exactly that
|
|
@@ -381,12 +647,12 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
381
647
|
// dynamic, but events are registered once at view creation from a static name list, so there
|
|
382
648
|
// is no shape that could express "sometimes emitted".
|
|
383
649
|
if let declaredType, isOptionalType(declaredType), underlyingFunctionType(of: declaredType) != nil {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
])
|
|
650
|
+
propDiagnostics.append(optionalEventDiagnostic(for: declaredType, name: name))
|
|
651
|
+
continue
|
|
387
652
|
}
|
|
388
653
|
|
|
389
|
-
let
|
|
654
|
+
let eventFunctionType = declaredType.flatMap(underlyingFunctionType)
|
|
655
|
+
let isEvent = eventFunctionType != nil
|
|
390
656
|
// `PropSet` stores its mask in `rawValue`, so a value prop of that name would emit a static
|
|
391
657
|
// member shadowing it and the option set would not compile ("circular reference"). The error
|
|
392
658
|
// would point at generated code, so catch it here and name the property.
|
|
@@ -395,13 +661,42 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
395
661
|
"'rawValue' can't be used as a prop name — it collides with the synthesized PropSet's storage. Rename the property"
|
|
396
662
|
)
|
|
397
663
|
}
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
678
|
eventProps.append(prop)
|
|
401
|
-
|
|
402
|
-
|
|
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)
|
|
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))
|
|
403
689
|
}
|
|
690
|
+
valueProps.append(prop)
|
|
404
691
|
}
|
|
692
|
+
if !letValuePropNames.isEmpty {
|
|
693
|
+
propDiagnostics.insert(letValuePropDiagnostic(for: varDecl, names: letValuePropNames), at: letDiagnosticIndex)
|
|
694
|
+
}
|
|
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)
|
|
405
700
|
}
|
|
406
701
|
|
|
407
702
|
guard valueProps.count <= maxValueProps else {
|
|
@@ -409,7 +704,199 @@ private func validatedViewProps(of declaration: some DeclGroupSyntax) throws ->
|
|
|
409
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"
|
|
410
705
|
)
|
|
411
706
|
}
|
|
412
|
-
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)
|
|
413
900
|
}
|
|
414
901
|
|
|
415
902
|
/// The function type a props property declares, unwrapping any number of enclosing parentheses and an
|
|
@@ -447,8 +934,9 @@ private func underlyingFunctionType(of type: TypeSyntax) -> FunctionTypeSyntax?
|
|
|
447
934
|
// MARK: - Synthesized members
|
|
448
935
|
|
|
449
936
|
/// The `PropName` enum: one case per value prop, `String`-backed so the raw value is the wire key.
|
|
450
|
-
/// Emitted even when empty
|
|
451
|
-
///
|
|
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.
|
|
452
940
|
private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
453
941
|
// An escaped case takes its raw value from the bare identifier already, but spelling it out keeps
|
|
454
942
|
// the wire key visible in the generated source and independent of that implicit rule.
|
|
@@ -462,7 +950,15 @@ private func propNameEnum(valueProps: [ViewProp]) -> DeclSyntax {
|
|
|
462
950
|
.joined(separator: "\n")
|
|
463
951
|
if valueProps.isEmpty {
|
|
464
952
|
return """
|
|
465
|
-
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
|
+
}
|
|
466
962
|
}
|
|
467
963
|
"""
|
|
468
964
|
}
|
|
@@ -517,6 +1013,72 @@ private func eventNamesConstant(eventProps: [ViewProp]) -> DeclSyntax {
|
|
|
517
1013
|
"""
|
|
518
1014
|
}
|
|
519
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
|
+
|
|
520
1082
|
/// The nested spelling of core's one generic diff, so a view writes `MyProps.Diff`.
|
|
521
1083
|
private func diffTypealias() -> DeclSyntax {
|
|
522
1084
|
return """
|