Skip to content

[bot] Fix Style/ArrayIntersect#897

Open
6[bot] wants to merge 4 commits intomainfrom
fix/style-array_intersect-23730406918
Open

[bot] Fix Style/ArrayIntersect#897
6[bot] wants to merge 4 commits intomainfrom
fix/style-array_intersect-23730406918

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 30, 2026

Automated fix fix for Style/ArrayIntersect corpus conformance.

Refs #321

Details

Cop Style/ArrayIntersect
Backend claude-oauth / hard
Model Claude Opus 4.6 (OAuth, high)
Mode fix
Turns 39
Run #453

Result

Done. Here's what was fixed:

**Problem:** Style/ArrayIntersect had 170 false negatives — it only detected `(a & b).any?`/`.empty?`/`.none?` but missed size comparison patterns and ActiveSupport predicates.

**Changes:**
1. **Size comparisons** — `(a & b).count > 0`, `.size > 0`, `.length > 0` (and `== 0`, `!= 0`)
2. **Size predicates** — `(a & b).count.positive?`, `.count.zero?` (and size/length)
3. **ActiveSupport** — `(a & b).present?`, `.blank?` (when `ActiveSupportExtensionsEnabled: true`)
4. All patterns work with both `(a & b)` and `a.intersection(b)` forms

**Results:** 41 FN resolved, 0 FP regressions, all 636 existing matches preserved.

Changes

  1 files changed
Agent conversation excerpt

Let me start by reading the key files.

Read: /home/runner/work/nitrocop/nitrocop/docs/agent-ci.md

Read: /home/runner/work/nitrocop/nitrocop/src/cop/style/array_intersect.rs

Read: /home/runner/work/nitrocop/nitrocop/vendor/rubocop/lib/rubocop/cop/style/array_intersect.rb

Read: /home/runner/work/nitrocop/nitrocop/vendor/rubocop/spec/rubocop/cop/style/array_intersect_spec.rb

Read: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/array_intersect/offense.rb

Read: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/array_intersect/no_offense.rb

The test fixtures already have FN cases added. Let me verify they fail, then implement the fix.

Bash: cargo test --lib -- cop::style::array_intersect 2>&1 | tail -30

Now let me check how ActiveSupportExtensionsEnabled is accessed in the codebase.

Grep: ActiveSupportExtensionsEnabled

Grep: fn get_bool

Good. Now let me also check how IntegerNode is accessed to look at integer literal values in the AST.

Grep: as_integer_node

Grep: integer_node.*value

Grep: int_node\)\.value

Grep: int_node.*as_slice

Let me check how the test utility parses fixture markers.

Grep: fn parse_fixture

Let me check how the test assertion compares offenses.

Read: /home/runner/work/nitrocop/nitrocop/src/testutil.rs

Read: /home/runner/work/nitrocop/nitrocop/src/testutil.rs

Grep: fn try_parse_annotation

Now I have a clear picture. The current implementation only detects any?/empty?/none? on intersection results. It's missing:

  1. Size comparisons: (a & b).count > 0, == 0, != 0 (also size, length)
  2. Size predicates: (a & b).count.positive?, .zero?
  3. ActiveSupport: (a & b).present?, .blank?

Let me update the fixture first with proper test cases, then implement the detection.

Write

Write

Now let me implement the detection logic.

Write

Now let me check if run_cop_full and run_cop_full_with_config exist.

Grep: pub fn run_cop_full

Good. Now let me run the tests.

Bash: cargo test --lib -- cop::style::array_intersect 2>&1 | tail -40

All 8 tests pass. Now let me run the corpus validation.

Bash: cargo build --release 2>&1 | tail -5

Bash: python3 scripts/check_cop.py Style/ArrayIntersect --rerun --clone --sample 15 2>&1

Good - 41 FN resolved, 0 regressions. Let me investigate the remaining FN to see if more can be fixed.

Bash: python3 scripts/investigate_cop.py Style/ArrayIntersect --context 2>&1 | head -100

Bash: python3 scripts/investigate_cop.py Style/ArrayIntersect --context 2>&1 | grep -oP '\[Use [^\]]+\]' | sed 's/.*instead of //;s/\.\]//' | sed 's/(.*)/EXPR/' | sort | uniq -c | sort -rn | head -20

Bash: python3 scripts/investigate_cop.py Style/ArrayIntersect --context 2>&1 | grep -B1 "size > 0\|count > 0\|length > 0" | head -30

Bash: python3 scripts/verify_cop_locations.py Style/ArrayIntersect 2>&1 | head -50

These FN should be caught now. Let me test a specific example directly.

Bash: echo '(sentence.downcase.split & subject.split).size > 0' | ./target/release/nitrocop --stdin --force-default-config --only Style/ArrayIntersect 2>&1

Bash: echo '(sentence.downcase.split & subject.split).size > 0' > /tmp/test_ai.rb && ./target/release/nitrocop /tmp/test_ai.rb --force-default-config --only Style/ArrayIntersect 2>&1

The pattern is detected. The verify script uses cached data. The check_cop with --rerun confirmed 41 resolved FN with 0 regressions.

Let me verify present?/blank? work correctly when ActiveSupport is configured:

Bash: `echo '(a & b).present?' > /tmp/test_ai.rb && ./target/release/nitrocop /tmp/test_ai.rb --force-default-config --only Style/ArrayIntersect 2>&1
echo '---'

Test with a rubocop config that enables Acti`

Bash: mkdir -p /tmp/test_as && echo '(a & b).present?' > /tmp/test_as/test.rb && echo '---' > /tmp/test_as/.rubocop.yml.lock cat > /tmp/test_as/.rubocop.yml << 'EOF' AllCops: ActiveSupportExtensionsEnable

Bash: # Test the specific CoralineAda examples from the FN list echo '(sentence.downcase.split & subject.split).size > 0' > /tmp/test_ai.rb && ./target/release/nitrocop /tmp/test_ai.rb --force-default-confi

All patterns correctly detected. The check_cop passed with 41 resolved FN and 0 regressions. Let me verify the full test suite one more time.

Bash: cargo test --lib -- cop::style::array_intersect 2>&1 | tail -15

All 8 tests pass. Now let me commit.

Bash: git status

Bash: git diff --stat

Bash: git log --oneline -5

Bash: git add src/cop/style/array_intersect.rs tests/fixtures/cops/style/array_intersect/offense.rb tests/fixtures/cops/style/array_intersect/no_offense.rb && git commit -m "$(cat <<'EOF' Fix Style/ArrayInt

Done. Here's what was fixed:

Problem: Style/ArrayIntersect had 170 false negatives — it only detected (a & b).any?/.empty?/.none? but missed size comparison patterns and ActiveSupport predicates.

Changes:

  1. Size comparisons(a & b).count > 0, .size > 0, .length > 0 (and == 0, != 0)
  2. Size predicates(a & b).count.positive?, .count.zero? (and size/length)
  3. ActiveSupport(a & b).present?, .blank? (when ActiveSupportExtensionsEnabled: true)
  4. All patterns work with both (a & b) and a.intersection(b) forms

Results: 41 FN resolved, 0 FP regressions, all 636 existing matches preserved.

Task prompt (6376 tokens)

Fix Style/ArrayIntersect — 0 FP, 170 FN

Instructions

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

Current state: 636 matches, 0 false positives, 170 false negatives.
Focus on: FN (RuboCop flags code nitrocop misses).

⚠ 636 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 Style/ArrayIntersect /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Style/ArrayIntersect /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/style/array_intersect/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/style/array_intersect/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::style::array_intersect
  5. Fix src/cop/style/array_intersect.rs
  6. Verify test passes: cargo test --lib -- cop::style::array_intersect
  7. Validate against corpus (REQUIRED before committing):
    python3 scripts/check_cop.py Style/ArrayIntersect --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 Style/ArrayIntersect: <message text>.
See the Current Fixture sections below for real examples from this cop.

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 Style/ArrayIntersect /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/style/array_intersect.rs and tests/fixtures/cops/style/array_intersect/
  • Run cargo test --lib -- cop::style::array_intersect to verify your fix (do NOT run the full test suite)
  • Run python3 scripts/check_cop.py Style/ArrayIntersect --rerun --clone --sample 15 before committing to catch regressions
  • Do NOT touch unrelated files
  • Do NOT use git stash
  • Do NOT push — you do not have push permission; the workflow handles pushing after you exit

Current Fixture: offense.rb

tests/fixtures/cops/style/array_intersect/offense.rb

(array1 & array2).any?
^^^^^^^^^^^^^^^^^^^^^^ Style/ArrayIntersect: Use `intersect?` instead of `(array1 & array2).any?`.

(a & b).none?
^^^^^^^^^^^^^ Style/ArrayIntersect: Use `intersect?` instead of `(a & b).none?`.

a.intersection(b).any?
^^^^^^^^^^^^^^^^^^^^^^ Style/ArrayIntersect: Use `intersect?` instead of `intersection(...).any?`.

Current Fixture: no_offense.rb

tests/fixtures/cops/style/array_intersect/no_offense.rb

array1.intersect?(array2)
(array1 & array2).any? { |x| false }
(array1 & array2).any?(&:block)
array1.intersection.any?
array1.intersection(array2, array3).any?
alpha & beta

# These are fine as standalone operations
(array1 & array2).size
(array1 & array2).length
(array1 & array2).count

Key Source Files

  • Rust implementation: src/cop/style/array_intersect.rs
  • RuboCop Ruby source (ground truth): vendor/rubocop/lib/rubocop/cop/style/array_intersect.rb
  • RuboCop test excerpts: vendor/rubocop/spec/rubocop/cop/style/array_intersect_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 Style/ArrayIntersect --repos-only
  • python3 scripts/investigate_cop.py Style/ArrayIntersect --context
  • python3 scripts/verify_cop_locations.py Style/ArrayIntersect

Top FN repos:

  • otwcode__otwarchive__412f04f (19 FN)
  • discourse__discourse__9c8f125 (13 FN) — example app/controllers/invites_controller.rb:604
  • publiclab__plots2__5e1d8c2 (12 FN)

Representative FN examples:

  • CoralineAda__alice__8b7efd5: alice/context.rb:253 — Use sentence.downcase.split.intersect?(subject.split) instead of (sentence.downcase.split & subject.split).size > 0.
  • CoralineAda__alice__8b7efd5: alice/grammar/dictionary.rb:11 — Use ['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'].intersect?(thing) instead of (['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'] & thing).count > 0.
  • CoralineAda__alice__8b7efd5: alice/grammar/ngram.rb:17 — Use [gram.flatten[0]].intersect?(matches) instead of ([gram.flatten[0]] & matches).present?.

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.

  • FN: 15 code bug(s)

FN #1: CoralineAda__alice__8b7efd5: alice/context.rb:253

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 252: def near_match(subject, sentence)) > class body (line 1: class Context)

Message: Use sentence.downcase.split.intersect?(subject.split)instead of(sentence.downcase.split & subject.split).size > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

    (sentence.downcase.split & subject.split).size > 0
^ Style/ArrayIntersect: Use `sentence.downcase.split.intersect?(subject.split)` instead of `(sentence.downcase.split & subject.split).size > 0`.

Full file context (30 lines before offense):

      223:       mutex.synchronize { @content << c }
      224:     end
      225: 
      226:     threads << Thread.new() do
      227:       c = Parser::Google.fetch_all(search_string)
      228:       mutex.synchronize { @content << c }
      229:     end
      230: 
      231:     threads << Thread.new() do
      232:       c = Parser::Google.fetch_all("facts about #{topic}")
      233:       mutex.synchronize { @content << c }
      234:     end
      235: 
      236:     threads << Thread.new() do
      237:       c = Parser::Wikipedia.fetch_all(topic)
      238:       mutex.synchronize { @content << c }
      239:     end
      240: 
      241:     threads.each(&:join)
      242:     @content = @content.flatten.compact.reject(&:empty?)
      243:     @content = @content.map{ |fact| Sanitize.clean(fact.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')).strip }.uniq
      244:     @content = @content.reject{ |fact| Grammar::SentenceParser.parse(fact).verbs.empty? }
      245:     @content = @content.reject{ |fact| fact =~ /click/i || fact =~ /website/i || fact =~ /quiz/i }
      246:     @content = @content.reject{ |s| s.include?("may refer to") || s.include?("disambiguation") }
      247:     @content = @content.map{ |s| Grammar::LanguageHelper.to_third_person(s.gsub(/^\**/, "")) }
      248:     @content = @content.uniq
      249:     @content
      250:   end
      251: 
      252:   def near_match(subject, sentence)
>>>   253:     (sentence.downcase.split & subject.split).size > 0
      254:   end
      255: 
      256:   def record_spoken(fact)
      257:     return unless fact
      258:     self.spoken << fact
      259:     update_attribute(:spoken, self.spoken.uniq)
      260:   end

FN #2: CoralineAda__alice__8b7efd5: alice/grammar/dictionary.rb:11

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 10: def self.coffee_or_tea(thing)) > class body (line 2: class Dictionary) > module body (line 1: module Grammar)

Prism AST at offense line: StatementsNode > CallNode > CallNode > ParenthesesNode > StatementsNode > CallNode > ArrayNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > StringNode > ArgumentsNode > LocalVariableReadNode > ArgumentsNode > IntegerNode

Message: Use ['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'].intersect?(thing)instead of(['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'] & thing).count > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      (['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'] & thing).count > 0
^ Style/ArrayIntersect: Use `['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'].intersect?(thing)` instead of `(['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'] & thing).count > 0`.

Full file context (30 lines before offense):

        1: module Grammar
        2:   class Dictionary
        3: 
        4:     def self.is_a?(category, thing)
        5:       return :params_reversed if self.respond_to?(thing)
        6:       return :unknown unless self.respond_to?(category)
        7:       self.public_send(category, parsed_thing(thing))
        8:     end
        9: 
       10:     def self.coffee_or_tea(thing)
>>>    11:       (['cup', 'cuppa', 'pot', 'kettle', 'mug', 'coffee', 'tea', 'latte', 'cappuccino', 'joe', 'mud'] & thing).count > 0
       12:     end
       13: 
       14:     private
       15: 
       16:     def self.parsed_thing(thing)
       17:       thing.downcase.split
       18:     end

FN #3: CoralineAda__alice__8b7efd5: alice/grammar/ngram.rb:17

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 16: def with_leading(matches, args={})) > class body (line 2: class Ngram) > module body (line 1: module Grammar)

Message: Use [gram.flatten[0]].intersect?(matches)instead of([gram.flatten[0]] & matches).present?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      phrase_matches = omnigrams.reject{|h| h.flatten.is_a? String}.inject([]) { |a, gram| a << gram if ([gram.flatten[0]] & matches).present?; a}
^ Style/ArrayIntersect: Use `[gram.flatten[0]].intersect?(matches)` instead of `([gram.flatten[0]] & matches).present?`.

Full file context (30 lines before offense):

        1: module Grammar
        2:   class Ngram
        3: 
        4:     include Grammar::LanguageHelper
        5: 
        6:     attr_accessor :contents
        7: 
        8:     def initialize(grams=[])
        9:       self.contents = grams
       10:     end
       11: 
       12:     def omnigrams
       13:       self.contents.map{|gram| gram.is_a?(Array) && gram.flatten || nil}.compact
       14:     end
       15: 
       16:     def with_leading(matches, args={})
>>>    17:       phrase_matches = omnigrams.reject{|h| h.flatten.is_a? String}.inject([]) { |a, gram| a << gram if ([gram.flatten[0]] & matches).present?; a}
       18:       matches = Search::Ngram.new((phrase_matches).compact.select(&:present?).uniq)
       19:       matches = matches.minus(IDENTIFIERS) if args[:object_only]
       20:       matches = matches.to_a.flatten.uniq if args[:flatten]
       21:       matches
       22:     end
       23: 
       24:     def with(matches)

FN #4: CoralineAda__alice__8b7efd5: alice/grammar/ngram.rb:25

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 24: def with(matches)) > class body (line 2: class Ngram) > module body (line 1: module Grammar)

Message: Use gram.flatten.intersect?(matches)instead of(gram.flatten & matches).present?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      phrase_matches = omnigrams.select{|h| h.flatten.is_a? Array}.inject([]) { |a, gram| a << gram if (gram.flatten & matches).present?; a}
^ Style/ArrayIntersect: Use `gram.flatten.intersect?(matches)` instead of `(gram.flatten & matches).present?`.

Full file context (30 lines before offense):

        1: module Grammar
        2:   class Ngram
        3: 
        4:     include Grammar::LanguageHelper
        5: 
        6:     attr_accessor :contents
        7: 
        8:     def initialize(grams=[])
        9:       self.contents = grams
       10:     end
       11: 
       12:     def omnigrams
       13:       self.contents.map{|gram| gram.is_a?(Array) && gram.flatten || nil}.compact
       14:     end
       15: 
       16:     def with_leading(matches, args={})
       17:       phrase_matches = omnigrams.reject{|h| h.flatten.is_a? String}.inject([]) { |a, gram| a << gram if ([gram.flatten[0]] & matches).present?; a}
       18:       matches = Search::Ngram.new((phrase_matches).compact.select(&:present?).uniq)
       19:       matches = matches.minus(IDENTIFIERS) if args[:object_only]
       20:       matches = matches.to_a.flatten.uniq if args[:flatten]
       21:       matches
       22:     end
       23: 
       24:     def with(matches)
>>>    25:       phrase_matches = omnigrams.select{|h| h.flatten.is_a? Array}.inject([]) { |a, gram| a << gram if (gram.flatten & matches).present?; a}
       26:       Search::Ngram.new((phrase_matches).compact.select(&:present?).uniq)
       27:     end
       28: 
       29:     def without(matches)
       30:       phrase_matches = omnigrams.select{|h| h.flatten.is_a? Array}.inject([]) { |a, gram| a << gram if (gram.flatten & matches).empty?; a}
       31:       Search::Ngram.new((phrase_matches).compact.select(&:present?).uniq)
       32:     end

FN #5: CoralineAda__alice__8b7efd5: alice/grammar/ngram_factory.rb:31

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 29: def omnigram(args={})) > class body (line 2: class NgramFactory) > module body (line 1: module Grammar)

Message: Use exclude.intersect?(unigram.flatten)instead of(exclude & unigram.flatten).count > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      uni = ngrams(1).reject{|unigram| (exclude & unigram.flatten).count > 0}
^ Style/ArrayIntersect: Use `exclude.intersect?(unigram.flatten)` instead of `(exclude & unigram.flatten).count > 0`.

Full file context (30 lines before offense):

        1: module Grammar
        2:   class NgramFactory
        3: 
        4:     attr_accessor :options, :target
        5: 
        6:     def self.filtered_grams_from(words)
        7:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
        8:       filtered.reject!{|name| Grammar::LanguageHelper::IDENTIFIERS.include?(name)}
        9:       filtered.reject!(&:empty?)
       10:       new(filtered.join(' ')).omnigrams
       11:     end
       12: 
       13:     def self.unfiltered_grams_from(words)
       14:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
       15:       filtered.reject!(&:empty?)
       16:       new(filtered.join(' ')).omnigrams
       17:     end
       18: 
       19:     def self.omnigrams_from(words)
       20:       words = words.downcase.split(/[^a-zA-Z0-9\_\-]/).uniq
       21:       words << words.map{|word| Lingua.stemmer(word.downcase)}
       22:       new(words.flatten).omnigrams.to_a.flatten.uniq.reject!(&:empty?)
       23:     end
       24: 
       25:     def initialize(target)
       26:       @target = target
       27:     end
       28: 
       29:     def omnigram(args={})
       30:       exclude = [args[:exclude]].flatten.compact
>>>    31:       uni = ngrams(1).reject{|unigram| (exclude & unigram.flatten).count > 0}
       32:       bi  = ngrams(2).reject{|unigram| (exclude & unigram.flatten).count > 0}
       33:       tri = ngrams(3).reject{|unigram| (exclude & unigram.flatten).count > 0}
       34:       Grammar::Ngram.new(uni + bi + tri)
       35:     end
       36: 
       37:     def omnigrams
       38:       omnigram.omnigrams

FN #6: CoralineAda__alice__8b7efd5: alice/grammar/ngram_factory.rb:32

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 29: def omnigram(args={})) > class body (line 2: class NgramFactory) > module body (line 1: module Grammar)

Message: Use exclude.intersect?(unigram.flatten)instead of(exclude & unigram.flatten).count > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      bi  = ngrams(2).reject{|unigram| (exclude & unigram.flatten).count > 0}
^ Style/ArrayIntersect: Use `exclude.intersect?(unigram.flatten)` instead of `(exclude & unigram.flatten).count > 0`.

Full file context (30 lines before offense):

        2:   class NgramFactory
        3: 
        4:     attr_accessor :options, :target
        5: 
        6:     def self.filtered_grams_from(words)
        7:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
        8:       filtered.reject!{|name| Grammar::LanguageHelper::IDENTIFIERS.include?(name)}
        9:       filtered.reject!(&:empty?)
       10:       new(filtered.join(' ')).omnigrams
       11:     end
       12: 
       13:     def self.unfiltered_grams_from(words)
       14:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
       15:       filtered.reject!(&:empty?)
       16:       new(filtered.join(' ')).omnigrams
       17:     end
       18: 
       19:     def self.omnigrams_from(words)
       20:       words = words.downcase.split(/[^a-zA-Z0-9\_\-]/).uniq
       21:       words << words.map{|word| Lingua.stemmer(word.downcase)}
       22:       new(words.flatten).omnigrams.to_a.flatten.uniq.reject!(&:empty?)
       23:     end
       24: 
       25:     def initialize(target)
       26:       @target = target
       27:     end
       28: 
       29:     def omnigram(args={})
       30:       exclude = [args[:exclude]].flatten.compact
       31:       uni = ngrams(1).reject{|unigram| (exclude & unigram.flatten).count > 0}
>>>    32:       bi  = ngrams(2).reject{|unigram| (exclude & unigram.flatten).count > 0}
       33:       tri = ngrams(3).reject{|unigram| (exclude & unigram.flatten).count > 0}
       34:       Grammar::Ngram.new(uni + bi + tri)
       35:     end
       36: 
       37:     def omnigrams
       38:       omnigram.omnigrams
       39:     end

FN #7: CoralineAda__alice__8b7efd5: alice/grammar/ngram_factory.rb:33

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 29: def omnigram(args={})) > class body (line 2: class NgramFactory) > module body (line 1: module Grammar)

Message: Use exclude.intersect?(unigram.flatten)instead of(exclude & unigram.flatten).count > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      tri = ngrams(3).reject{|unigram| (exclude & unigram.flatten).count > 0}
^ Style/ArrayIntersect: Use `exclude.intersect?(unigram.flatten)` instead of `(exclude & unigram.flatten).count > 0`.

Full file context (30 lines before offense):

        3: 
        4:     attr_accessor :options, :target
        5: 
        6:     def self.filtered_grams_from(words)
        7:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
        8:       filtered.reject!{|name| Grammar::LanguageHelper::IDENTIFIERS.include?(name)}
        9:       filtered.reject!(&:empty?)
       10:       new(filtered.join(' ')).omnigrams
       11:     end
       12: 
       13:     def self.unfiltered_grams_from(words)
       14:       filtered = words.gsub(/[^a-zA-Z0-9\-\_]/, ' ').split.map(&:strip)
       15:       filtered.reject!(&:empty?)
       16:       new(filtered.join(' ')).omnigrams
       17:     end
       18: 
       19:     def self.omnigrams_from(words)
       20:       words = words.downcase.split(/[^a-zA-Z0-9\_\-]/).uniq
       21:       words << words.map{|word| Lingua.stemmer(word.downcase)}
       22:       new(words.flatten).omnigrams.to_a.flatten.uniq.reject!(&:empty?)
       23:     end
       24: 
       25:     def initialize(target)
       26:       @target = target
       27:     end
       28: 
       29:     def omnigram(args={})
       30:       exclude = [args[:exclude]].flatten.compact
       31:       uni = ngrams(1).reject{|unigram| (exclude & unigram.flatten).count > 0}
       32:       bi  = ngrams(2).reject{|unigram| (exclude & unigram.flatten).count > 0}
>>>    33:       tri = ngrams(3).reject{|unigram| (exclude & unigram.flatten).count > 0}
       34:       Grammar::Ngram.new(uni + bi + tri)
       35:     end
       36: 
       37:     def omnigrams
       38:       omnigram.omnigrams
       39:     end
       40: 

FN #8: CoralineAda__alice__8b7efd5: alice/handlers/conversation.rb:99

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Full-file enclosing chain: method body (line 97: def set_context_from_predicate) > class body (line 5: class Conversation) > module body (line 3: module Handlers)

Message: Use command_string.components.intersect?(Grammar::LanguageHelper::PRONOUNS)instead of(command_string.components & Grammar::LanguageHelper::PRONOUNS).count > 0.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      return if (command_string.components & Grammar::LanguageHelper::PRONOUNS).count > 0
^ Style/ArrayIntersect: Use `command_string.components.intersect?(Grammar::LanguageHelper::PRONOUNS)` instead of `(command_string.components & Grammar::LanguageHelper::PRONOUNS).count > 0`.

Full file context (30 lines before offense):

       69:       fact ||= current_context.declarative_fact(topic.pluralize, speak)
       70:       fact ||= current_context.declarative_fact(topic.singularize, speak)
       71:       fact ||= current_context.relational_fact(topic, speak)
       72:       fact ||= current_context.relational_fact(topic.pluralize, speak)
       73:       fact ||= current_context.relational_fact(topic.singularize, speak)
       74:     end
       75: 
       76:     def current_context
       77:       Context.current
       78:     end
       79: 
       80:     def subject_or_predicate
       81:       subject || predicate
       82:     end
       83: 
       84:     def subject
       85:       @subject ||= command.subject unless command.subject.is_bot?
       86:     end
       87: 
       88:     def predicate
       89:       @predicate = User.from(command.predicate)
       90:       @predicate ||= command.predicate
       91:     end
       92: 
       93:     def set_context_from_subject_or_predicate
       94:       set_context_from_predicate || set_context_from_subject
       95:     end
       96: 
       97:     def set_context_from_predicate
       98:       return unless predicate.present?
>>>    99:       return if (command_string.components & Grammar::LanguageHelper::PRONOUNS).count > 0
      100:       update_context(context_from(predicate.respond_to?(:downcase) && predicate.downcase || predicate.name.downcase))
      101:       true
      102:     end
      103: 
      104:     def set_context_from_subject
      105:       return unless subject
      106:       return if (command_string.components & Grammar::LanguageHelper::PRONOUNS).any?

Omitted 7 additional diagnosed FN example(s) for brevity.

github-actions bot and others added 2 commits March 30, 2026 06:32
…tterns

Add detection for (a & b).count/size/length > 0 / == 0 / != 0,
.count.positive? / .zero?, and .present? / .blank? (with ActiveSupport).
Resolves 41 FN with 0 regressions (636 existing matches preserved).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@6 6 bot force-pushed the fix/style-array_intersect-23730406918 branch from 2dce9b1 to 0b3996f Compare March 30, 2026 06:34
@6 6 bot marked this pull request as ready for review March 30, 2026 06:34
@6 6 bot enabled auto-merge (squash) March 30, 2026 06:34
@github-actions
Copy link
Copy Markdown
Contributor

Cop-check results (8 shards)

Cop Baseline FP Baseline FN Local FP Local FN FP Δ FN Δ Result
Style/ArrayIntersect 0 17 0 13 0 -4
Style/ArrayIntersect 0 31 0 23 0 -8
Style/ArrayIntersect 0 26 0 20 0 -6
Style/ArrayIntersect 0 21 0 16 0 -5
Style/ArrayIntersect 0 24 0 14 0 -10
Style/ArrayIntersect 0 25 0 23 0 -2
Style/ArrayIntersect 0 8 0 5 0 -3
Style/ArrayIntersect 0 18 0 15 0 -3

All shards passed — ready to merge.

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 30, 2026

Auto-repair Started

  • Checks run: #23731277359
  • Route: easy
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Reason: build-and-test: Clippy
  • Repair workflow: #23731426577
Task prompt (4672 tokens)

PR Repair Task: PR #897

Before making changes, read docs/agent-ci.md.

Repair the existing PR branch in place. Do not create a new branch or PR.
Preserve the intent of the current PR and make the smallest changes needed to fix the failing checks.
Do not repair this PR by reverting it back to origin/main, deleting the whole diff, or otherwise turning it into an empty/no-op PR.
If the only plausible fix is a full revert of the PR, stop and explain that clearly instead of performing the revert.
Do not edit unrelated files or do broad cleanup.

Context

  • PR: [bot] Fix Style/ArrayIntersect #897 [bot] Fix Style/ArrayIntersect
  • Branch: fix/style-array_intersect-23730406918
  • Checks run: #1905 (Checks)
  • Route: easy
  • Selected backend: codex / hard
  • Failure summary: build-and-test: Clippy

Required Outcome

Fix the currently failing checks shown below.
Before finishing, run the targeted validations needed to make the workflow's final verification likely to pass.

Required Local Verification

The workflow will rerun these commands before pushing. Your changes should make them pass:

cargo clippy --profile ci -- -D warnings

Current PR Diff Stat

src/cop/style/array_intersect.rs                   | 275 ++++++++++++++++++++-
 .../cops/style/array_intersect/no_offense.rb       |   7 +
 .../fixtures/cops/style/array_intersect/offense.rb |  27 ++
 3 files changed, 297 insertions(+), 12 deletions(-)

Current PR Diff Excerpt

diff --git a/src/cop/style/array_intersect.rs b/src/cop/style/array_intersect.rs
index d1f95f178..47c780f24 100644
--- a/src/cop/style/array_intersect.rs
+++ b/src/cop/style/array_intersect.rs
@@ -3,8 +3,70 @@ use crate::cop::{Cop, CopConfig};
 use crate::diagnostic::Diagnostic;
 use crate::parse::source::SourceFile;
 
+/// Style/ArrayIntersect detects array intersection patterns replaceable
+/// with `Array#intersect?` (Ruby 3.1+).
+///
+/// Handles three families of patterns:
+/// 1. Direct predicates: `(a & b).any?` / `.empty?` / `.none?`
+///    (plus `.present?` / `.blank?` when `ActiveSupportExtensionsEnabled`)
+/// 2. Size comparisons: `(a & b).count > 0`, `== 0`, `!= 0`
+///    (also `.size` and `.length`)
+/// 3. Size predicates: `(a & b).count.positive?`, `.count.zero?`
+///
+/// All patterns also match the `a.intersection(b)` form (1 argument only).
 pub struct ArrayIntersect;
 
