Skip to content

Add new Rails/ActiveStorageAttachedDependent cop to flag silent no-op dependent: values #1612

Description

@melody-universe

🤖 This issue and the followup PR were written with some heavy assistance from Claude, but I have personally reviewed and edited both.

Is your feature request related to a problem? Please describe.

Rails accepts a dependent: keyword on most association macros, and the conventional values across has_many, has_one, and belongs_to are mostly interchangeable: :destroy, :delete_all, :nullify, etc. Developers absorb the rule "every association takes dependent: :destroy" and apply it uniformly.

ActiveStorage's has_one_attached and has_many_attached accept the keyword too, but the contract is narrower: only :purge_later actually triggers the purge. Rails stores the value verbatim, but the consumer ActiveStorage::Attachment#purge_dependent_blob_later checks for strict equality on :purge_later. Anything else (true, false, nil, :destroy, arbitrary symbols) leaves orphan blobs in storage when the parent record is destroyed. false is runtime-equivalent to the rest, but in my opinion, we should allow it because it's an intuitive way to express "opt out of dependent behavior." Open to suggestions here (and of course, anywhere else).

The footgun is that the kwarg is accepted — no exception, no deprecation warning, no log line — so the bug surfaces only as gradual storage bloat. Copy-pasting from an adjacent has_many where dependent: :destroy is correct is a common way to introduce it. As one example in the wild, a-thousand-channels/ORTE-backend currently declares three has_one_attached attachments with dependent: :destroy, all of which are silent no-ops; the project is small enough that the storage cost is negligible, but the same pattern in larger applications produces orphaned blobs that accumulate indefinitely.

Describe the solution you'd like

A new cop in the Rails department, Rails/ActiveStorageAttachedDependent, that flags has_one_attached / has_many_attached declarations whose dependent: value is anything other than :purge_later or false.

dependent: value result
omitted / :purge_later / false OK
:purge warning (rails/rails#36423)
anything else (true, nil, :destroy, arbitrary symbols) error

The cop does not autocorrect. Every offending value is runtime-equivalent — they all fail the consumer's strict dependent == :purge_later check — so the right replacement depends on the user's intent (did they mean to opt out, use the default, or something else?). :purge is reported at warning severity per-offense because the documentation claims it works while the consumer disagrees.

Examples

# bad — silent no-op
has_one_attached :avatar, dependent: true

# bad — silent no-op
has_many_attached :photos, dependent: :destroy

# bad — documented but not honored, see rails/rails#36423
has_one_attached :avatar, dependent: :purge

# good
has_one_attached :avatar
has_one_attached :avatar, dependent: :purge_later
has_one_attached :avatar, dependent: false

Describe alternatives you've considered

  • Upstream fix in Rails. ActiveStorage::Attached::Model could validate dependent: at declaration time, or the consumer could be relaxed to honor :purge. The Rails issue has been open since 2019; a lint catches the existing footgun without depending on the upstream change.
  • Documentation-only note. A docs callout doesn't help existing offenders or prevent future regressions.
  • Project-local custom cop. I prototyped this internally; reviewer feedback was that the rule is generic and belongs upstream, where every Rails project benefits.

Additional context

Implementation in #1613 — registers the cop in config/default.yml as Enabled: pending and ships RSpec coverage for each branch (true / nil / :purge / arbitrary symbol / has_many_attached variant / documented-allowed values / omitted kwarg) plus regenerated documentation.

See also the Rails Active Storage Overview guide.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions