Skip to content

Don't infer a constant from a polymorphic association - #68

Merged
dduugg merged 1 commit into
mainfrom
fix-polymorphic-association-references
Oct 2, 2026
Merged

dduugg merged 1 commit into
mainfrom
fix-polymorphic-association-references

Conversation

@dduugg

@dduugg dduugg commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #19.

pks inferred a class from an association's name unless class_name: was given, so belongs_to :event, polymorphic: true counted as a reference to Event. When another pack defined Event, that showed up as a privacy or dependency violation for code that never names it.

Rails reads a polymorphic association's class from a type column (event_type by default) at runtime, so the declaration references no constant. get_reference_from_active_record_association now returns no reference when the options include a literal polymorphic: true. Both parsers go through that function, so both are fixed.

Where to look

  • class_name: is dropped too when polymorphic: true is set. pks follows Rails 8.1, which rejects that option there (:class_name should be invalid in polymorphic belongs_to rails/rails#55089). Earlier versions accept it and use it only to load the class for counter_cache:. pks doesn't count that load as a reference. The code comment and CHANGELOG say so. With counter_cache:, 8.1 still loads the class named after the association, and pks ignores that too.
  • The inherited_resources gem's belongs_to constantizes the class for each symbol, polymorphic or not (class_methods.rb in 2.1.0). So the reference is kept where that method may be running: in a class or module whose name ends in Controller, or outside any class or module. The second case covers ActiveAdmin's register and controller do blocks, which pass their options through to it. An Active Record model declares its associations inside its class, so neither case keeps a model's reference.
  • Only a literal true counts. polymorphic: false, nil, or a variable still infer the class as before.
  • Constants inside a scope lambda are still reported, because the visitor still walks the call's arguments.
  • custom_associations such as cache_belongs_to get the same treatment, as they already do for class_name:.

packwerk

packwerk's AssociationInspector has the same bug, which is why the issue saw the same result from both tools. So this is a deliberate difference from packwerk, and it's listed under "Behavioral differences" in the README. That section's intro now says some differences are deliberate.

A package_todo.yml entry recorded for one of these associations is no longer found, so pks check reports it as stale until pks update runs. check-unused-dependencies may also report a dependency that only such an association used. The CHANGELOG entry covers both.

Not covered

  • with_options polymorphic: true do belongs_to :event end still reports Event, since the option isn't on the call itself.
  • A braced hash (belongs_to :event, { polymorphic: true }) or **opts still reports Event, the same limitation class_name: already has.
  • Controllers are recognized by name. An inherited_resources belongs_to with polymorphic: true in a concern, an abstract base controller not named *Controller, or a Class.new(InheritedResources::Base) block no longer reports the class. A model whose name ends in Controller still reports it, as on main.
  • pks reads only the first symbol, so belongs_to :post, :event, polymorphic: true in a controller reports only Post, as on main.

Test plan

  • Unit tests (packwerk parser) for polymorphic: true, class_name: with polymorphic: true, a scope lambda with polymorphic: true, polymorphic: false, a controller, a plain class nested inside a controller, and an ActiveAdmin register block.
  • Unit test (experimental parser) for polymorphic: true.
  • test_check_ignores_polymorphic_association runs pks check on a fixture model with belongs_to :event, polymorphic: true, where Event is private to another pack. On main it reports Privacy violation: `::Event` is private to `packs/baz` . It also asserts an empty stderr, so a renamed fixture can't make it pass by matching no file.
  • On main's parse_utils.rs, the plain, class_name:, scope, and experimental unit tests fail, as does the integration test. Treating only controllers as exceptions fails the ActiveAdmin test, and checking the outermost namespace instead of the innermost fails the nested-class test.
  • Checked by hand with both parsers: hash-rocket and quoted keys, a multi-line call, and a custom association all drop the reference. polymorphic: nil, a variable, and a controller (including a namespaced one) keep it.
  • Recorded a todo entry for the fixture's association: check reports it stale, update removes it, and check is then clean.
  • cargo test (323 passed), fmt, and clippy with -Dwarnings.
  • CI passes.

@dduugg
dduugg requested a review from a team as a code owner October 2, 2026 00:20
@dduugg
dduugg force-pushed the fix-polymorphic-association-references branch from 2b0ceaa to 3907509 Compare October 2, 2026 00:58
get_reference_from_active_record_association inferred a class from the
association's name unless `class_name:` was given, so
`belongs_to :event, polymorphic: true` was read as a reference to
`Event`. When another pack defined `Event`, that produced privacy and
dependency violations for code that never names it (#19).

Rails reads a polymorphic association's class from its `_type` column
at runtime, so the declaration references no constant. The function now
returns no reference when the options include a literal
`polymorphic: true`, even when `class_name:` is also given. Rails 8.1
rejects `class_name:` there (rails/rails#55089). Earlier versions accept
it and use it only to load the class for `counter_cache:`, which pks
doesn't count as a reference. The code comment and CHANGELOG say so.

InheritedResources' `belongs_to` constantizes the class for each symbol,
polymorphic or not, so the reference is kept where that method may be
running: in a class or module whose name ends in `Controller`, or
outside any class or module. The second case covers ActiveAdmin's
`register` and `controller do` blocks, which pass their options through
to it. An Active Record model declares its associations inside its
class, so neither case drops a model's reference.

`polymorphic: false`, `nil`, or a non-literal value still infer a class
as before, and constants inside a scope lambda are still collected by
the visitor. Both parsers share this function, so both are fixed, and it
applies to custom_associations too. `with_options polymorphic: true`
blocks are not handled.

packwerk reports these references too, so this is a new difference from
it, noted in the README. A package_todo.yml entry recorded for one of
these associations is now reported as stale until `pks update` is run,
and check-unused-dependencies may report a dependency that only such an
association used. The CHANGELOG entry covers both.

Fixes #19
@dduugg
dduugg force-pushed the fix-polymorphic-association-references branch from 3907509 to c04446c Compare October 2, 2026 01:29
@dduugg
dduugg merged commit 97a2c87 into main Oct 2, 2026
15 checks passed
@dduugg
dduugg deleted the fix-polymorphic-association-references branch October 2, 2026 16:17
@dduugg dduugg mentioned this pull request Oct 2, 2026
7 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

False positive Packwerk violation for polymorphic belongs_to :event association

1 participant