Skip to content

1.5.0

Latest

Choose a tag to compare

@yysskk yysskk released this 13 Aug 13:45
b05fc75

This release makes the macro tell you, at the declaration, about the shapes it cannot copy — an init accessor, an opaque type, a property that expands a parameter pack, one declared inside #if, one named after its own type, and a class that is not final — each of which previously surfaced, if at all, as an error inside generated code or as a silently wrong copy. The generated method also now caps its access level at the least visible property it copies, the rule Swift applies to a struct's memberwise initializer, so a copy can never vary state from somewhere that state is hidden.

Added

  • An error on a property with an init accessor. Such a property is computed and was skipped as one, but it stands in for the storage it initializes in the memberwise initializer, so the generated call passed that storage and left the property out — "extra argument" and "missing argument" at once, inside generated code. Copying such a type means routing through the accessor, which only its @storageRestrictions describes, so it is reported instead. Macros that generate init accessors, @Observable among them, are unaffected: each macro sees the declaration as written rather than what another expands it into.
  • An error on a stored property named after the type it belongs to, such as a Money in struct Money. The generated method takes a parameter of that name and the call that builds the copy sits in its body, so Money(Money: …) read Money as the parameter and failed with "cannot call value of non-function type", pointing inside generated code. Renaming the property is the one fix that leaves every supported declaration working: building the copy with Self(…) instead would need a required initializer on a class the macro has just advised to be final.
  • An error on a stored property with an opaque type, such as var value: some Equatable. Written in a parameter, some P declares a fresh generic parameter for the caller to choose rather than naming the property's own type, so the generated code did not type-check. An any P existential is a type in its own right and is copied as before.
  • An error on a stored property whose type expands a parameter pack, such as let values: (repeat each T). A copy is built by passing each property to an initializer, and Swift does not compile such a call for a value that expands a pack — with a memberwise initializer or one written by hand alike — so a type that stores one needs a copying written by hand. Types generic over a pack are otherwise copied as usual.
  • An error on a stored property declared inside #if. A macro is handed the declaration before the directives are resolved, so such a property was invisible and the generated call simply left it out: on the configurations where its branch is active the expansion failed to compile, or — when the initializer defaults that parameter — silently reset the property on every copy. Every clause is checked, #elseif, #else, and nested directives included. A member inside #if that a copy never carries anyway, such as a computed, static, or lazy property or a let with an initial value, is still skipped silently.
  • A warning on a class that is not final, with a Fix-It that marks it. copying builds the copy by calling the annotated type's own initializer and returns it as that type, so a subclass inherited a method that rebuilt only the superclass: its own stored properties were dropped and the dynamic type changed, with nothing in the language to catch it. The Fix-It writes final after any modifier the class already carries and demotes an open class to public final, since open and final contradict each other. A struct and an actor cannot be subclassed and are never flagged. It is a warning rather than an error because a class nothing subclasses copies itself correctly.

Changed

  • The generated copying method now caps its access level at the least visible property it copies, the rule Swift applies to a struct's memberwise initializer. It previously took the annotated type's level alone, so a public type with an internal or private property got a public method — which either handed out what the property's access level withholds, letting any module build a copy that varies it, or did not compile at all, because a public method cannot take a parameter of a less visible type. The type's own level counts as before: open yields public, and a private type yields fileprivate. A private property is the one reversal — it keeps the method private, which reaches the type declaration and its extensions in the same file, exactly as far as the property does. A modifier that constrains only the setter, such as private(set), does not lower the cap, since copying reads the property rather than assigning through it, and neither do the members the method does not copy. The initializer the Fix-It writes carries the same capped level, and a type declared internal explicitly now yields a method with no modifier spelled out — the same level, written the way a declaration carries it by default. To keep a public copying method, declare the copied properties public.

Fixed

  • Correct the documentation of let constants with an initial value. They were described as fixed, which holds for a literal but not for an expression such as UUID() or Date(): a copy is a new instance and runs that expression again, so it does not share the original's value. The documentation now says so and shows how to preserve the value instead, by dropping the initial value and supplying the default through an initializer.
  • Stop accepting a variadic parameter for a copied property's argument label. An initializer such as init(id: Int, tags: String...) passed the check for a tags: [String] property, and the generated call then failed to compile — Swift has no array-to-variadic splat — with no warning to say what was missing. A variadic is now only ever an extra parameter the call leaves out, and the macro names the initializer it needs.
  • Match initializer labels to escaped property names. A property declared var `default`: String never matched init(default value: String), the way Swift spells that label, so the macro warned about an initializer that was right there — and its Fix-It inserted a second init(default:), an invalid redeclaration. Labels are now compared as identifiers, and the warning spells the signature the way Swift writes a compound name, init(default:). Generated code and the Fix-It's initializer keep the backticks the language requires, while the documentation the macro writes names each parameter as its label, without them.
  • Spell a parameter pack in the generated return type as an expansion. A type such as struct Bundle<each T> produced -> Bundle<T>, which Swift rejects with "pack reference 'T' can only appear in pack expansion", so the expansion never compiled; the return type now reads Bundle<repeat each T>. A plain generic parameter, constrained or not, keeps its bare name as before.
  • No more false "missing initializer" warning when the initializer is declared inside #if. The initializer was invisible for the same reason a conditional property was, so the macro reported one as missing and offered a Fix-It that wrote a second initializer with the signature the first one already had. Any conditionally compiled initializer now silences the check, since a macro cannot know which branch a build takes.
  • Spell the copying parameter of an implicitly unwrapped optional property, such as var label: UILabel!, as a plain optional. Swift only accepts ! at the top level of a property's or a parameter's type, so the (UILabel!)? parameter generated before did not compile. It denotes the same type as (UILabel?)?, so the parameter takes the same arguments and the property stays implicitly unwrapped in the copy. The initializer the Fix-It writes is a top-level position and keeps the declared UILabel!.
  • Accept bindings that share one type annotation, such as var x, y: Int. Swift attaches the annotation to the last binding only, so every preceding one was reported as missing a type and the expansion failed on code that compiles. A binding now takes the annotation of a later binding in the same declaration, stopping at the first one with an initial value — which types itself by inference, and which Swift rejects sharing an annotation with anyway (var x, y: Int = 0).

Full Changelog: 1.4.0...1.5.0