+/// Extract (lhs_source, rhs_source) from an intersection expression node.
+/// Matches `(a & b)` (parenthesized `&` call) and `a.intersection(b)` (1-arg form).
+fn extract_intersection_parts(node: &ruby_prism::Node<'_>) -> Option<(String, String)> {
+    // (a & b) form
+    if let Some(paren) = node.as_parentheses_node() {
+        if let Some(body) = paren.body() {
+            if let Some(stmts) = body.as_statements_node() {
+                let stmt_list: Vec<_> = stmts.body().iter().collect();
+                if stmt_list.len() == 1 {
+                    if let Some(inner_call) = stmt_list[0].as_call_node() {
+                        let m = std::str::from_utf8(inner_call.name().as_slice()).unwrap_or("");
+                        if m == "&" {
+                            let recv = inner_call.receiver()?;
+                            let args = inner_call.arguments()?;
+                            let arg_list: Vec<_> = args.arguments().iter().collect();
+                            if arg_list.len() == 1 {
+                                let lhs = std::str::from_utf8(recv.location().as_slice())
+                                    .unwrap_or("")
+                                    .to_string();
+                                let rhs = std::str::from_utf8(arg_list[0].location().as_slice())
+                                    .unwrap_or("")
+                                    .to_string();
+                                return Some((lhs, rhs));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    // a.intersection(b) form
+    if let Some(call) = node.as_call_node() {
+        let m = std::str::from_utf8(call.name().as_slice()).unwrap_or("");
+        if m == "intersection" {
+            let recv = call.receiver()?;
+            let args = call.arguments()?;
+            let arg_list: Vec<_> = args.arguments().iter().collect();
+            if arg_list.len() == 1 {
+                let lhs = std::str::from_utf8(recv.location().as_slice())
+                    .unwrap_or("")
+                    .to_string();
+                let rhs = std::str::from_utf8(arg_list[0].location().as_slice())
+                    .unwrap_or("")
+                    .to_string();
+                return Some((lhs, rhs));
+            }
+        }
+    }
+    None
+}
+
 impl Cop for ArrayIntersect {
     fn name(&self) -> &'static str {
         "Style/ArrayIntersect"
@@ -40,8 +102,12 @@ impl Cop for ArrayIntersect {
 
         let method_name = std::str::from_utf8(call.name().as_slice()).unwrap_or("");
 
-        // Pattern: (array1 & array2).any? / .empty? / .none?
-        if matches!(method_name, "any?" | "empty?" | "none?") {
+        let active_support = config.get_bool("ActiveSupportExtensionsEnabled", false);
+
+        // Pattern 1: (a & b).any? / .empty? / .none? / .present? / .blank?
+        if matches!(method_name, "any?" | "empty?" | "none?")
+            || (active_support && matches!(method_name, "present?" | "blank?"))
+        {
             // Skip if the call has arguments or a block (any? with block)
             if call.arguments().is_some() || call.block().is_some() {
                 return;
@@ -62,12 +128,35 @@ impl Cop for ArrayIntersect {
                                         let loc = node.location();
                                         let (line, column) =
                                             source.offset_to_line_col(loc.start_offset());
-                                        let msg = format!(
-                                            "Use `intersect?` instead of `({}).{}`.",
-                                            std::str::from_utf8(inner_call.location().as_slice())
-                                                .unwrap_or("array1 & array2"),
-                                            method_name
-                                        );
+
+                                        // Keep backward-compatible message for original patterns
+                                        let msg =
+                                            if matches!(method_name, "any?" | "empty?" | "none?") {
+                                                format!(
+                                                    "Use `intersect?` instead of `({}).{}`.",
+                                                    std::str::from_utf8(
+                                                        inner_call.location().as_slice()
+                                                    )
+                                                    .unwrap_or("array1 & array2"),
+                                                    method_name
+                                                )
+                                            } else if let Some((lhs, rhs)) =
+                                                extract_intersection_parts(&receiver)
+                                            {
+                                                let existing = std::str::from_utf8(loc.as_slice())
+                                                    .unwrap_or("");
+                                                format!(
+                                                    "Use `{}.intersect?({})` instead of `{}`.",
+                                                    lhs, rhs, existing
+                                                )
+                                            } else {
+                                                let existing = std::str::from_utf8(loc.as_slice())
+                                                    .unwrap_or("");
+                                                format!(
+                                                    "Use `intersect?` instead of `{}`.",
+                                                    existing
+                                                )
+                                            };
                                         diagnostics
                                             .push(self.diagnostic(source, line, column, msg));
                                     }
@@ -90,10 +179,27 @@ impl Cop for ArrayIntersect {
                                     let loc = node.location();
                                     let (line, column) =
                                         source.offset_to_line_col(loc.start_offset());
-                                    let msg = format!(
-                                        "Use `intersect?` instead of `intersection(...).{}`.",
-                                        method_name
-                                    );
+
+                                    let msg = if matches!(method_name, "any?" | "empty?" | "none?")
+                                    {
+                                        format!(
+                                            "Use `intersect?` instead of `intersection(...).{}`.",
+                                            method_name
+                                        )
+                                    } else if let Some((lhs, rhs)) =
+                                        extract_intersection_parts(&receiver)
+                                    {
+                                        let existing =
+                                            std::str::from_utf8(loc.as_slice()).unwrap_or("");
+                                        format!(
+                                            "Use `{}.intersect?({})` instead of `{}`.",
+                                            lhs, rhs, existing
+                                        )
+                                    } else {
+                                        let existing =
+                                            std::str::from_utf8(loc.as_slice()).unwrap_or("");
+                                        format!("Use `intersect?` instead of `{}`.", existing)
+                                    };
                                     diagnostics.push(self.diagnostic(source, line, column, msg));
                                 }
                             }
@@ -102,6 +208,82 @@ impl Cop for ArrayIntersect {
                 }
             }
         }
+
+        // Pattern 2: (a & b).count > 0 / == 0 / != 0
+        if matches!(method_name, ">" | "==" | "!=") {
+            if let Some(args) = call.arguments() {
+                let arg_list: Vec<_> = args.arguments().iter().collect();
+                if arg_list.len() == 1 {
+                    if let Some(int_node) = arg_list[0].as_integer_node() {
+                        if int_node.location().as_slice() == b"0" {
+                            if let Some(recv) = call.receiver() {
+                                if let Some(recv_call) = recv.as_call_node() {
+                                    let rm = std::str::from_utf8(recv_call.name().as_slice())
+                                        .unwrap_or("");
+                                    if matches!(rm, "count" | "size" | "length") {
+                                        if recv_call.arguments().is_none()
+                                            && recv_call.block().is_none()
+                                        {
+                                            if let Some(inner_recv) = recv_call.receiver() {
+                                                if let Some((lhs, rhs)) =
+                                                    extract_intersection_parts(&inner_recv)
+                                                {
+                                                    let loc = node.location();
+                                                    let (line, column) = source
+                                                        .offset_to_line_col(loc.start_offset());
+                                                    let existing =
+                                                        std::str::from_utf8(loc.as_slice())
+                                                            .unwrap_or("");
+                                                    let msg = format!(
+                                                        "Use `{}.intersect?({})` instead of `{}`.",
+                                                        lhs, rhs, existing
+                                                    );
+                                                    diagnostics.push(
+                                                        self.diagnostic(source, line, column, msg),
+                                                    );
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Pattern 3: (a & b).count.positive? / .zero?
+        if matches!(method_name, "positive?" | "zero?") {
+            if call.arguments().is_none() && call.block().is_none() {
+                if let Some(recv) = call.receiver() {
+                    if let Some(recv_call) = recv.as_call_node() {
+                        let rm = std::str::from_utf8(recv_call.name().as_slice()).unwrap_or("");
+                        if matches!(rm, "count" | "size" | "length") {
... (151 more lines truncated)

Failed Checks Packet

build-and-test

  • Route: easy
  • Failed steps: Clippy
build-and-test	Clippy	2026-03-30T06:35:37.8035915Z ##[group]Run cargo clippy --profile ci -- -D warnings
cargo clippy --profile ci -- -D warnings
shell: /usr/bin/bash -e {0}
env:
CARGO_HOME: /home/runner/.cargo
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
CACHE_ON_FAILURE: false
UV_CACHE_DIR: /home/runner/work/_temp/setup-uv-cache
##[endgroup]
Updating crates.io index
Checking nitrocop v0.0.1-pre5 (/home/runner/work/nitrocop/nitrocop)
error: this `if` statement can be collapsed
--> src/cop/style/array_intersect.rs:223:37
|
223 | / ...                   if matches!(rm, "count" | "size" | "length") {
224 | | ...                       if recv_call.arguments().is_none()
225 | | ...                           && recv_call.block().is_none()
...   |
247 | | ...                   }
| |_______________________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#collapsible_if
= note: `-D clippy::collapsible-if` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::collapsible_if)]`
help: collapse nested if block
|
223 ~                                     if matches!(rm, "count" | "size" | "length")
224 ~                                         && recv_call.arguments().is_none()
225 |                                             && recv_call.block().is_none()
...
245 |                                             }
246 ~                                         }
|
error: this `if` statement can be collapsed
--> src/cop/style/array_intersect.rs:257:9
|
257 | /         if matches!(method_name, "positive?" | "zero?") {
258 | |             if call.arguments().is_none() && call.block().is_none() {
259 | |                 if let Some(recv) = call.receiver() {
260 | |                     if let Some(recv_call) = recv.as_call_node() {
...   |
286 | |         }
| |_________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#collapsible_if
help: collapse nested if block
|
257 ~         if matches!(method_name, "positive?" | "zero?")
258 ~             && call.arguments().is_none() && call.block().is_none() {
259 |                 if let Some(recv) = call.receiver() {
...
284 |                 }
285 ~             }
|
error: this `if` statement can be collapsed
--> src/cop/style/array_intersect.rs:262:25
|
262 | /                         if matches!(rm, "count" | "size" | "length") {
263 | |                             if recv_call.arguments().is_none() && recv_call.block().is_none() {
264 | |                                 if let Some(inner_recv) = recv_call.receiver() {
265 | |                                     if let Some((lhs, rhs)) =
...   |
282 | |                         }
| |_________________________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#collapsible_if
help: collapse nested if block
|
262 ~                         if matches!(rm, "count" | "size" | "length")
263 ~                             && recv_call.arguments().is_none() && recv_call.block().is_none() {
264 |                                 if let Some(inner_recv) = recv_call.receiver() {
...
280 |                                 }
281 ~                             }
|
error: could not compile `nitrocop` (lib) due to 3 previous errors
##[error]Process completed with exit code 101.

Constraints

  • Keep the fix scoped to the failing checks.
  • Reuse the existing PR branch and existing tests where possible.
  • Prefer the minimal patch that makes the deterministic verification pass.
  • A full revert to origin/main or an empty PR is treated as a failed repair, not a success.
  • If a fix is blocked by missing context, explain that clearly in the final message.

Available Local Helper Scripts

These helper scripts are available in this CI checkout. Prefer the stable top-level CLI paths shown below over ad hoc commands when they directly help with diagnosis or validation.

  • scripts/check_cop.py — aggregate corpus regression check for one cop
  • scripts/dispatch_cops.py — dispatch helpers for changed cops, task generation, ranking, and prior attempts
  • scripts/investigate_cop.py — inspect FP/FN examples from corpus oracle data
  • scripts/verify_cop_locations.py — verify exact known oracle FP/FN locations
  • scripts/corpus_smoke_test.py — smoke-test a few pinned repos

Typical usage when present:

python3 scripts/check_cop.py Department/CopName --verbose --rerun --clone
python3 scripts/dispatch_cops.py changed --base origin/main --head HEAD
python3 scripts/investigate_cop.py Department/CopName --context
python3 scripts/verify_cop_locations.py Department/CopName
python3 scripts/corpus_smoke_test.py --binary target/release/nitrocop

6 bot added a commit that referenced this pull request Mar 30, 2026
@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 30, 2026

Auto-repair Succeeded

  • Checks run: #23731277359
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Repair workflow: #23731426577
  • Repair commit: 9624059952a4370ea3d48ac82566717229c2d5fe

Auto-repair succeeded with backend codex / hard.

Validated locally before push using /home/runner/work/_temp/agent-pr-repair/repair/verify.sh.

@6 6 bot force-pushed the fix/style-array_intersect-23730406918 branch from 519c342 to 9624059 Compare March 30, 2026 06:42
@github-actions
Copy link
Copy Markdown
Contributor

Cop-check results (8 shards)

Cop Baseline FP Baseline FN Local FP Local FN FP Δ FN Δ Result
Style/ArrayIntersect 0 17 0 13 0 -4
Style/ArrayIntersect 0 31 0 23 0 -8
Style/ArrayIntersect 0 26 0 20 0 -6
Style/ArrayIntersect 0 21 0 16 0 -5
Style/ArrayIntersect 0 24 0 14 0 -10
Style/ArrayIntersect 0 25 0 23 0 -2
Style/ArrayIntersect 0 8 0 5 0 -3
Style/ArrayIntersect 0 18 0 15 0 -3

All shards passed — ready to merge.

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 30, 2026

Auto-repair Started

  • Checks run: #23731522359
  • Route: easy
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Reason: build-and-test: Format
  • Repair workflow: #23731680907
Task prompt (4044 tokens)

PR Repair Task: PR #897

Before making changes, read docs/agent-ci.md.

Repair the existing PR branch in place. Do not create a new branch or PR.
Preserve the intent of the current PR and make the smallest changes needed to fix the failing checks.
Do not repair this PR by reverting it back to origin/main, deleting the whole diff, or otherwise turning it into an empty/no-op PR.
If the only plausible fix is a full revert of the PR, stop and explain that clearly instead of performing the revert.
Do not edit unrelated files or do broad cleanup.

Context

  • PR: [bot] Fix Style/ArrayIntersect #897 [bot] Fix Style/ArrayIntersect
  • Branch: fix/style-array_intersect-23730406918
  • Checks run: #1910 (Checks)
  • Route: easy
  • Selected backend: codex / hard
  • Failure summary: build-and-test: Format

Required Outcome

Fix the currently failing checks shown below.
Before finishing, run the targeted validations needed to make the workflow's final verification likely to pass.

Required Local Verification

The workflow will rerun these commands before pushing. Your changes should make them pass:

cargo fmt --check

Current PR Diff Stat

src/cop/style/array_intersect.rs                   | 272 ++++++++++++++++++++-
 .../cops/style/array_intersect/no_offense.rb       |   7 +
 .../fixtures/cops/style/array_intersect/offense.rb |  27 ++
 3 files changed, 294 insertions(+), 12 deletions(-)

Current PR Diff Excerpt

diff --git a/src/cop/style/array_intersect.rs b/src/cop/style/array_intersect.rs
index d1f95f178..3a21bae65 100644
--- a/src/cop/style/array_intersect.rs
+++ b/src/cop/style/array_intersect.rs
@@ -3,8 +3,70 @@ use crate::cop::{Cop, CopConfig};
 use crate::diagnostic::Diagnostic;
 use crate::parse::source::SourceFile;
 
+/// Style/ArrayIntersect detects array intersection patterns replaceable
+/// with `Array#intersect?` (Ruby 3.1+).
+///
+/// Handles three families of patterns:
+/// 1. Direct predicates: `(a & b).any?` / `.empty?` / `.none?`
+///    (plus `.present?` / `.blank?` when `ActiveSupportExtensionsEnabled`)
+/// 2. Size comparisons: `(a & b).count > 0`, `== 0`, `!= 0`
+///    (also `.size` and `.length`)
+/// 3. Size predicates: `(a & b).count.positive?`, `.count.zero?`
+///
+/// All patterns also match the `a.intersection(b)` form (1 argument only).
 pub struct ArrayIntersect;
 
+/// Extract (lhs_source, rhs_source) from an intersection expression node.
+/// Matches `(a & b)` (parenthesized `&` call) and `a.intersection(b)` (1-arg form).
+fn extract_intersection_parts(node: &ruby_prism::Node<'_>) -> Option<(String, String)> {
+    // (a & b) form
+    if let Some(paren) = node.as_parentheses_node() {
+        if let Some(body) = paren.body() {
+            if let Some(stmts) = body.as_statements_node() {
+                let stmt_list: Vec<_> = stmts.body().iter().collect();
+                if stmt_list.len() == 1 {
+                    if let Some(inner_call) = stmt_list[0].as_call_node() {
+                        let m = std::str::from_utf8(inner_call.name().as_slice()).unwrap_or("");
+                        if m == "&" {
+                            let recv = inner_call.receiver()?;
+                            let args = inner_call.arguments()?;
+                            let arg_list: Vec<_> = args.arguments().iter().collect();
+                            if arg_list.len() == 1 {
+                                let lhs = std::str::from_utf8(recv.location().as_slice())
+                                    .unwrap_or("")
+                                    .to_string();
+                                let rhs = std::str::from_utf8(arg_list[0].location().as_slice())
+                                    .unwrap_or("")
+                                    .to_string();
+                                return Some((lhs, rhs));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    // a.intersection(b) form
+    if let Some(call) = node.as_call_node() {
+        let m = std::str::from_utf8(call.name().as_slice()).unwrap_or("");
+        if m == "intersection" {
+            let recv = call.receiver()?;
+            let args = call.arguments()?;
+            let arg_list: Vec<_> = args.arguments().iter().collect();
+            if arg_list.len() == 1 {
+                let lhs = std::str::from_utf8(recv.location().as_slice())
+                    .unwrap_or("")
+                    .to_string();
+                let rhs = std::str::from_utf8(arg_list[0].location().as_slice())
+                    .unwrap_or("")
+                    .to_string();
+                return Some((lhs, rhs));
+            }
+        }
+    }
+    None
+}
+
 impl Cop for ArrayIntersect {
     fn name(&self) -> &'static str {
         "Style/ArrayIntersect"
@@ -40,8 +102,12 @@ impl Cop for ArrayIntersect {
 
         let method_name = std::str::from_utf8(call.name().as_slice()).unwrap_or("");
 
-        // Pattern: (array1 & array2).any? / .empty? / .none?
-        if matches!(method_name, "any?" | "empty?" | "none?") {
+        let active_support = config.get_bool("ActiveSupportExtensionsEnabled", false);
+
+        // Pattern 1: (a & b).any? / .empty? / .none? / .present? / .blank?
+        if matches!(method_name, "any?" | "empty?" | "none?")
+            || (active_support && matches!(method_name, "present?" | "blank?"))
+        {
             // Skip if the call has arguments or a block (any? with block)
             if call.arguments().is_some() || call.block().is_some() {
                 return;
@@ -62,12 +128,35 @@ impl Cop for ArrayIntersect {
                                         let loc = node.location();
                                         let (line, column) =
                                             source.offset_to_line_col(loc.start_offset());
-                                        let msg = format!(
-                                            "Use `intersect?` instead of `({}).{}`.",
-                                            std::str::from_utf8(inner_call.location().as_slice())
-                                                .unwrap_or("array1 & array2"),
-                                            method_name
-                                        );
+
+                                        // Keep backward-compatible message for original patterns
+                                        let msg =
+                                            if matches!(method_name, "any?" | "empty?" | "none?") {
+                                                format!(
+                                                    "Use `intersect?` instead of `({}).{}`.",
+                                                    std::str::from_utf8(
+                                                        inner_call.location().as_slice()
+                                                    )
+                                                    .unwrap_or("array1 & array2"),
+                                                    method_name
+                                                )
+                                            } else if let Some((lhs, rhs)) =
+                                                extract_intersection_parts(&receiver)
+                                            {
+                                                let existing = std::str::from_utf8(loc.as_slice())
+                                                    .unwrap_or("");
+                                                format!(
+                                                    "Use `{}.intersect?({})` instead of `{}`.",
+                                                    lhs, rhs, existing
+                                                )
+                                            } else {
+                                                let existing = std::str::from_utf8(loc.as_slice())
+                                                    .unwrap_or("");
+                                                format!(
+                                                    "Use `intersect?` instead of `{}`.",
+                                                    existing
+                                                )
+                                            };
                                         diagnostics
                                             .push(self.diagnostic(source, line, column, msg));
                                     }
@@ -90,10 +179,27 @@ impl Cop for ArrayIntersect {
                                     let loc = node.location();
                                     let (line, column) =
                                         source.offset_to_line_col(loc.start_offset());
-                                    let msg = format!(
-                                        "Use `intersect?` instead of `intersection(...).{}`.",
-                                        method_name
-                                    );
+
+                                    let msg = if matches!(method_name, "any?" | "empty?" | "none?")
+                                    {
+                                        format!(
+                                            "Use `intersect?` instead of `intersection(...).{}`.",
+                                            method_name
+                                        )
+                                    } else if let Some((lhs, rhs)) =
+                                        extract_intersection_parts(&receiver)
+                                    {
+                                        let existing =
+                                            std::str::from_utf8(loc.as_slice()).unwrap_or("");
+                                        format!(
+                                            "Use `{}.intersect?({})` instead of `{}`.",
+                                            lhs, rhs, existing
+                                        )
+                                    } else {
+                                        let existing =
+                                            std::str::from_utf8(loc.as_slice()).unwrap_or("");
+                                        format!("Use `intersect?` instead of `{}`.", existing)
+                                    };
                                     diagnostics.push(self.diagnostic(source, line, column, msg));
                                 }
                             }
@@ -102,6 +208,79 @@ impl Cop for ArrayIntersect {
                 }
             }
         }
+
+        // Pattern 2: (a & b).count > 0 / == 0 / != 0
+        if matches!(method_name, ">" | "==" | "!=") {
+            if let Some(args) = call.arguments() {
+                let arg_list: Vec<_> = args.arguments().iter().collect();
+                if arg_list.len() == 1 {
+                    if let Some(int_node) = arg_list[0].as_integer_node() {
+                        if int_node.location().as_slice() == b"0" {
+                            if let Some(recv) = call.receiver() {
+                                if let Some(recv_call) = recv.as_call_node() {
+                                    let rm = std::str::from_utf8(recv_call.name().as_slice())
+                                        .unwrap_or("");
+                                    if matches!(rm, "count" | "size" | "length")
+                                        && recv_call.arguments().is_none()
+                                        && recv_call.block().is_none()
+                                    {
+                                        if let Some(inner_recv) = recv_call.receiver() {
+                                            if let Some((lhs, rhs)) =
+                                                extract_intersection_parts(&inner_recv)
+                                            {
+                                                let loc = node.location();
+                                                let (line, column) =
+                                                    source.offset_to_line_col(loc.start_offset());
+                                                let existing = std::str::from_utf8(loc.as_slice())
+                                                    .unwrap_or("");
+                                                let msg = format!(
+                                                    "Use `{}.intersect?({})` instead of `{}`.",
+                                                    lhs, rhs, existing
+                                                );
+                                                diagnostics.push(
+                                                    self.diagnostic(source, line, column, msg),
+                                                );
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Pattern 3: (a & b).count.positive? / .zero?
+        if matches!(method_name, "positive?" | "zero?")
+            && call.arguments().is_none()
+            && call.block().is_none()
+        {
+            if let Some(recv) = call.receiver() {
+                if let Some(recv_call) = recv.as_call_node() {
+                    let rm = std::str::from_utf8(recv_call.name().as_slice()).unwrap_or("");
+                    if matches!(rm, "count" | "size" | "length")
... (148 more lines truncated)

Failed Checks Packet

build-and-test

  • Route: easy
  • Failed steps: Format
build-and-test	Format	2026-03-30T06:43:23.1408734Z ##[group]Run cargo fmt --check
cargo fmt --check
shell: /usr/bin/bash -e {0}
env:
CARGO_HOME: /home/runner/.cargo
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
CACHE_ON_FAILURE: false
##[endgroup]
Diff in /home/runner/work/nitrocop/nitrocop/src/cop/style/array_intersect.rs:266:
if let Some(inner_recv) = recv_call.receiver() {
if let Some((lhs, rhs)) = extract_intersection_parts(&inner_recv) {
let loc = node.location();
-                                let (line, column) =
-                                    source.offset_to_line_col(loc.start_offset());
-                                let existing =
-                                    std::str::from_utf8(loc.as_slice()).unwrap_or("");
+                                let (line, column) = source.offset_to_line_col(loc.start_offset());
+                                let existing = std::str::from_utf8(loc.as_slice()).unwrap_or("");
let msg = format!(
"Use `{}.intersect?({})` instead of `{}`.",
lhs, rhs, existing
##[error]Process completed with exit code 1.

Constraints

  • Keep the fix scoped to the failing checks.
  • Reuse the existing PR branch and existing tests where possible.
  • Prefer the minimal patch that makes the deterministic verification pass.
  • A full revert to origin/main or an empty PR is treated as a failed repair, not a success.
  • If a fix is blocked by missing context, explain that clearly in the final message.

Available Local Helper Scripts

These helper scripts are available in this CI checkout. Prefer the stable top-level CLI paths shown below over ad hoc commands when they directly help with diagnosis or validation.

  • scripts/check_cop.py — aggregate corpus regression check for one cop
  • scripts/dispatch_cops.py — dispatch helpers for changed cops, task generation, ranking, and prior attempts
  • scripts/investigate_cop.py — inspect FP/FN examples from corpus oracle data
  • scripts/verify_cop_locations.py — verify exact known oracle FP/FN locations
  • scripts/corpus_smoke_test.py — smoke-test a few pinned repos

Typical usage when present:

python3 scripts/check_cop.py Department/CopName --verbose --rerun --clone
python3 scripts/dispatch_cops.py changed --base origin/main --head HEAD
python3 scripts/investigate_cop.py Department/CopName --context
python3 scripts/verify_cop_locations.py Department/CopName
python3 scripts/corpus_smoke_test.py --binary target/release/nitrocop

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 30, 2026

Auto-repair Rejected

The workflow rejected this repair because it edited files outside the allowed scope for route repair-python-workflow.

Agent File Scope

  • Profile: repair-python-workflow

Allowed path patterns:

  • scripts/**
  • tests/python/**
  • .github/workflows/**
  • pyproject.toml
  • mise.toml

Disallowed changed files:

  • src/cop/style/array_intersect.rs

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