Skip to content

[bot] Fix Rails/DuplicateAssociation#217

Closed
6[bot] wants to merge 1 commit intomainfrom
fix/rails-duplicate_association-23577367093
Closed

[bot] Fix Rails/DuplicateAssociation#217
6[bot] wants to merge 1 commit intomainfrom
fix/rails-duplicate_association-23577367093

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 26, 2026

Status: Agent is working on this fix...

Cop: Rails/DuplicateAssociation | Backend: codex / hard | Model: gpt-5.4 (xhigh) | Mode: fix
Code bugs: 1 | Run: https://github.com/6/nitrocop/actions/runs/23577367093

Refs #163

Task prompt (4854 tokens)

Fix Rails/DuplicateAssociation — 77 FP, 0 FN

Instructions

You are fixing ONE cop in nitrocop, a Rust Ruby linter that uses Prism for parsing.

Current state: 114 matches, 77 false positives, 0 false negatives.
Focus on: FP (nitrocop flags code RuboCop does not).

⚠ 114 existing matches must not regress. Validate with check_cop.py before committing.

Workflow

  1. Read the Pre-diagnostic Results and Corpus FP/FN Examples sections below first
  2. Verify with RuboCop first (for FP fixes): before writing any code, confirm RuboCop's
    behavior on BOTH the specific FP case AND the general pattern:
    echo '<specific FP case>' > /tmp/test.rb && rubocop --only Rails/DuplicateAssociation /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Rails/DuplicateAssociation /tmp/test.rb
    If RuboCop flags the general pattern, your fix must be narrow enough to not suppress it.
  3. Add a test case FIRST:
    • FN fix: add the missed pattern to tests/fixtures/cops/rails/duplicate_association/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/rails/duplicate_association/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::rails::duplicate_association
  5. Fix src/cop/rails/duplicate_association.rs
  6. Verify test passes: cargo test --lib -- cop::rails::duplicate_association
  7. Validate against corpus (REQUIRED before committing):
    python3 scripts/check_cop.py Rails/DuplicateAssociation --rerun --clone --sample 15
    If this reports FP or FN regression, your fix is too broad — narrow it down.
  8. Add a /// doc comment on the cop struct documenting what you found and fixed
  9. Commit only your cop's files

Fixture Format

Mark offenses with ^ markers on the line AFTER the offending source line.
The ^ characters must align with the offending columns. The message format is Rails/DuplicateAssociation: <message text>.
See the Current Fixture sections below for real examples from this cop.

IMPORTANT: This is a config/context issue, NOT a detection bug

Pre-diagnostic shows nitrocop already detects all FP/FN patterns correctly in isolation.
The corpus mismatches are caused by configuration differences in target repos.

Do NOT loop trying to fix detection logic — the detection code is correct.

Instead:

  1. Investigate why the cop doesn't fire (FN) or fires incorrectly (FP) in the target
    repo's config context. Common causes:
    • Include/Exclude patterns in the cop's config not matching the file path
    • The cop being disabled by the target repo's .rubocop.yml
    • # rubocop:disable comments in the source file
    • File path patterns (e.g., spec files excluded by default)
  2. Look at src/config/ for how config affects this cop
  3. If you can fix the config resolution, do so. Otherwise document your findings as a
    /// comment on the cop struct and commit what you have.

If your test passes immediately

If you add a test case and it passes without code changes, the corpus mismatch is
caused by config/context differences, not a detection bug.
Do NOT loop trying to make the test fail. Instead:

  1. Investigate config resolution (Include/Exclude, cop enablement, disable comments)
  2. The fix is likely in src/config/ or the cop's config handling, not detection logic
  3. If you cannot determine the root cause within 5 minutes, document your findings as
    a /// comment on the cop struct and commit

CRITICAL: Avoid regressions in the opposite direction

When fixing FPs, your change MUST NOT suppress legitimate detections. When fixing FNs,
your change MUST NOT flag code that RuboCop accepts. A fix that eliminates a few issues
in one direction but introduces hundreds in the other is a catastrophic regression.

Before exempting a category of patterns, verify with RuboCop that the general case
is still an offense:

rubocop --only Rails/DuplicateAssociation /tmp/test.rb

If RuboCop flags the general pattern but not your specific case, the difference is in
a narrow context (e.g., enclosing structure, receiver type, argument count) — your fix
must target that specific context, not the broad category.

Rule of thumb: if your fix adds an early return or continue that skips a whole
node type, operator class, or naming pattern, it's probably too broad. Prefer adding a
condition that matches the SPECIFIC differentiating context.

Rules

  • Only modify src/cop/rails/duplicate_association.rs and tests/fixtures/cops/rails/duplicate_association/
  • Run cargo test --lib -- cop::rails::duplicate_association to verify your fix (do NOT run the full test suite)
  • Run python3 scripts/check_cop.py Rails/DuplicateAssociation --rerun --clone --sample 15 before committing to catch regressions
  • Do NOT touch unrelated files
  • Do NOT use git stash

Current Fixture: offense.rb

tests/fixtures/cops/rails/duplicate_association/offense.rb

class User < ApplicationRecord
  has_many :posts
  ^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `posts` is defined multiple times. Don't repeat associations.
  has_many :posts, dependent: :destroy
  ^^^^^^^^ Rails/DuplicateAssociation: Association `posts` is defined multiple times. Don't repeat associations.
end

class Post < ApplicationRecord
  belongs_to :author
  ^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `author` is defined multiple times. Don't repeat associations.
  belongs_to :author, optional: true
  ^^^^^^^^^^ Rails/DuplicateAssociation: Association `author` is defined multiple times. Don't repeat associations.
end

class Company < ApplicationRecord
  has_one :address
  ^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `address` is defined multiple times. Don't repeat associations.
  has_one :address, dependent: :destroy
  ^^^^^^^ Rails/DuplicateAssociation: Association `address` is defined multiple times. Don't repeat associations.
end

# has_and_belongs_to_many duplicates
class Tag < ApplicationRecord
  has_and_belongs_to_many :articles
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `articles` is defined multiple times. Don't repeat associations.
  has_and_belongs_to_many :articles
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `articles` is defined multiple times. Don't repeat associations.
end

# String argument instead of symbol
class Invoice < ApplicationRecord
  has_many 'items'
  ^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `items` is defined multiple times. Don't repeat associations.
  has_many 'items', dependent: :destroy
  ^^^^^^^^ Rails/DuplicateAssociation: Association `items` is defined multiple times. Don't repeat associations.
end

# class_name duplicate detection (has_many, not belongs_to)
class Account < ApplicationRecord
  has_many :foos, class_name: 'Foo'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `class_name: 'Foo'` is defined multiple times. Don't repeat associations.
  has_many :bars, class_name: 'Foo'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `class_name: 'Foo'` is defined multiple times. Don't repeat associations.
end

# class_name duplicate detection (has_one)
class Profile < ApplicationRecord
  has_one :baz, class_name: 'Bar'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `class_name: 'Bar'` is defined multiple times. Don't repeat associations.
  has_one :qux, class_name: 'Bar'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `class_name: 'Bar'` is defined multiple times. Don't repeat associations.
end

# belongs_to in if/else branches (lorint/brick pattern)
class IfBranchModel < ActiveRecord::Base
  if ActiveRecord.version >= Gem::Version.new('5.0')
    belongs_to :person, optional: true
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `person` is defined multiple times. Don't repeat associations.
  else
    belongs_to :person
    ^^^^^^^^^^^^^^^^^^ Rails/DuplicateAssociation: Association `person` is defined multiple times. Don't repeat associations.
  end
end

Current Fixture: no_offense.rb

tests/fixtures/cops/rails/duplicate_association/no_offense.rb

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_one :profile, dependent: :destroy
  belongs_to :company
end

# Serializer classes should not be flagged
class CollectionSerializer < ActivityPub::Serializer
  has_many :items, key: :items, if: -> { condition_a }
  has_many :items, key: :ordered_items, if: -> { condition_b }
end

# has_and_belongs_to_many without duplicates
class Category < ApplicationRecord
  has_and_belongs_to_many :posts
  has_and_belongs_to_many :tags
end

# belongs_to with same class_name is NOT flagged
class Order < ApplicationRecord
  belongs_to :foos, class_name: 'Foo'
  belongs_to :bars, class_name: 'Foo'
end

# class_name with extra options is NOT flagged
class Report < ApplicationRecord
  has_many :foos, if: :condition, class_name: 'Foo'
  has_many :bars, if: :some_condition, class_name: 'Foo'
  has_one :baz, -> { condition }, class_name: 'Bar'
  has_one :qux, -> { some_condition }, class_name: 'Bar'
end

Key Source Files

  • Rust implementation: src/cop/rails/duplicate_association.rs
  • RuboCop Ruby source (ground truth): vendor/rubocop-rails/lib/rubocop/cop/rails/duplicate_association.rb
  • RuboCop test excerpts: vendor/rubocop-rails/spec/rubocop/cop/rails/duplicate_association_spec.rb

Read these files before making changes.

Start Here

Use the existing corpus data to focus on the most concentrated regressions first.

Helpful local commands:

  • python3 scripts/investigate_cop.py Rails/DuplicateAssociation --repos-only
  • python3 scripts/investigate_cop.py Rails/DuplicateAssociation --context
  • python3 scripts/verify_cop_locations.py Rails/DuplicateAssociation

Top FP repos:

  • voormedia__rails-erd__7c66258 (38 FP) — example examples/applications/gemcutter/models/user.rb:3
  • lorint__brick__fa07b7f (26 FP) — example spec/models/northwind_spec.rb:25
  • gisiahq__gisia__b7f70e0 (4 FP) — example app/models/commit_status.rb:47

Representative FP examples:

  • databasically__lowdown__d593927: vendor/rails/activerecord/test/models/author.rb:11 — Association class_name: "Post" is defined multiple times. Don't repeat associations.
  • gisiahq__gisia__b7f70e0: app/models/commit_status.rb:47 — Association ci_stage is defined multiple times. Don't repeat associations.
  • gisiahq__gisia__b7f70e0: app/models/commit_status.rb:50 — Association needs is defined multiple times. Don't repeat associations.

Pre-diagnostic Results

Diagnosis Summary

Each example was tested by running nitrocop on the extracted source in isolation
with --force-default-config to determine if the issue is a code bug or config issue.
Note: source context is truncated and may not parse perfectly. If a diagnosis
seems wrong (e.g., your test passes immediately for a 'CODE BUG'), treat it as
a config/context issue instead.

  • FP: 0 confirmed code bug(s), 14 context-dependent
  • Omitted 1 pre-diagnostic FP example(s) with no source context because diagnosed FP examples were available

FP #1: databasically__lowdown__d593927: vendor/rails/activerecord/test/models/author.rb:11

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

  has_many :posts_with_comments, :include => :comments, :class_name => "Post"
  has_many :popular_grouped_posts, :include => :comments, :class_name => "Post", :group => "type", :having => "SUM(comments_count) > 1", :select => "type"
  has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id'
  has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1
  has_many :posts_with_categories, :include => :categories, :class_name => "Post"
  has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post"
  has_many :posts_containing_the_letter_a, :class_name => "Post"
  has_many :posts_with_extension, :class_name => "Post" do #, :extend => ProxyTestExtension
    def testing_proxy_owner
      proxy_owner
    end
    def testing_proxy_reflection
      proxy_reflection
    end
    def testing_proxy_target

Message: Association class_name: "Post" is defined multiple times. Don't repeat associations.

FP #2: gisiahq__gisia__b7f70e0: app/models/commit_status.rb:47

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

  belongs_to :project, inverse_of: :builds
  belongs_to :pipeline,
    class_name: 'Ci::Pipeline',
    foreign_key: :commit_id,
    inverse_of: :statuses
  belongs_to :user
  belongs_to :runner, optional: true
  belongs_to :ci_stage,
    class_name: 'Ci::Stage',
    foreign_key: :stage_id
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
  has_many :taggings, class_name: 'Ci::BuildTag',
    foreign_key: :build_id,
    inverse_of: :build
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build

Message: Association ci_stage is defined multiple times. Don't repeat associations.

FP #3: gisiahq__gisia__b7f70e0: app/models/commit_status.rb:50

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

    foreign_key: :commit_id,
    inverse_of: :statuses
  belongs_to :user
  belongs_to :runner, optional: true
  belongs_to :ci_stage,
    class_name: 'Ci::Stage',
    foreign_key: :stage_id
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
  has_many :taggings, class_name: 'Ci::BuildTag',
    foreign_key: :build_id,
    inverse_of: :build
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build

  has_many :tags,
    class_name: 'Ci::Tag',

Message: Association needs is defined multiple times. Don't repeat associations.

FP #4: gisiahq__gisia__b7f70e0: app/models/commit_status.rb:54

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

  belongs_to :ci_stage,
    class_name: 'Ci::Stage',
    foreign_key: :stage_id
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
  has_many :taggings, class_name: 'Ci::BuildTag',
    foreign_key: :build_id,
    inverse_of: :build
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build

  has_many :tags,
    class_name: 'Ci::Tag',
    through: :taggings,
    source: :tag
  belongs_to :ci_stage,
    class_name: 'Ci::Stage',

Message: Association needs is defined multiple times. Don't repeat associations.

FP #5: gisiahq__gisia__b7f70e0: app/models/commit_status.rb:60

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

    inverse_of: :build
  has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build

  has_many :tags,
    class_name: 'Ci::Tag',
    through: :taggings,
    source: :tag
  belongs_to :ci_stage,
    class_name: 'Ci::Stage',
    foreign_key: :stage_id

  attribute :retried, default: false
  alias_method :author, :user
  alias_attribute :pipeline_id, :commit_id

Message: Association ci_stage is defined multiple times. Don't repeat associations.

FP #6: lorint__brick__fa07b7f: spec/models/northwind_spec.rb:25

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

      # And then just added :children_firstname to the uniques,
      # and the six column aliases in the :as portion.

      # validates_presence_of :hire_date

      # Can't do required: false in AR < 4.2
      if ActiveRecord.version < Gem::Version.new('4.2')
        belongs_to :reports_to, class_name: name
      else
        # Note that required: false is available from Rails 4.2 onwards, and works the same as
        # optional: true in Rails 5.0 and later.
        belongs_to :reports_to, class_name: name, required: false
      end

      has_many :orders

Message: Association reports_to is defined multiple times. Don't repeat associations.

FP #7: lorint__brick__fa07b7f: spec/models/northwind_spec.rb:29

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

      # Can't do required: false in AR < 4.2
      if ActiveRecord.version < Gem::Version.new('4.2')
        belongs_to :reports_to, class_name: name
      else
        # Note that required: false is available from Rails 4.2 onwards, and works the same as
        # optional: true in Rails 5.0 and later.
        belongs_to :reports_to, class_name: name, required: false
      end

      has_many :orders
    end
    class Customer < ActiveRecord::Base
      has_many :orders
    end

Message: Association reports_to is defined multiple times. Don't repeat associations.

FP #8: lorint__brick__fa07b7f: spec/models/restaurant_reverse_spec.rb:39

NOT REPRODUCED in isolation — CONTEXT-DEPENDENT
nitrocop does not flag this in isolation. The FP is triggered
by surrounding code context or file-level state.
Investigate what full-file context causes the false detection.

Source context:

RSpec.describe 'Restaurant', type: :model do
  # Set up Models
  # =============
  before(:all) do
    unload_class('RestaurantCategory')
    class RestaurantCategory < ActiveRecord::Base
      if ActiveRecord.version >= Gem::Version.new('5.0')
        belongs_to :parent, class_name: name, optional: true
      else
        belongs_to :parent, class_name: name
      end
      has_many :subcategories, class_name: name, foreign_key: :parent_id, dependent: :destroy
      has_many :restaurants, foreign_key: :category_id, inverse_of: :category

      # Generated by first running:  Restaurant.suggest_template(2, false, true)

Message: Association parent is defined multiple times. Don't repeat associations.

Omitted 6 additional diagnosed FP example(s) for brevity.

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 26, 2026

Agent failed. See run: https://github.com/6/nitrocop/actions/runs/23577367093

@6 6 bot closed this Mar 26, 2026
@6 6 bot deleted the fix/rails-duplicate_association-23577367093 branch March 26, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants