Skip to content

Retain initialized constant properties flagged as assign-only#1136

Open
danwood wants to merge 1 commit into
peripheryapp:masterfrom
danwood:fix-assignonly-init-retained
Open

Retain initialized constant properties flagged as assign-only#1136
danwood wants to merge 1 commit into
peripheryapp:masterfrom
danwood:fix-assignonly-init-retained

Conversation

@danwood

@danwood danwood commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Symptom

A let stored property that is assigned in an explicit initializer but never read is reported as an assign-only property, even though removing it is not safe.

Real-world example

public struct NavigationItem: Identifiable, Hashable {
    public let id = UUID()
    public let title: String
    public let color: Color?

    public init(title: String, color: Color? = nil) {
        self.title = title
        self.color = color
    }
}

// Used elsewhere:
NavigationItem(title: "Designs", color: .accentColor)

Here color is read only through SwiftUI's synthesized body / Hashable / Identifiable machinery, which the index does not attribute as a read. It is therefore reported as assign-only. Scanning a real SwiftUI app surfaced hundreds of such properties.

Why it's a false positive

Acting on the report by removing the declaration breaks the build:

  • the self.color = color assignment in the initializer body is left without a target, and
  • the color: initializer parameter is dropped, breaking every call site that passes it.

A let can only be assigned once, in an initializer body, so when that initializer is written explicitly and is actually used, the property is a genuine stored value that simply isn't read — not an assign-only property.

Fix

When a property would otherwise be reported as assign-only, it is instead retained if it is a let constant initialized by an explicit initializer that is itself referenced.

The exclusion is deliberately narrow so existing detection is unchanged:

  • a let initialized only by a compiler-synthesized memberwise initializer is still reported (removing it is safe — the synthesizer simply stops generating the parameter);
  • a let whose initializer is never called is still reported;
  • var assign-only detection is untouched.

To distinguish a let from a var, the declaration visitor now records whether each variable declaration is a let binding, plumbed through the indexer onto the declaration.

A regression test (testRetainsInitializedConstantProperties) covers both the retained case (used initializer) and the still-reported case (unused initializer).

A `let` stored property whose value is supplied by an explicit initializer
that is itself used was incorrectly reported as an assign-only property when
it was never read. Such a property is a genuine stored value: removing its
declaration would orphan the `self.x = x` assignment in the initializer body
and drop the matching initializer parameter, breaking every call site that
supplies the argument.

This commonly arises with model types whose properties are read only through
synthesized code the index does not attribute as a read, such as a struct
conforming to Codable or Identifiable, or a value consumed by a SwiftUI view
body. The property is now retained rather than reported.

The exclusion is deliberately narrow. A `let` initialized only by a
compiler-synthesized memberwise initializer remains removable, as does one
whose initializer is never called, so existing assign-only detection for both
`let` and `var` properties is unchanged.

To distinguish a `let` constant from a `var`, the declaration visitor now
records whether each variable declaration is a `let` binding, plumbed through
the indexer onto the declaration.
@danwood danwood marked this pull request as ready for review June 15, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant