Skip to content

[bot] Fix Style/SafeNavigationChainLength#882

Open
6[bot] wants to merge 3 commits intomainfrom
fix/style-safe_navigation_chain_length-23729861199
Open

[bot] Fix Style/SafeNavigationChainLength#882
6[bot] wants to merge 3 commits intomainfrom
fix/style-safe_navigation_chain_length-23729861199

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 30, 2026

Automated fix fix for Style/SafeNavigationChainLength corpus conformance.

Refs #448

Details

Cop Style/SafeNavigationChainLength
Backend codex / hard
Model gpt-5.4 (xhigh)
Mode fix
Turns 1
Run #439

Result

Implemented the fix in [safe_navigation_chain_length.rs](/home/runner/work/nitrocop/nitrocop/src/cop/style/safe_navigation_chain_length.rs) and updated [offense.rb](/home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/safe_navigation_chain_length/offense.rb) with valid FN fixtures and aligned offense columns.

The cop now tracks safe-navigation ancestry with a Prism visitor instead of a receiver-only walk. That fixes the missed cases from block-pass arguments like `&:inspect` and nested safe-nav calls inside direct call arguments, while stopping ancestry from leaking through non-call wrappers that Parser would treat as boundaries. I also added the required `///` investigation note on the cop struct.

Verified with:
- `cargo test --lib -- cop::style::safe_navigation_chain_length`
- `python3 scripts/check_cop.py Style/SafeNavigationChainLength --rerun --clone --sample 15`

Corpus gate result: `New FP 0`, `New FN 0`, `Resolved FN 40`, `pass`.

Committed as `98b5f49d` with message: `Fix Style/SafeNavigationChainLength ancestor tracking`.

Changes

  1 files changed
Agent conversation excerpt

Reading docs/agent-ci.md first, then I’ll inspect the current cop, fixtures, and RuboCop’s reference implementation to isolate the FN pattern before touching code.

Reading docs/agent-ci.md first, then I’ll inspect the current cop, fixtures, and RuboCop’s reference implementation to isolate the FN pattern before touching code.

exec_command: sed -n '1,220p' docs/agent-ci.md

exec_command: sed -n '1,220p' src/cop/style/safe_navigation_chain_length.rs

exec_command: sed -n '1,220p' vendor/rubocop/lib/rubocop/cop/style/safe_navigation_chain_length.rb

exec_command: sed -n '1,220p' vendor/rubocop/spec/rubocop/cop/style/safe_navigation_chain_length_spec.rb

exec_command: sed -n '1,220p' tests/fixtures/cops/style/safe_navigation_chain_length/offense.rb

exec_command: sed -n '1,220p' tests/fixtures/cops/style/safe_navigation_chain_length/no_offense.rb

The fixture already contains the representative FN cases, so I’m checking whether the current implementation actually misses them and whether the bug is in receiver-only counting versus Prism parent traversal.

The fixture already contains the representative FN cases, so I’m checking whether the current implementation actually misses them and whether the bug is in receiver-only counting versus Prism parent traversal.

exec_command: cargo test --lib -- cop::style::safe_navigation_chain_length

exec_command: git status --short -- tests/fixtures/cops/style/safe_navigation_chain_length src/cop/style/safe_navigation_chain_length.rs

There’s already an uncommitted fixture edit in offense.rb. I’m leaving it intact and working with it rather than rewriting around it.

There’s already an uncommitted fixture edit in offense.rb. I’m leaving it intact and working with it rather than rewriting around it.

write_stdin: ``

exec_command: git diff -- tests/fixtures/cops/style/safe_navigation_chain_length/offense.rb

write_stdin: ``

I’ve confirmed the failing fixture cases were pre-added. Next I’m waiting on the targeted test run, then I’ll inspect the AST shape for the multiline and argument-contained chains that the current receiver walk likely misses.

I’ve confirmed the failing fixture cases were pre-added. Next I’m waiting on the targeted test run, then I’ll inspect the AST shape for the multiline and argument-contained chains that the current receiver walk likely misses.

write_stdin: ``

exec_command: rg -n "parent\(|parent_id|node_id|visit_call_node|ParseResult" src | head -n 80

write_stdin: ``

exec_command: sed -n '180,240p' src/node_pattern/interpreter.rs

exec_command: sed -n '1040,1105p' src/cop/util.rs

I’m checking how parent traversal is exposed in the Prism bindings. If generic parent links are available, the fix should match RuboCop much more closely than the current receiver-only walk.

I’m checking how parent traversal is exposed in the Prism bindings. If generic parent links are available, the fix should match RuboCop much more closely than the current receiver-only walk.

exec_command: rg -n "\.parent\(\)" src | head -n 120

exec_command: rg -n "as_call_node\(\).*parent|parent\(\).*as_call_node|CompactNode|node_id" src vendor -g '!target' | head -n 120

write_stdin: ``

The likely fix is to switch from receiver-only counting to ancestor-based counting that mirrors RuboCop, while still treating Prism block-bearing calls as chain boundaries. I’m verifying exact duplicate/offense behavior with RuboCop before I edit the cop.

The likely fix is to switch from receiver-only counting to ancestor-based counting that mirrors RuboCop, while still treating Prism block-bearing calls as chain boundaries. I’m verifying exact duplicate/offense behavior with RuboCop before I edit the cop.

exec_command: printf '%s ' 'x&.foo&.bar&.baz&.qux' > /tmp/safe_nav.rb && rubocop --only Style/SafeNavigationChainLength /tmp/safe_nav.rb

exec_command: printf '%s ' 'parsed_lockfile&.specs&.find { |s| s.name == dependency_name }&.source' > /tmp/safe_nav_block.rb && rubocop --only Style/SafeNavigationChainLength /tmp/safe_nav_block.rb

exec_command: cat <<'RUBY' > /tmp/safe_nav_multiline.rb s &.split('.') &.map(&:to_i) &.extend(Comparable) RUBY rubocop --only Style/SafeNavigationChainLength /tmp/safe_nav_multiline.rb

exec_command: printf '%s ' "params[:view_token] || session['view_token']&.[](record&.id&.to_s)" > /tmp/safe_nav_arg.rb && rubocop --only Style/SafeNavigationChainLength /tmp/safe_nav_arg.rb

exec_command: sed -n '1000,1045p' src/cop/lint/duplicate_methods.rs

write_stdin: ``

write_stdin: ``

write_stdin: ``

write_stdin: ``

The missed cases split into two categories now: nested chains across transparent Prism wrappers like ArgumentsNode, and chains that currently get cut off by block-pass syntax like &:inspect, which RuboCop does not treat as a boundary. I’m confirming the exact Prism node types before patching.

The missed cases split into two categories now: nested chains across transparent Prism wrappers like ArgumentsNode, and chains that currently get cut off by block-pass syntax like &:inspect, which RuboCop does not treat as a boundary. I’m confirming the exact Prism node types before patching.

exec_command: rg -n "as_block_node|BlockNode|BlockArgumentNode|block\(\)" src | head -n 120

exec_command: printf '%s ' '@object&.errors&.map(&:attribute)&.include?(attribute.to_sym)' > /tmp/safe_nav_symbol_proc.rb && rubocop --disable-pending-cops --only Style/SafeNavigationChainLength /tmp/safe_nav_symbo

exec_command: printf '%s ' 'got = called&.args&.map(&:inspect)&.join(", ")' > /tmp/safe_nav_symbol_proc2.rb && rubocop --disable-pending-cops --only Style/SafeNavigationChainLength /tmp/safe_nav_symbol_proc2.rb

exec_command: printf '%s ' '@names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }' > /tmp/safe_nav_block_pass_and_block.rb && rubocop --disable-pending-cops --only Style/SafeNavigationChainLength /

write_stdin: ``

write_stdin: ``

exec_command: sed -n '1,160p' src/cop/performance/chain_array_allocation.rs

Plan:

  1. Replace receiver-only counting with a RuboCop-style ancestor walk rooted at each safe-nav call.
  2. Make that walk Prism-aware by skipping transparent wrapper nodes that Parser doesn’t expose and by treating only real BlockNodes as chain boundaries.
  3. Re-run the focused test, then the corpus sampler, then commit only the cop file and this fixture set.

Plan:

  1. Replace receiver-only counting with a RuboCop-style ancestor walk rooted at each safe-nav call.
  2. Make that walk Prism-aware by skipping transparent wrapper nodes that Parser doesn’t expose and by treating only real BlockNodes as chain boundaries.
  3. Re-run the focused test, then the corpus sampler, then commit only the cop file and this fixture set.

update_plan

exec_command: sed -n '140,200p' src/cop/style/class_and_module_children.rs

exec_command: sed -n '140,190p' src/cop/rspec/described_class.rs

exec_command: rg --files src/bin

exec_command: rg -n "ArgumentsNode|as_arguments_node|StatementsNode|as_statements_node" src | head -n 120

exec_command: sed -n '388,430p' src/cop/bundler/duplicated_gem.rs

Task prompt (6190 tokens)

Fix Style/SafeNavigationChainLength — 0 FP, 40 FN

Instructions

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

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

⚠ 960 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/SafeNavigationChainLength /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Style/SafeNavigationChainLength /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/safe_navigation_chain_length/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/style/safe_navigation_chain_length/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::style::safe_navigation_chain_length
  5. Fix src/cop/style/safe_navigation_chain_length.rs
  6. Verify test passes: cargo test --lib -- cop::style::safe_navigation_chain_length
  7. Validate against corpus (REQUIRED before committing):
    python3 scripts/check_cop.py Style/SafeNavigationChainLength --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/SafeNavigationChainLength: <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/SafeNavigationChainLength /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/safe_navigation_chain_length.rs and tests/fixtures/cops/style/safe_navigation_chain_length/
  • Run cargo test --lib -- cop::style::safe_navigation_chain_length to verify your fix (do NOT run the full test suite)
  • Run python3 scripts/check_cop.py Style/SafeNavigationChainLength --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/safe_navigation_chain_length/offense.rb

x&.foo&.bar&.baz
^^^^^^^^^^^^^^^^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

a&.b&.c&.d
^^^^^^^^^^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

user&.address&.city&.name
^^^^^^^^^^^^^^^^^^^^^^^^^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Current Fixture: no_offense.rb

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

x&.foo&.bar
x&.foo
x.foo.bar.baz
x&.foo.bar
x = 1
y = x&.to_s

# Block breaks safe navigation chain — each segment counted separately
parsed_lockfile&.specs&.find { |s| s.name == dependency_name }&.source
sources = requirements&.map { |r| r.fetch(:source) }&.uniq&.compact
items&.select { |i| i.valid? }&.map { |i| i.name }&.join(", ")
result&.data&.each { |d| process(d) }&.count

Key Source Files

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

Top FN repos:

  • inspec__inspec-azure__4d8eaea (4 FN) — example libraries/azure_role_definition.rb:34
  • activemerchant__active_merchant__ca45b1b (3 FN) — example lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb:105
  • seek4science__seek__573bc1a (3 FN) — example app/models/concerns/workflow_extraction.rb:151

Representative FN examples:

  • Betterment__delayed__00d575c: lib/delayed/priority.rb:76 — Avoid safe navigation chains longer than 2 calls.
  • MarkUsProject__Markus__41d3df9: app/controllers/results_controller.rb:706 — Avoid safe navigation chains longer than 2 calls.
  • MarkUsProject__Markus__41d3df9: app/controllers/submissions_controller.rb:882 — Avoid safe navigation chains longer than 2 calls.

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: Betterment__delayed__00d575c: lib/delayed/priority.rb:76

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

Full-file enclosing chain: method body (line 70: def names=(names)) > class body (line 59: class << self) > class body (line 2: class Priority < Numeric) > module body (line 1: module Delayed)

Message: Avoid safe navigation chains longer than 2 calls.

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

        @names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

       46:     # Delayed::Priority.alerts = {
       47:     #   high: { age: 30.seconds, run_time: 15.seconds, attempts: 3 },
       48:     #   medium: { age: 2.minutes, run_time: 1.minute, attempts: 6 },
       49:     #   low: { age: 10.minutes, run_time: 2.minutes, attempts: 9 },
       50:     # }
       51: 
       52:     DEFAULT_ALERTS = {
       53:       interactive: { age: 1.minute, run_time: 30.seconds, attempts: 3 },
       54:       user_visible: { age: 3.minutes, run_time: 90.seconds, attempts: 5 },
       55:       eventual: { age: 1.5.hours, run_time: 5.minutes, attempts: 8 },
       56:       reporting: { age: 4.hours, run_time: 10.minutes, attempts: 8 },
       57:     }.freeze
       58: 
       59:     class << self
       60:       attr_writer :assign_at_midpoint
       61: 
       62:       def names
       63:         @names || default_names
       64:       end
       65: 
       66:       def alerts
       67:         @alerts || default_alerts
       68:       end
       69: 
       70:       def names=(names)
       71:         raise "must include a name for priority >= 0" if names && !names.value?(0)
       72: 
       73:         @ranges = nil
       74:         @alerts = nil
       75:         @names_to_priority = nil
>>>    76:         @names = names&.sort_by(&:last)&.to_h&.transform_values { |v| new(v) }
       77:       end
       78: 
       79:       def alerts=(alerts)
       80:         if alerts
       81:           unknown_names = alerts.keys - names.keys
       82:           raise "unknown priority name(s): #{unknown_names}" if unknown_names.any?
       83:         end

FN #2: MarkUsProject__Markus__41d3df9: app/controllers/results_controller.rb:706

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

Full-file enclosing chain: method body (line 705: def view_token_param) > class body (line 1: class ResultsController < ApplicationController)

Prism AST at offense line: StatementsNode > OrNode > CallNode > CallNode > ArgumentsNode > SymbolNode > CallNode > CallNode > CallNode > ArgumentsNode > StringNode > ArgumentsNode > CallNode > CallNode > CallNode

Message: Avoid safe navigation chains longer than 2 calls.

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

    params[:view_token] || session['view_token']&.[](record&.id&.to_s)
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

      676:                I18n.t('submissions.release_token_expires'),
      677:                I18n.t('submissions.release_token_url')]]
      678:     assignment = Assignment.find(params[:assignment_id])
      679:     csv_string = MarkusCsv.generate(data, header) do |row|
      680:       view_token, view_token_expiry, result_id = row.pop(3)
      681:       view_token_expiry ||= I18n.t('submissions.release_token_expires_null')
      682:       url = view_marks_course_result_url(current_course.id, result_id, view_token: view_token)
      683:       [*row, view_token, view_token_expiry, url]
      684:     end
      685:     send_data csv_string,
      686:               disposition: 'attachment',
      687:               filename: "#{assignment.short_identifier}_release_view_tokens.csv"
      688:   end
      689: 
      690:   def print
      691:     pdf_report = record.generate_print_pdf
      692:     send_data pdf_report.to_pdf,
      693:               filename: record.print_pdf_filename,
      694:               type: 'application/pdf'
      695:   end
      696: 
      697:   private
      698: 
      699:   def extra_mark_params
      700:     params.require(:extra_mark).permit(:result,
      701:                                        :description,
      702:                                        :extra_mark)
      703:   end
      704: 
      705:   def view_token_param
>>>   706:     params[:view_token] || session['view_token']&.[](record&.id&.to_s)
      707:   end
      708: 
      709:   def criterion_id_param
      710:     params[:criterion_id]
      711:   end
      712: 
      713:   def requested_results

FN #3: MarkUsProject__Markus__41d3df9: app/controllers/submissions_controller.rb:882

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

Full-file enclosing chain: if branch (line 881: if !record.nil?) > method body (line 880: def view_token_param) > class body (line 3: class SubmissionsController < ApplicationController)

Prism AST at offense line: StatementsNode > OrNode > CallNode > CallNode > ArgumentsNode > SymbolNode > CallNode > CallNode > CallNode > ArgumentsNode > StringNode > ArgumentsNode > CallNode > CallNode > CallNode > CallNode

Message: Avoid safe navigation chains longer than 2 calls.

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

      params[:view_token] || session['view_token']&.[](record.current_result&.id&.to_s)
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

      852:     @submission = record
      853:     @assignment = record.grouping.assignment
      854:     @course = record.course
      855:     if @assignment.past_remark_due_date?
      856:       head :bad_request
      857:     else
      858:       record.update(
      859:         remark_request: params[:submission][:remark_request],
      860:         remark_request_timestamp: Time.current
      861:       )
      862:       if params[:save]
      863:         head :ok
      864:       elsif params[:submit]
      865:         unless record.remark_result
      866:           record.make_remark_result
      867:           record.non_pr_results.reload
      868:         end
      869:         record.remark_result.update(marking_state: Result::MARKING_STATES[:incomplete])
      870:         record.get_original_result.update(released_to_students: false)
      871:         render js: 'location.reload();'
      872:       else
      873:         head :bad_request
      874:       end
      875:     end
      876:   end
      877: 
      878:   private
      879: 
      880:   def view_token_param
      881:     if !record.nil?
>>>   882:       params[:view_token] || session['view_token']&.[](record.current_result&.id&.to_s)
      883:     else
      884:       false
      885:     end
      886:   end
      887: 
      888:   def html_content(file: nil, grouping: nil, revision_identifier: nil, file_dir: '/', filename: nil)
      889:     if file.present?

FN #4: Pistos__diakonos__e284f5d: lib/diakonos/version.rb:6

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

Full-file enclosing chain: method body (line 5: def self.parse_version(s)) > module body (line 1: module Diakonos)

Prism AST at offense line: StatementsNode > CallNode > CallNode > CallNode > LocalVariableReadNode

Message: Avoid safe navigation chains longer than 2 calls.

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

    s
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

        1: module Diakonos
        2:   VERSION = '0.10.0'
        3:   LAST_MODIFIED = '2026-02-28'
        4: 
        5:   def self.parse_version(s)
>>>     6:     s
        7:     &.split( '.' )
        8:     &.map(&:to_i)
        9:     &.extend(Comparable)
       10:   end
       11: 
       12:   def self.check_ruby_version
       13:     ruby_version = parse_version(RUBY_VERSION)

FN #5: Shopify__shipit-engine__ff8282b: test/helpers/hooks_helper.rb:28

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

Full-file enclosing chain: method body (line 20: def expect_no_hook(*args)) > module body (line 3: module HooksHelper)

Message: Avoid safe navigation chains longer than 2 calls.

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

    got = called&.args&.map(&:inspect)&.join(', ')
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

        1: # frozen_string_literal: true
        2: 
        3: module HooksHelper
        4:   def expect_hook(event, stack = nil, payload = nil)
        5:     spy_on_hook
        6:     yield
        7:     assert_received_with(Shipit::Hook, :emit) do |call|
        8:       if call.args.first == event && (stack.nil? || call.args.second == stack)
        9:         if payload.respond_to?(:call)
       10:           payload.call(call.args.third)
       11:         elsif payload
       12:           payload == call.args.third
       13:         else
       14:           true
       15:         end
       16:       end
       17:     end
       18:   end
       19: 
       20:   def expect_no_hook(*args)
       21:     spy_on_hook
       22:     yield
       23:     spy = Spy::Subroutine.get(Shipit::Hook, :emit)
       24:     called = spy.calls.find do |call|
       25:       args.map.with_index.all? { |value, index| value == call.args[index] }
       26:     end
       27:     matcher = args.map(&:inspect).join(', ')
>>>    28:     got = called&.args&.map(&:inspect)&.join(', ')
       29:     refute(called, "Expected no hook matching: (#{matcher})\n  got: (#{got})")
       30:   end
       31: 
       32:   private
       33: 
       34:   def spy_on_hook
       35:     Spy.on(Shipit::Hook, :emit).and_call_through

FN #6: Shopify__spoom__3d124a0: test/spoom/cli/srb/sigs_test.rb:502

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

Full-file enclosing chain: method body (line 473: def test_export_check_sync_raises_if_rbi_is_not_up_to_date) > class body (line 9: class SigsTest < TestWithProject) > module body (line 8: module Srb) > module body (line 7: module Cli) > module body (line 6: module Spoom)

Message: Avoid safe navigation chains longer than 2 calls.

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

          assert_equal(<<~ERR.rstrip, result.err&.lines&.map(&:rstrip)&.join("\n"))
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

      472: 
      473:         def test_export_check_sync_raises_if_rbi_is_not_up_to_date
      474:           @project.write!("foo.gemspec", GEMSPEC)
      475:           @project.write!("lib/foo.rb", <<~RB)
      476:             class Foo
      477:               #: -> void
      478:               def bar; end
      479: 
      480:               #: -> void
      481:               def foo; end
      482:             end
      483:           RB
      484: 
      485:           rbi = <<~RBI
      486:             # typed: true
      487: 
      488:             # DO NOT EDIT MANUALLY
      489:             # This is an autogenerated file for types exported from the `foo` gem.
      490:             # Please instead update this file by running `bundle exec spoom srb sigs export`.
      491: 
      492:             class Foo
      493:               sig { void }
      494:               def foo; end
      495:             end
      496:           RBI
      497: 
      498:           @project.write!("rbi/foo.rbi", rbi)
      499: 
      500:           result = @project.spoom("srb sigs export --no-color --check-sync")
      501:           refute(result.status)
>>>   502:           assert_equal(<<~ERR.rstrip, result.err&.lines&.map(&:rstrip)&.join("\n"))
      503:             --- generated
      504:             +++ current
      505:             @@ -6,8 +6,5 @@
      506: 
      507:              class Foo
      508:                sig { void }
      509:             -  def bar; end

FN #7: TelosLabs__rails-world__44c01d6: app/builders/application_form_builder.rb:71

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

Full-file enclosing chain: method body (line 70: def attribute_has_error?(attribute)) > class body (line 1: class ApplicationFormBuilder < ActionView::Helpers::FormBuilder)

Message: Avoid safe navigation chains longer than 2 calls.

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

    @object&.errors&.map(&:attribute)&.include?(attribute.to_sym)
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

       41:   end
       42: 
       43:   def error_message(attribute, options = {})
       44:     return unless attribute_has_error?(attribute)
       45: 
       46:     options = add_base_styles(options, __method__, attribute)
       47:     error_message = @object.errors.full_messages_for(attribute).join(", ")
       48:     content_tag :div, error_message, options
       49:   end
       50: 
       51:   def toggle_field(attribute, content_or_options = nil, options = {})
       52:     options = add_base_styles(options, __method__, attribute)
       53:     @template.render "shared/form_elements/toggle_field", form: self, attribute: attribute, options: options
       54:   end
       55: 
       56:   private
       57: 
       58:   def add_base_styles(options, method_name, attribute)
       59:     return options if options[:skip_base_styling].present?
       60: 
       61:     style = []
       62:     style << options[:class]
       63:     style << (STYLES[method_name] || STYLES[:default])
       64:     style.join(" ").strip
       65: 
       66:     options[:class] = style
       67:     options
       68:   end
       69: 
       70:   def attribute_has_error?(attribute)
>>>    71:     @object&.errors&.map(&:attribute)&.include?(attribute.to_sym)
       72:   end
       73: end

FN #8: activemerchant__active_merchant__ca45b1b: lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb:105

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

Full-file enclosing chain: method body (line 102: def decrypt_sensitive_fields(data)) > class body (line 5: class CecabankJsonGateway < Gateway) > module body (line 4: module Billing) > module body (line 3: module ActiveMerchant)

Message: Avoid safe navigation chains longer than 2 calls.

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

        cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*')
^ Style/SafeNavigationChainLength: Avoid safe navigation chains longer than 2 calls.

Full file context (30 lines before offense):

       75: 
       76:         if @options[:encryption_key]
       77:           params = parse(request_data['parametros'])
       78:           sensitive_fields = decrypt_sensitive_fields(params['encryptedData'])
       79:           filtered_params = filter_params(sensitive_fields)
       80:           params['encryptedData'] = encrypt_sensitive_fields(filtered_params)
       81:         else
       82:           params = filter_params(decode_params(request_data['parametros']))
       83:         end
       84: 
       85:         request_data['parametros'] = encode_params(params)
       86:         before_message = before_message.gsub(%r(\")i, '\\\"')
       87:         after_message = request_data.to_json.gsub(%r(\")i, '\\\"')
       88:         transcript.sub(before_message, after_message)
       89:       end
       90: 
       91:       private
       92: 
       93:       def filter_params(params)
       94:         params.
       95:           gsub(%r(("pan\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
       96:           gsub(%r(("caducidad\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
       97:           gsub(%r(("cvv2\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
       98:           gsub(%r(("csc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
       99:           gsub(%r(("authentication_value\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]')
      100:       end
      101: 
      102:       def decrypt_sensitive_fields(data)
      103:         cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
      104:         cipher.key = [@options[:encryption_key]].pack('H*')
>>>   105:         cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*')
      106:         cipher.update([data].pack('H*')) + cipher.final
      107:       end
      108: 
      109:       def encrypt_sensitive_fields(data)
      110:         cipher = OpenSSL::Cipher.new('AES-256-CBC').encrypt
      111:         cipher.key = [@options[:encryption_key]].pack('H*')
      112:         cipher.iv = @options[:initiator_vector]&.split('')&.map(&:to_i)&.pack('c*')

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

@6 6 bot force-pushed the fix/style-safe_navigation_chain_length-23729861199 branch from 94adfe2 to d925dc8 Compare March 30, 2026 06:25
@6 6 bot marked this pull request as ready for review March 30, 2026 06:25
@6 6 bot enabled auto-merge (squash) March 30, 2026 06:25
@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/SafeNavigationChainLength 0 11 0 0 0 -11
Style/SafeNavigationChainLength 0 3 0 0 0 -3
Style/SafeNavigationChainLength 0 6 0 0 0 -6
Style/SafeNavigationChainLength 0 2 0 0 0 -2
Style/SafeNavigationChainLength 0 3 0 0 0 -3
Style/SafeNavigationChainLength 0 4 0 0 0 -4
Style/SafeNavigationChainLength 0 3 0 0 0 -3
Style/SafeNavigationChainLength 0 8 0 0 0 -8

All shards passed — ready to merge.

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