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.
@@ -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 two requirements that can only be written
60
- /// per-props: `allProps` (every bit set, the changed set on the first application) and
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, and an empty `switch` over an uninhabited
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
- "@Field is no longer used — @ViewProps treats every stored property as a prop. Remove the @Field attribute",
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. Retained for the decode surface, which lands with the
276
- /// core props contract.
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, and more than 64 value props.
299
- private func validatedViewProps(of declaration: some DeclGroupSyntax) throws -> ViewPropsModel {
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
- // `@Field` is the v1 property wrapper and has no meaning here — every stored property is already
325
- // part of the surface. Left in place it would wrap the value in `Field<T>`, so the props type
326
- // would decode against the wrong type. Flag it rather than emit code built on it.
327
- if let fieldAttribute = varDecl.attributes.firstAttribute(named: "Field") {
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
- throw DiagnosticsError(diagnostics: [
371
- optionalEventDiagnostic(for: declaredType, name: name)
372
- ])
650
+ propDiagnostics.append(optionalEventDiagnostic(for: declaredType, name: name))
651
+ continue
373
652
  }
374
653
 
375
- let isEvent = declaredType.map { underlyingFunctionType(of: $0) != nil } ?? false
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
- let prop = ViewProp(name: name, type: resolvedType)
385
- if isEvent {
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
- } else {
388
- valueProps.append(prop)
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(valueProps: valueProps, eventProps: eventProps)
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 (an uninhabited enum is legal and keeps the conformance's associated type
437
- /// satisfied), so a props type with only events still conforms.
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: String, CaseIterable {
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 """