Skip to content

[bot] Fix RSpec/LeakyLocalVariable#885

Open
6[bot] wants to merge 2 commits intomainfrom
fix/rspec-leaky_local_variable-23729874503
Open

[bot] Fix RSpec/LeakyLocalVariable#885
6[bot] wants to merge 2 commits intomainfrom
fix/rspec-leaky_local_variable-23729874503

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 30, 2026

Automated fix fix for RSpec/LeakyLocalVariable corpus conformance.

Refs #569

Details

Cop RSpec/LeakyLocalVariable
Backend codex / hard
Model gpt-5.4 (xhigh)
Mode fix
Turns 1
Run #446

Result

Updated [src/cop/rspec/leaky_local_variable.rs#L2657](/home/runner/work/nitrocop/nitrocop/src/cop/rspec/leaky_local_variable.rs#L2657) so `if`/`unless` predicate reads count as reads-before-write during the hook flow check. That fixes the missed `module_def = nil` pattern where a `before` hook does `if module_def.nil?; module_def = ...; end`. I also documented the root cause on the cop struct at [src/cop/rspec/leaky_local_variable.rs#L333](/home/runner/work/nitrocop/nitrocop/src/cop/rspec/leaky_local_variable.rs#L333), added a focused regression test at [src/cop/rspec/leaky_local_variable.rs#L3599](/home/runner/work/nitrocop/nitrocop/src/cop/rspec/leaky_local_variable.rs#L3599), and replaced the placeholder tail of [tests/fixtures/cops/rspec/leaky_local_variable/offense.rb#L366](/home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/rspec/leaky_local_variable/offense.rb#L366) with real corpus repros.

Validation passed:
- `cargo test --lib -- cop::rspec::leaky_local_variable`
- `python3 scripts/check_cop.py RSpec/LeakyLocalVariable --rerun --clone --sample 15`

Corpus gate result: no new regressions, 5 sampled FNs resolved. Commit: `bf1a8f74`.

Changes

  2 files changed
Agent conversation excerpt

Reading docs/agent-ci.md first, then the current Rust cop and RuboCop ground-truth files to isolate the missed FN pattern before I touch fixtures.

Reading docs/agent-ci.md first, then the current Rust cop and RuboCop ground-truth files to isolate the missed FN pattern before I touch fixtures.

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

exec_command: sed -n '1,260p' src/cop/rspec/leaky_local_variable.rs

exec_command: sed -n '1,260p' vendor/rubocop-rspec/lib/rubocop/cop/rspec/leaky_local_variable.rb

exec_command: sed -n '1,260p' vendor/rubocop-rspec/spec/rubocop/cop/rspec/leaky_local_variable_spec.rb

I’ve got the workflow and the current cop history. Next I’m reading the rest of the Rust implementation and then I’ll reproduce the reported FN with a fixture-first test so the failure is concrete.

I’ve got the workflow and the current cop history. Next I’m reading the rest of the Rust implementation and then I’ll reproduce the reported FN with a fixture-first test so the failure is concrete.

exec_command: sed -n '261,620p' src/cop/rspec/leaky_local_variable.rs

exec_command: sed -n '261,520p' vendor/rubocop-rspec/spec/rubocop/cop/rspec/leaky_local_variable_spec.rb

exec_command: rg -n "def .*shared_examples|module_def = nil|join_table_name = 'object_query_5'|insert_tee_log|test_tarball" tests/fixtures/cops/rspec/leaky_local_variable src/cop/rspec/leaky_local_variable.rs vendo

The missing corpus patterns are already present in the offense fixture. I’m checking whether they’re correctly annotated and whether the current unit test actually misses them, which will tell us if this is a real detection bug or a fixture/config mismatch.

The missing corpus patterns are already present in the offense fixture. I’m checking whether they’re correctly annotated and whether the current unit test actually misses them, which will tell us if this is a real detection bug or a fixture/config mismatch.

exec_command: sed -n '340,410p' tests/fixtures/cops/rspec/leaky_local_variable/offense.rb

exec_command: sed -n '1,260p' tests/fixtures/cops/rspec/leaky_local_variable/no_offense.rb

exec_command: cargo test --lib -- cop::rspec::leaky_local_variable

The fixture already contains the reported FN snippets, which means the next question is whether the existing test currently fails. If it already passes, this is not a cop-logic bug and I’ll switch to confirming why corpus/context diverged.

The fixture already contains the reported FN snippets, which means the next question is whether the existing test currently fails. If it already passes, this is not a cop-logic bug and I’ll switch to confirming why corpus/context diverged.

write_stdin: ``

write_stdin: ``

write_stdin: ``

write_stdin: ``

Compilation is still running. While that finishes, I’m tracing the current implementation around file-level and nested-scope assignment handling, because the reported missed examples cluster around top-level assignments, nested contexts, and def/block wrappers.

Compilation is still running. While that finishes, I’m tracing the current implementation around file-level and nested-scope assignment handling, because the reported missed examples cluster around top-level assignments, nested contexts, and def/block wrappers.

exec_command: rg -n "check_scope_for_leaky_vars|check_var_used_in_example_scopes|check_var_used_in_describe_blocks|stmt_example_scope_var_interaction|collect_assignments_in_scope|node_references_var|ImplicitNode|ch

exec_command: sed -n '620,1160p' src/cop/rspec/leaky_local_variable.rs

exec_command: sed -n '1160,1765p' src/cop/rspec/leaky_local_variable.rs

exec_command: sed -n '1765,3260p' src/cop/rspec/leaky_local_variable.rs

exec_command: sed -n '3260,3340p' src/cop/rspec/leaky_local_variable.rs

write_stdin: ``

The unit test already fails exactly where expected, so this is a real detection gap. The missing offenses are the appended corpus patterns at the end of offense.rb; I’m mapping those line numbers to the AST traversal that currently drops them.

The unit test already fails exactly where expected, so this is a real detection gap. The missing offenses are the appended corpus patterns at the end of offense.rb; I’m mapping those line numbers to the AST traversal that currently drops them.

exec_command: nl -ba tests/fixtures/cops/rspec/leaky_local_variable/offense.rb | sed -n '320,350p'

exec_command: sed -n '1,220p' src/testutil.rs

exec_command: rg -n "run_cop_full|cop_fixture_tests|offense_fixture" src/testutil.rs src/cop/rspec/leaky_local_variable.rs

exec_command: nl -ba tests/fixtures/cops/rspec/leaky_local_variable/offense.rb | sed -n '350,410p'

I found why the fixture is red: the tail of offense.rb only has bare assignment lines, so those expectations can never pass. I’m turning those placeholders into real minimal reproductions from the corpus examples, then I’ll use that failing fixture to drive the implementation fix.

I found why the fixture is red: the tail of offense.rb only has bare assignment lines, so those expectations can never pass. I’m turning those placeholders into real minimal reproductions from the corpus examples, then I’ll use that failing fixture to drive the implementation fix.

exec_command: rg -n "ruby_prism|parse_result|debug.*ast|dump.*prism|NodeInspector|pretty.*prism|parse.*source" src scripts tests -g '!target'

exec_command: python3 scripts/investigate_cop.py RSpec/LeakyLocalVariable --context | sed -n '1,260p'

Plan:

  1. Replace the placeholder tail in offense.rb with minimal real repros for the diagnosed FN cases from the prompt.
  2. Re-run cargo test --lib -- cop::rspec::leaky_local_variable to see which patterns still fail after the fixture is valid.
  3. Trace the missed AST path in src/cop/rspec/leaky_local_variable.rs, patch the narrow traversal/dataflow gap, and add a short /// note documenting the fix.
  4. Re-run the focused test, then python3 scripts/check_cop.py RSpec/LeakyLocalVariable --rerun --clone --sample 15, and commit only the cop files if the corpus check is clean.

Plan:

  1. Replace the placeholder tail in offense.rb with minimal real repros for the diagnosed FN cases from the prompt.
  2. Re-run cargo test --lib -- cop::rspec::leaky_local_variable to see which patterns still fail after the fixture is valid.
  3. Trace the missed AST path in src/cop/rspec/leaky_local_variable.rs, patch the narrow traversal/dataflow gap, and add a short /// note documenting the fix.
  4. Re-run the focused test, then python3 scripts/check_cop.py RSpec/LeakyLocalVariable --rerun --clone --sample 15, and commit only the cop files if the corpus check is clean.

write_stdin: ``

exec_command: rg -n "RSPEC_EXAMPLE_GROUPS|is_rspec_example_group|shared_examples|shared_context" src/cop/util.rs src/cop -g '!target'

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

I’m updating offense.rb now: removing the impossible placeholder annotations at the end and replacing them with actual minimal reproductions for the diagnosed FN cases from the prompt.

I’m updating offense.rb now: removing the impossible placeholder annotations at the end and replacing them with actual minimal reproductions for the diagnosed FN cases from the prompt.

apply_patch

The fixture now contains real repros instead of disconnected assignments. I’m rerunning the focused test to see which of these patterns the cop still misses before touching Rust.

The fixture now contains real repros instead of disconnected assignments. I’m rerunning the focused test to see which of these patterns the cop still misses before touching Rust.

exec_command: cargo test --lib -- cop::rspec::leaky_local_variable

write_stdin: ``

write_stdin: ``

write_stdin: ``

write_stdin: ``

While the rebuild finishes, I’m checking the likely gap: the cop’s assignment collectors only recurse through a hand-picked set of structural nodes. Any assignment embedded in a general expression subtree can still disappear, which would explain the high-FN repos like cenit and some of these new repros.

Task prompt (16359 tokens)

Fix RSpec/LeakyLocalVariable — 11 FP, 99 FN

Instructions

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

Current state: 5,422 matches, 11 false positives, 99 false negatives.
Focus on: FN (RuboCop flags code nitrocop misses).

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

Mixed issues: some code bugs, some config issues

Pre-diagnostic shows SOME patterns are correctly detected in isolation (config issues)
and SOME are genuinely missed (code bugs). See the per-example diagnosis below.

  • For examples marked CODE BUG: follow the standard TDD workflow
  • For examples marked CONFIG/CONTEXT: investigate config resolution, not detection logic

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 RSpec/LeakyLocalVariable /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/rspec/leaky_local_variable.rs and tests/fixtures/cops/rspec/leaky_local_variable/
  • Run cargo test --lib -- cop::rspec::leaky_local_variable to verify your fix (do NOT run the full test suite)
  • Run python3 scripts/check_cop.py RSpec/LeakyLocalVariable --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

Prism Notes

  • hash splits into HashNode (literal {}) and KeywordHashNode (keyword args foo(a: 1)). If you handle one, check if you need the other.
  • begin is overloaded: explicit begin..end is BeginNode, implicit method body is StatementsNode. Check which one(s) your cop needs.

Current Fixture: offense.rb

tests/fixtures/cops/rspec/leaky_local_variable/offense.rb

# Variable used in before hook
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  before { user.update(admin: true) }
end

# Variable used in it block
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'updates the user' do
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Variable used in let
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  let(:my_user) { user }
end

# Variable used in subject
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  subject { user }
end

# Variable used as it_behaves_like second argument
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it_behaves_like 'some example', user
end

# Variable used as part of it_behaves_like argument (array)
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it_behaves_like 'some example', [user, user2]
end

# Block parameter reassigned outside example scope
shared_examples 'some examples' do |subject|
  subject = SecureRandom.uuid
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'renders the subject' do
    expect(mail.subject).to eq(subject)
  end
end

# Variable used in interpolation inside example block body
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'does something' do
    expect("foo_#{user.name}").to eq("foo_bar")
  end
end

# Variable used in both description and block body
describe SomeClass do
  article = foo ? 'a' : 'the'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it "updates #{article} user" do
    user.update(preferred_article: article)
  end
end

# Variable used in nested context's example
describe SomeClass do
  template_params = { name: 'sample_confirmation' }
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  describe '#perform' do
    context 'when valid' do
      it 'sends template' do
        message = create(:message, params: template_params)
        described_class.new(message: message).perform
      end
    end
  end
end

# Variable used in nested context's around hook
shared_examples 'sentinel support' do
  prefix = 'redis'
  ^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  context 'when configuring' do
    around do |example|
      ClimateControl.modify("#{prefix}_PASSWORD": 'pass') { example.run }
    end
  end
end

# Variable used in skip metadata AND block body
describe SomeClass do
  skip_message = 'not yet implemented'
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'does something', skip: skip_message do
    puts skip_message
  end
end

# Variable used as include_context non-first argument
describe SomeClass do
  config = { key: 'value' }
  ^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  include_context 'shared setup', config
end

# Variable used inside include_context block
describe SomeClass do
  payload = build(:payload)
  ^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  include_context 'authenticated' do
    let(:data) { payload }
  end
end

# Variable used in it block AND reassigned after use
describe SomeClass do
  user = create(:user)
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'updates the user' do
    expect { user.update(admin: true) }.to change(user, :updated_at)
    user = create(:user)
  end
end

# Variable assigned outside describe block, used in before hook
user = create(:user)
^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

describe SomeClass do
  before { user.update(admin: true) }
end

# Variable assigned outside describe block, used in example
record = build(:record)
^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

RSpec.describe SomeClass do
  it 'validates the record' do
    expect(record).to be_valid
  end
end

# Variable overwritten at scope level — only last assignment is offense (FP fix)
# The first `result = nil` is dead; only `result = compute()` leaks.
describe SomeClass do
  result = nil
  result = compute()
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'checks the result' do
    expect(result).to eq(42)
  end
end

# Variable overwritten with intervening non-reading statement — only last is offense
describe SomeClass do
  count = 0
  do_something
  count = items.size
  ^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'has the right count' do
    expect(count).to eq(5)
  end
end

# Variable used via operator-assign (+=) inside example block
describe SomeClass do
  total = 10
  ^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'increments the total' do
    total += 5
    expect(total).to eq(15)
  end
end

# Variable used via operator-assign (-=) inside hook
describe SomeClass do
  counter = 100
  ^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  before do
    counter -= 1
  end

  it 'checks counter' do
    expect(counter).to eq(99)
  end
end

# Variable used in interpolated regex inside example
describe SomeClass do
  pattern = 'foo'
  ^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it 'matches the pattern' do
    expect('foobar').to match(/#{pattern}/)
  end
end

# Dead file-level assignments are NOT flagged; only the last unconditional
# assignment before the describe-block reference is live. (dev-sec pattern)
flags = parse_config('/proc/cpuinfo').flags
flags ||= ''
flags = flags.split(' ')
^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

describe '/proc/cpuinfo' do
  it 'Flags should include NX' do
    expect(flags).to include('nx')
  end
end

# Variables inside .each blocks used in nested example scopes
describe "iterator block" do
  [1, 2].each do |v|
    val = v.to_s
    ^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
    context "when val=#{val}" do
      it "works" do
        expect(val).to eq(v.to_s)
      end
    end
  end
end

# File-level variable assigned in if/elsif branches, used in describe block
root_group = 'root'
^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

if os == 'aix'
  root_group = 'system'
  ^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
elsif os == 'freebsd'
  root_group = 'wheel'
  ^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
end

describe SomeClass do
  its('groups') { should include root_group }
end

# Variable assigned in if-condition, used in let block
describe SomeClass do
  specs.each do |spec|
    context spec['name'] do
      if error = spec['error']
         ^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
        let(:expected_error) { error }

        it 'fails' do
          expect { run }.to raise_error(expected_error)
        end
      end
    end
  end
end

# Variable assigned before non-RSpec block containing RSpec.describe
describe SomeClass do
  max_count = 4
  ^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  with_new_environment do
    spec = RSpec.describe "SomeTest" do
      it "test" do
        expect(max_count).to eq(4)
      end
    end

    spec.run
  end
end

# Ruby 3.1 keyword shorthand: `method(url:)` is shorthand for `method(url: url)`
# Prism wraps the value in an ImplicitNode containing a LocalVariableReadNode.
describe "Feed importing" do
  url = "feed02/feed.xml"
  ^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it "imports stories" do
    server = create_server(url:)
  end
end

# Ruby 3.1 keyword shorthand with multiple shorthand args
describe "#update" do
  headers = { "CONTENT_TYPE" => "application/json" }
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  it "marks a story as read" do
    put("/stories/#{story.id}", headers:)
  end
end

# Ruby 3.1 keyword shorthand in before hook
describe "fetching" do
  last_fetched = Time.parse("2014-08-12T00:01:00Z")
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

  before do
    create_feed(last_fetched:)
  end
end

# def self.method with variables leaking into example scopes via .each
describe "dynamic cases" do
  def self.define_cases(items)
    items.each do |label, value|
      result = value.upcase
      ^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
      context label do
        it { expect(something).to eq(result) }
      end
    end
  end
end

# def method with variables leaking into RSpec.describe inside a block
describe "instance method" do
  def run_test
    counter = 0
    ^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
    with_new_rspec_environment do
      RSpec.describe "inner" do
        it { expect(counter).to eq(0) }
      end
    end
  end
end

# def self.method with direct example scopes (no wrapping describe)
describe "direct examples in def self" do
  def self.it_is_correct_for(label, expected)
    result = expected.to_s
    ^^^^^^^^^^^^^^^^^^^^^^^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.
    it "is correct for #{label}" do
      expect(compute).to eq(result)
    end
  end
end

Current Fixture: no_offense.rb

tests/fixtures/cops/rspec/leaky_local_variable/no_offense.rb

# Variable assigned inside example
describe SomeClass do
  it 'updates the user' do
    user = create(:user)
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Variable used only as it description
describe SomeClass do
  description = "updates the user"
  it description do
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Variable used only in it description interpolation
describe SomeClass do
  article = foo ? 'a' : 'the'
  it "updates #{article} user" do
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Block parameter used in example (not reassigned)
shared_examples 'some examples' do |subject|
  it 'renders the subject' do
    expect(mail.subject).to eq(subject)
  end
end

# Block keyword parameter used in example
shared_examples 'some examples' do |subject:|
  it 'renders the subject' do
    expect(mail.subject).to eq(subject)
  end
end

# Block parameter reassigned inside example
shared_examples 'some examples' do |subject|
  it 'renders the subject' do
    subject = 'hello'
    expect(mail.subject).to eq(subject)
  end
end

# Two variables same name in different scopes
describe SomeClass do
  let(:my_user) do
    user = create(:user)
    user.flag!
    user
  end

  it 'updates the user' do
    user = create(:user)
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Variable not referenced in any example
describe SomeClass do
  user = create(:user)
  user.flag!

  it 'does something' do
    expect(foo).to eq(bar)
  end
end

# Variable used as first it_behaves_like argument (shared example name)
describe SomeClass do
  examples = foo ? 'definite article' : 'indefinite article'
  it_behaves_like examples
end

# Variable used in interpolation for it_behaves_like argument
describe SomeClass do
  article = foo ? 'a' : 'the'
  it_behaves_like 'some example', "#{article} user"
end

# Variable used in symbol interpolation for it_behaves_like argument
describe SomeClass do
  article = foo ? 'a' : 'the'
  it_behaves_like 'some example', :"#{article}_user"
end

# Block argument shadowed by local variable in iterator
describe SomeClass do
  %i[user user2].each do |user|
    let(user) do
      user = create(:user)
      user.flag!
      user
    end
  end
end

# Outside of a describe block (FactoryBot)
FactoryBot.define :foo do
  bar = 123

  after(:create) do |foo|
    foo.update(bar: bar)
  end
end

# Variable used only in skip metadata
describe SomeClass do
  skip_message = 'not yet implemented'

  it 'does something', skip: skip_message do
    expect(1 + 2).to eq(3)
  end
end

# Variable used only in pending metadata
describe SomeClass do
  pending_message = 'work in progress'

  it 'does something', pending: pending_message do
    expect(1 + 2).to eq(3)
  end
end

# Variable reassigned before use inside example (VariableForce scoping)
describe SomeClass do
  user = create(:user)

  it 'updates the user' do
    user = create(:user)
    expect { user.update(admin: true) }.to change(user, :updated_at)
  end
end

# Variable used only as first include_context argument (context name)
describe SomeClass do
  ctx = condition ? 'admin context' : 'user context'
  include_context ctx
end

# Variable used in interpolated string for include_context argument
describe SomeClass do
  role = 'admin'
  include_context 'shared setup', "#{role} context"
end

# Variable reassigned inside begin block before use (VariableForce)
describe SomeClass do
  user = create(:user)

  it 'updates the user' do
    begin
      user = create(:other_user)
      expect(user).to be_valid
    end
  end
end

# Variable used only as first arg to include_examples (the shared group name)
describe SomeClass do
  name = condition ? 'admin' : 'user'
  include_examples name
end

# Variable used only as first arg to it_should_behave_like
describe SomeClass do
  behavior = condition ? 'creates record' : 'updates record'
  it_should_behave_like behavior
end

# Variable overwritten in nested context — outer assignment dead, not used in examples
# The outer assignment's value is never read by any example scope; the variable
# is only used at group level.
describe Outer do
  config = { default: true }
  validate(config)

  context 'custom config' do
    it 'does something' do
      expect(1).to eq(1)
    end
  end
end

# Variable assigned inside iterator block param, NOT a group-level assignment
describe SomeClass do
  items.each do |item|
    item = transform(item)
    process(item)
  end

  it 'works' do
    expect(result).to eq(true)
  end
end

# Operator-assign at group level, variable NOT used in example scope
describe SomeClass do
  counter = 0
  counter += items.size

  it 'does something unrelated' do
    expect(1 + 2).to eq(3)
  end
end

# File-level variable referenced only at group level (not in example scope).
# No offense for the file-level assignment.
payload = build(:payload)

describe SomeClass do
  payload.validate  # used at group level, not in example scope

  it 'works' do
    expect(1).to eq(1)
  end
end

# File-level variable NOT referenced in any example scope — no offense.
status = :inactive

describe OtherClass do
  status  # referenced at group level only, not inside any example scope

  it 'does something' do
    expect(true).to eq(true)
  end
end

# Variable initialized at group scope, reassigned in before hook (VariableForce: dead assignment)
# RuboCop's VariableForce tracks that the before hook reassigns the variable before
# any example reads it (hooks run before examples), making the group-level value dead.
describe SomeClass do
  result = nil

  before :each do
    result = compute_something()
  end

  it 'checks the result' do
    expect(result).to eq(42)
  end
end

# Variable initialized at group scope, reassigned in before hook, read in multiple its
describe SomeClass do
  response = nil

  before do
    response = make_request()
  end

  it 'returns a response' do
    expect(response).to be_instance_of(Response)
  end

  it 'has a body' do
    expect(response.body).to eq('ok')
  end
end

# Variable initialized at group scope, reassigned in before :all hook
describe SomeClass do
  path = nil

  before :all do
    path = Dir.mktmpdir('test')
  end

  it 'uses the path' do
    expect(File.exist?(path)).to be true
  end
end

# Variable reassigned in first it block, read in second it block
# VariableForce sees linear flow: group assign -> it1 reassign -> it2 read
# and attributes the read to the it1 assignment, not the group assignment.
describe SomeClass do
  data = []

  it 'populates data' do
    data = [1, 2, 3]
  end

  it 'checks data' do
    expect(data).to eq([1, 2, 3])
  end
end

# Variable assigned inside iterator block, shadowed by block param in later iterator
# (openproject pattern: schema_name assigned in .each block, then used in a different
# .each block where schema_name is a block parameter — the block param shadows the var)
describe SomeClass do
  items.each do |item|
    schema_name = item.name
    registry[schema_name] = item
  end

  registry.each do |schema_name, item|
    describe schema_name do
      let(:schema) { load_schema(schema_name) }

      it "validates #{schema_name}" do
        expect(item).to match_schema(schema)
      end
    end
  end
end

# Variable assigned inside non-RSpec DSL method block (rswag pattern)
# post/response/path are DSL methods, not RSpec example groups or scopes.
# Variables assigned inside them and used only at the same DSL scope level
# (not inside example scopes) should not be flagged.
describe SomeClass do
  path "/api/resource" do
    post "Create resource" do
      expected_schema = load_schema("create_request")
      parameter name: :params, in: :body, schema: expected_schema

      response "200", "success" do
        expected_schema = load_schema("create_response")
        schema expected_schema

        xit
      end
    end
  end
end

# File-level variable used in non-describe-block scope (Capybara::SpecHelper.spec)
# The spec method with a receiver is NOT an RSpec example group.
# Variables assigned inside it blocks should not be collected as file-level vars.
Capybara::SpecHelper.spec '#ancestor' do
  before do
    @session.visit('/with_html')
  end

  it 'should find the element' do
    el = @session.find(:css, '#child')
    expect(el.ancestor('//p')).to have_text('Lorem ipsum')
  end

  it 'should raise on multiple matches' do
    el = @session.find(:css, '#child')
    expect { el.ancestor('//div') }.to raise_error(Capybara::Ambiguous)
  end
end

# Variable assigned inside .each at group scope, used only in example description
# (jruby pattern: format = "%" + f, used in it "supports #{format}")
describe SomeClass do
  %w(d i).each do |f|
    format = "%" + f

    it "supports integer formats using #{format}" do
      ("%#{f}" % 10).should == "10"
    end
  end
end

# Sibling block scope: same-named variable in sibling non-RSpec blocks.
# The post block has its own expected_schema that is NOT used in any example scope.
# The get block also has expected_schema that IS used in example scopes (separate offense).
# The post block's variable should NOT be flagged — it's a different local binding.
# (discourse/rswag pattern)
describe SomeClass do
  path "/api" do
    post "Create" do
      expected_schema = load_schema("create")
      parameter name: :params, schema: expected_schema
      response "200" do
        xit
      end
    end
  end
end

# Variable initialized to nil, reassigned inside nested expect block in example.
# (excon pattern: response = nil, then response = make_request() inside expect do end)
describe SomeClass do
  response = nil

  it 'returns a response' do
    expect do
      response = make_request()
    end.to_not raise_error
  end

  it 'has status' do
    expect(response.status).to eq(200)
  end
end

# Variable initialized to empty array, reassigned via lambda in example body.
# (excon pattern: data = [], then data = [...] inside lambda/block)
describe SomeClass do
  data = []
  it 'yields data' do
    response_block = lambda do |chunk, remaining, total|
      data = [chunk, remaining, total]
    end
    conn.request(response_block: response_block)
  end
  it 'has expected data' do
    expect(data).to eq(['x', 0, 100])
  end
end

# Variable inside shared_examples block (not file-level)
RSpec.shared_examples "permitted roles" do |**kwargs|
  to = kwargs.delete(:to)
  label = kwargs.except(:to).map { |key, value| "#{key} is #{value}" }.join(" AND ")

  Array(to).each do |role|
    context "#{label} #{role.inspect} authorization" do
      let(:user) { public_send(role) }
      it { is_expected.to be_truthy }
    end
  end
end

# Variable used only in describe argument (group scope, not example scope)
describe SomeClass do
  v = SomeModule::SOME_CONSTANT
  describe "with value #{v}" do
    subject { described_class.new }
    it { is_expected.to be_valid }
  end
end

# Variable used as argument to nested describe (ConstantPathNode)
# RuboCop's part_of_example_scope? doesn't match describe arguments
RSpec.describe(SomeClass) do
  result = described_class

  describe result::Success do
    it "works" do
      expect(true).to be true
    end
  end
end

# Variable used only in context metadata (group scope, not example scope)
describe SomeClass do
  exclude_test = some_platform?
  describe "platform tests", skip: exclude_test do
    it "works" do
      expect(true).to be true
    end
  end
end

# Variable used only in shared_examples_for block (not file-level)
shared_examples_for "a testable resource" do |testcase|
  context_name = "With mode #{testcase[:mode]}"
  context context_name do
    let(:resource) { build_resource(testcase) }
    it "applies correctly" do
      expect(resource).to be_valid
    end
  end
end

Key Source Files

  • Rust implementation: src/cop/rspec/leaky_local_variable.rs
  • RuboCop Ruby source (ground truth): vendor/rubocop-rspec/lib/rubocop/cop/rspec/leaky_local_variable.rb
  • RuboCop test excerpts: vendor/rubocop-rspec/spec/rubocop/cop/rspec/leaky_local_variable_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 RSpec/LeakyLocalVariable --repos-only
  • python3 scripts/investigate_cop.py RSpec/LeakyLocalVariable --context
  • python3 scripts/verify_cop_locations.py RSpec/LeakyLocalVariable

Top FP repos:

  • bdewey__org-ruby__e4a6e7d (4 FP) — example spec/parser_spec.rb:132
  • puppetlabs__puppetlabs-docker__04382c9 (2 FP) — example spec/acceptance/docker_full_spec.rb:43
  • robotdana__leftovers__1f40b1a (2 FP) — example spec/config_built_in_spec.rb:21

Top FN repos:

  • cenit-io__cenit__0106da3 (18 FN) — example spec/integration/mongoff/validator_spec.rb:44
  • saml-idp__saml_idp__4dde1a3 (7 FN) — example spec/lib/saml_idp/incoming_metadata_spec.rb:5
  • tobymao__18xx__8f5e103 (7 FN) — example spec/lib/engine/game/fixtures_auto_actions_spec.rb:22

Representative FP examples:

  • bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:132 — Do not use local variables defined outside of examples inside of them.
  • bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:154 — Do not use local variables defined outside of examples inside of them.
  • bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:214 — Do not use local variables defined outside of examples inside of them.

Representative FN examples:

  • DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:15 — Do not use local variables defined outside of examples inside of them.
  • DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:16 — Do not use local variables defined outside of examples inside of them.
  • DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:62 — Do not use local variables defined outside of examples inside of them.

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)
  • FP: 6 context-dependent
  • Omitted 5 pre-diagnostic FP example(s) with no source context because diagnosed FP examples were available

FN #1: DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:15

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

Full-file enclosing chain: block (do..end) (line 13: context '#Work with proper query' do) > block (do..end) (line 8: RSpec.describe Database::Multiple, '#multiple' do)

Message: Do not use local variables defined outside of examples inside of them.

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

    join_table_name = 'object_query_5'
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

        1: # frozen_string_literal: true
        2: 
        3: require 'spec_helper'
        4: 
        5: require './lib/anonymizer/model/database.rb'
        6: require './lib/anonymizer/model/database/multiple.rb'
        7: 
        8: RSpec.describe Database::Multiple, '#multiple' do
        9:   it 'should exists class Multiple' do
       10:     expect(Object.const_defined?('Database::Multiple')).to be true
       11:   end
       12: 
       13:   context '#Work with proper query' do
       14:     table_name = 'object_store_2'
>>>    15:     join_table_name = 'object_query_5'
       16:     join_table_column = 'oo_id'
       17:     column_name = 'email'
       18:     info = JSON.parse("
       19:         {
       20:           \"type\": \"email\",
       21:           \"action\": \"multiple_update\",
       22:           \"linked_tables\": {

FN #2: DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:16

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

Full-file enclosing chain: block (do..end) (line 13: context '#Work with proper query' do) > block (do..end) (line 8: RSpec.describe Database::Multiple, '#multiple' do)

Message: Do not use local variables defined outside of examples inside of them.

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

    join_table_column = 'oo_id'
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

        1: # frozen_string_literal: true
        2: 
        3: require 'spec_helper'
        4: 
        5: require './lib/anonymizer/model/database.rb'
        6: require './lib/anonymizer/model/database/multiple.rb'
        7: 
        8: RSpec.describe Database::Multiple, '#multiple' do
        9:   it 'should exists class Multiple' do
       10:     expect(Object.const_defined?('Database::Multiple')).to be true
       11:   end
       12: 
       13:   context '#Work with proper query' do
       14:     table_name = 'object_store_2'
       15:     join_table_name = 'object_query_5'
>>>    16:     join_table_column = 'oo_id'
       17:     column_name = 'email'
       18:     info = JSON.parse("
       19:         {
       20:           \"type\": \"email\",
       21:           \"action\": \"multiple_update\",
       22:           \"linked_tables\": {
       23:             \"#{join_table_name}\": {

FN #3: DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:62

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

Full-file enclosing chain: block (do..end) (line 60: context '#Work with linked tables' do) > block (do..end) (line 8: RSpec.describe Database::Multiple, '#multiple' do)

Message: Do not use local variables defined outside of examples inside of them.

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

    join_table_name = 'object_query_5'
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

       32:       expect(Database::Multiple.query(table_name, column_name, info)).to eq(
       33:         [
       34:           "UPDATE #{table_name} SET #{column_name} = (" \
       35:           'SELECT fake_user.email FROM fake_user ' \
       36:           "ORDER BY RAND() LIMIT 1) WHERE #{table_name}.#{column_name} IS NOT NULL",
       37:           "UPDATE #{join_table_name} as t1 INNER JOIN #{table_name} as t2 ON t1.#{join_table_column} " \
       38:           "SET t1.#{column_name} = t2.#{column_name} WHERE t1.#{column_name} IS NOT NULL"
       39:         ]
       40:       )
       41:     end
       42:   end
       43: 
       44:   context '#Manage id type' do
       45:     type = 'id'
       46:     it 'should get random value' do
       47:       expect(Database::Multiple.manage_type(type)).to eq('SELECT FLOOR((NOW() + RAND()) * (RAND() * 119))) ')
       48:     end
       49:   end
       50: 
       51:   context '#Manage non-id type' do
       52:     type = 'email'
       53:     it 'should get proper value for non-id type' do
       54:       expect(Database::Multiple.manage_type(type)).to eq(
       55:         'SELECT fake_user.email FROM fake_user ORDER BY RAND() LIMIT 1) '
       56:       )
       57:     end
       58:   end
       59: 
       60:   context '#Work with linked tables' do
       61:     table_name = 'object_store_2'
>>>    62:     join_table_name = 'object_query_5'
       63:     join_table_column = 'oo_id'
       64:     column_name = 'email'
       65:     linked_tables = JSON.parse("
       66:       {
       67:         \"#{join_table_name}\": {
       68:           \"column\": \"#{join_table_column}\"
       69:         }

FN #4: DivanteLtd__anonymizer__694eb35: spec/unit/model/database/multiple_spec.rb:63

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

Full-file enclosing chain: block (do..end) (line 60: context '#Work with linked tables' do) > block (do..end) (line 8: RSpec.describe Database::Multiple, '#multiple' do)

Message: Do not use local variables defined outside of examples inside of them.

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

    join_table_column = 'oo_id'
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

       33:         [
       34:           "UPDATE #{table_name} SET #{column_name} = (" \
       35:           'SELECT fake_user.email FROM fake_user ' \
       36:           "ORDER BY RAND() LIMIT 1) WHERE #{table_name}.#{column_name} IS NOT NULL",
       37:           "UPDATE #{join_table_name} as t1 INNER JOIN #{table_name} as t2 ON t1.#{join_table_column} " \
       38:           "SET t1.#{column_name} = t2.#{column_name} WHERE t1.#{column_name} IS NOT NULL"
       39:         ]
       40:       )
       41:     end
       42:   end
       43: 
       44:   context '#Manage id type' do
       45:     type = 'id'
       46:     it 'should get random value' do
       47:       expect(Database::Multiple.manage_type(type)).to eq('SELECT FLOOR((NOW() + RAND()) * (RAND() * 119))) ')
       48:     end
       49:   end
       50: 
       51:   context '#Manage non-id type' do
       52:     type = 'email'
       53:     it 'should get proper value for non-id type' do
       54:       expect(Database::Multiple.manage_type(type)).to eq(
       55:         'SELECT fake_user.email FROM fake_user ORDER BY RAND() LIMIT 1) '
       56:       )
       57:     end
       58:   end
       59: 
       60:   context '#Work with linked tables' do
       61:     table_name = 'object_store_2'
       62:     join_table_name = 'object_query_5'
>>>    63:     join_table_column = 'oo_id'
       64:     column_name = 'email'
       65:     linked_tables = JSON.parse("
       66:       {
       67:         \"#{join_table_name}\": {
       68:           \"column\": \"#{join_table_column}\"
       69:         }
       70:       }

FN #5: MissionCriticalCloud__vagrant-cloudstack__e09bfb4: functional-tests/vmlifecycle/vmlifecycle_spec.rb:11

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

Message: Do not use local variables defined outside of examples inside of them.

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

insert_tee_log = '  2>&1 | tee -a vagrant.log ' if which('tee')
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

        1: def which(cmd)
        2:   exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
        3:   ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
        4:     exts.each { |ext|
        5:       exe = File.join(path, "#{cmd}#{ext}")
        6:       return exe if File.executable?(exe) && !File.directory?(exe)
        7:     }
        8:   end
        9:   return nil
       10: end
>>>    11: insert_tee_log = '  2>&1 | tee -a vagrant.log ' if which('tee')
       12: 
       13: describe 'VM Life Cycle' do
       14:   it 'starts Linux and Windows VM' do
       15:     expect(`vagrant up  #{insert_tee_log}`).to include(
       16:       'linux-box: Machine is booted and ready for use!',
       17:       'windows-box: Machine is booted and ready for use!'
       18:     )

FN #6: OpenVoxProject__openvox__2665666: spec/unit/pops/types/ruby_generator_spec.rb:421

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

Full-file enclosing chain: block (do..end) (line 420: context 'when generating static code' do) > block (do..end) (line 210: context 'when generating from Object types' do) > block (do..end) (line 12: describe 'Puppet Ruby Generator' do)

Prism AST at offense line: StatementsNode > LocalVariableWriteNode > NilNode

Message: Do not use local variables defined outside of examples inside of them.

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

      module_def = nil
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      391:       end
      392: 
      393:       context 'creates an instance' do
      394:         it 'that the TypeCalculator infers to the Object type' do
      395:           expect(TypeCalculator.infer(first.from_hash('name' => 'Bob Builder'))).to eq(first_type)
      396:         end
      397: 
      398:         it "where attributes of kind 'reference' are not considered part of #_pcore_all_contents" do
      399:           inst = first.from_hash('name' => 'Bob Builder')
      400:           wrinst = second.create('Bob Builder', '42 Cool Street', '12345', 'bob@example.com', 23, 40, inst, inst)
      401:           results = []
      402:           wrinst._pcore_all_contents([]) { |v| results << v }
      403:           expect(results).to eq([inst])
      404:         end
      405:       end
      406: 
      407:       context 'when used from Puppet' do
      408:         let(:type_usage) { <<-PUPPET.unindent }
      409:           $i = MyModule::FirstGenerated('Bob Builder', 52)
      410:           notice($i['name'])
      411:           notice($i['age'])
      412:         PUPPET
      413: 
      414:         it 'The [] method is present on a created instance' do
      415:           expect(notices).to eql(['Bob Builder', '52'])
      416:         end
      417:       end
      418:     end
      419: 
      420:     context 'when generating static code' do
>>>   421:       module_def = nil
      422: 
      423:       before(:each) do
      424:         # Ideally, this would be in a before(:all) but that is impossible since lots of Puppet
      425:         # environment specific settings are configured by the spec_helper in before(:each)
      426:         if module_def.nil?
      427:           first_type = nil
      428:           second_type = nil

FN #7: OpenVoxProject__openvox__2665666: spec/unit/pops/types/ruby_generator_spec.rb:733

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

Full-file enclosing chain: block (do..end) (line 732: context 'when generating static code' do) > block (do..end) (line 532: context 'when generating from TypeSets' do) > block (do..end) (line 12: describe 'Puppet Ruby Generator' do)

Prism AST at offense line: StatementsNode > LocalVariableWriteNode > NilNode

Message: Do not use local variables defined outside of examples inside of them.

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

      module_def = nil
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      703:         it 'creates an instance of the class' do
      704:           inst = first.from_hash('name' => 'Bob Builder', 'age' => 52)
      705:           expect(inst).to be_a(first)
      706:           expect(inst.name).to eq('Bob Builder')
      707:           expect(inst.age).to eq(52)
      708:         end
      709: 
      710:         it 'accepts an initializer where optional keys are missing' do
      711:           inst = first.from_hash('name' => 'Bob Builder')
      712:           expect(inst).to be_a(first)
      713:           expect(inst.name).to eq('Bob Builder')
      714:           expect(inst.age).to eq(30)
      715:         end
      716: 
      717:         it 'does not accept an initializer where optional values are nil and type does not accept nil' do
      718:           expect { first.from_hash('name' => 'Bob Builder', 'age' => nil) }.to(
      719:             raise_error(TypeAssertionError,
      720:               "MyModule::FirstGenerated initializer has wrong type, entry 'age' expects an Integer value, got Undef")
      721:           )
      722:         end
      723:       end
      724: 
      725:       context 'creates an instance' do
      726:         it 'that the TypeCalculator infers to the Object type' do
      727:           expect(TypeCalculator.infer(first.from_hash('name' => 'Bob Builder'))).to eq(first_type)
      728:         end
      729:       end
      730:     end
      731: 
      732:     context 'when generating static code' do
>>>   733:       module_def = nil
      734:       module_def2 = nil
      735: 
      736:       before(:each) do
      737:         # Ideally, this would be in a before(:all) but that is impossible since lots of Puppet
      738:         # environment specific settings are configured by the spec_helper in before(:each)
      739:         if module_def.nil?
      740:           eval_and_collect_notices(source) do

FN #8: SUSE__machinery__e41b642: spec/integration/support/inspect_unmanaged_files_examples.rb:40

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

Full-file enclosing chain: block (do..end) (line 19: describe "--scope=unmanaged-files" do) > block (do..end) (line 18: shared_examples "inspect unmanaged files" do |base, skip_remote_mounts_test|)

Message: Do not use local variables defined outside of examples inside of them.

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

    test_tarball = File.join(Machinery::ROOT, "../machinery/spec/definitions/vagrant/unmanaged_files.tgz")
^ RSpec/LeakyLocalVariable: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

       10: # GNU General Public License for more details.
       11: #
       12: # You should have received a copy of the GNU General Public License
       13: # along with this program; if not, contact SUSE LLC.
       14: #
       15: # To contact SUSE about this file by physical or electronic mail,
       16: # you may find current contact information at www.suse.com
       17: 
       18: shared_examples "inspect unmanaged files" do |base, skip_remote_mounts_test|
       19:   describe "--scope=unmanaged-files" do
       20:     let(:ignore_list) {
       21:       [
       22:         "/var/lib/logrotate.status",
       23:         "/var/spool/cron/lastrun/cron.daily",
       24:         "/boot/0x*",
       25:         "/var/log/sa",
       26:         "/root/.local",
       27:         "/etc/ssh",
       28:         "/var/log/mcelog",
       29:         "/var/lib/dpkg",
       30:         "/var/lib/apt",
       31:         "/var/lib/ureadahead",
       32:         "/var/log/upstart",
       33:         "/var/lib/systemd/coredump/"
       34:       ]
       35:     }
       36: 
       37:     def parse_md5sums(output)
       38:       output.split("\n").map { |e| e.split.first }
       39:     end
>>>    40:     test_tarball = File.join(Machinery::ROOT, "../machinery/spec/definitions/vagrant/unmanaged_files.tgz")
       41: 
       42:     it "extracts list of unmanaged files" do
       43:       measure("Inspect system") do
       44:         inspect_command = @machinery.run_command(
       45:           "FORCE_MACHINERY_PROGRESS_OUTPUT=true #{machinery_command} inspect " \
       46:             "#{@subject_system.ip} #{inspect_options if defined?(inspect_options)} " \
       47:             "--scope=unmanaged-files --extract-files " \

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

FP #1: bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:132

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

  describe "Export to Textile test cases" do
    data_directory = File.join(File.dirname(__FILE__), "textile_examples")
    org_files = File.expand_path(File.join(data_directory, "*.org" ))
    files = Dir.glob(org_files)
    files.each do |file|
      basename = File.basename(file, ".org")
      textile_name = File.join(data_directory, basename + ".textile")
      textile_name = File.expand_path(textile_name)

      it "should convert #{basename}.org to Textile" do
        expected = IO.read(textile_name)
        expected.should be_kind_of(String)
        parser = Orgmode::Parser.new(IO.read(file))
        actual = parser.to_textile

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      102:     valid_keywords.each do |kw|
      103:       it "should match custom keyword #{kw}" do
      104:         (kw =~ p.custom_keyword_regexp).should be_true
      105:       end
      106:     end
      107:     invalid_keywords.each do |kw|
      108:       it "should not match custom keyword #{kw}" do
      109:         (kw =~ p.custom_keyword_regexp).should be_nil
      110:       end
      111:     end
      112:     it "should not match blank as a custom keyword" do
      113:       ("" =~ p.custom_keyword_regexp).should be_nil
      114:     end
      115:   end
      116: 
      117:   describe "Custom include/exclude parser" do
      118:     fname = File.join(File.dirname(__FILE__), %w[html_examples export-tags.org])
      119:     p = Orgmode::Parser.load(fname)
      120:     it "should load tags" do
      121:       p.should have(2).export_exclude_tags
      122:       p.should have(1).export_select_tags
      123:     end
      124:   end
      125: 
      126:   describe "Export to Textile test cases" do
      127:     data_directory = File.join(File.dirname(__FILE__), "textile_examples")
      128:     org_files = File.expand_path(File.join(data_directory, "*.org" ))
      129:     files = Dir.glob(org_files)
      130:     files.each do |file|
      131:       basename = File.basename(file, ".org")
>>>   132:       textile_name = File.join(data_directory, basename + ".textile")
      133:       textile_name = File.expand_path(textile_name)
      134: 
      135:       it "should convert #{basename}.org to Textile" do
      136:         expected = IO.read(textile_name)
      137:         expected.should be_kind_of(String)
      138:         parser = Orgmode::Parser.new(IO.read(file))
      139:         actual = parser.to_textile

FP #2: bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:154

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

    # Dynamic generation of examples from each *.org file in html_examples.
    # Each of these files is convertable to HTML.
    data_directory = File.join(File.dirname(__FILE__), "html_examples")
    org_files = File.expand_path(File.join(data_directory, "*.org" ))
    files = Dir.glob(org_files)
    files.each do |file|
      basename = File.basename(file, ".org")
      textile_name = File.join(data_directory, basename + ".html")
      textile_name = File.expand_path(textile_name)

      it "should convert #{basename}.org to HTML" do
        expected = IO.read(textile_name)
        expected.should be_kind_of(String)
        parser = Orgmode::Parser.new(IO.read(file), { :allow_include_files => true })
        actual = parser.to_html

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      124:   end
      125: 
      126:   describe "Export to Textile test cases" do
      127:     data_directory = File.join(File.dirname(__FILE__), "textile_examples")
      128:     org_files = File.expand_path(File.join(data_directory, "*.org" ))
      129:     files = Dir.glob(org_files)
      130:     files.each do |file|
      131:       basename = File.basename(file, ".org")
      132:       textile_name = File.join(data_directory, basename + ".textile")
      133:       textile_name = File.expand_path(textile_name)
      134: 
      135:       it "should convert #{basename}.org to Textile" do
      136:         expected = IO.read(textile_name)
      137:         expected.should be_kind_of(String)
      138:         parser = Orgmode::Parser.new(IO.read(file))
      139:         actual = parser.to_textile
      140:         actual.should be_kind_of(String)
      141:         actual.should == expected
      142:       end
      143:     end
      144:   end
      145: 
      146:   describe "Export to HTML test cases" do
      147:     # Dynamic generation of examples from each *.org file in html_examples.
      148:     # Each of these files is convertable to HTML.
      149:     data_directory = File.join(File.dirname(__FILE__), "html_examples")
      150:     org_files = File.expand_path(File.join(data_directory, "*.org" ))
      151:     files = Dir.glob(org_files)
      152:     files.each do |file|
      153:       basename = File.basename(file, ".org")
>>>   154:       textile_name = File.join(data_directory, basename + ".html")
      155:       textile_name = File.expand_path(textile_name)
      156: 
      157:       it "should convert #{basename}.org to HTML" do
      158:         expected = IO.read(textile_name)
      159:         expected.should be_kind_of(String)
      160:         parser = Orgmode::Parser.new(IO.read(file), { :allow_include_files => true })
      161:         actual = parser.to_html

FP #3: bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:214

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

      # Do not use syntax coloring for source code blocks
      org_files = File.expand_path(File.join(code_syntax_examples_directory, "*-no-color.org"))
    end
    files = Dir.glob(org_files)

    files.each do |file|
      basename = File.basename(file, ".org")
      org_filename = File.join(code_syntax_examples_directory, basename + ".html")
      org_filename = File.expand_path(org_filename)

      it "should convert #{basename}.org to HTML" do
        expected = IO.read(org_filename)
        expected.should be_kind_of(String)
        parser = Orgmode::Parser.new(IO.read(file), :allow_include_files => true)
        actual = parser.to_html

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      184:     it "should render #+INCLUDE when ORG_RUBY_INCLUDE_ROOT is set" do
      185:       data_directory = File.expand_path(File.join(File.dirname(__FILE__), "html_examples"))
      186:       ENV['ORG_RUBY_INCLUDE_ROOT'] = data_directory
      187:       expected = File.read(File.join(data_directory, "include-file.html"))
      188:       org_file = File.join(data_directory, "include-file.org")
      189:       parser = Orgmode::Parser.new(IO.read(org_file))
      190:       actual = parser.to_html
      191:       actual.should == expected
      192:       ENV['ORG_RUBY_INCLUDE_ROOT'] = nil
      193:     end
      194:   end
      195: 
      196:   describe "Export to HTML test cases with code syntax highlight" do
      197:     code_syntax_examples_directory = File.join(File.dirname(__FILE__), "html_code_syntax_highlight_examples")
      198: 
      199:     # Include the code syntax highlight support tests
      200:     if defined? CodeRay
      201:       # Use CodeRay for syntax highlight (pure Ruby solution)
      202:       org_files = File.expand_path(File.join(code_syntax_examples_directory, "*-coderay.org"))
      203:     elsif defined? Pygments
      204:       # Use pygments (so that it works with Jekyll, Gollum and possibly Github)
      205:       org_files = File.expand_path(File.join(code_syntax_examples_directory, "*-pygments.org"))
      206:     else
      207:       # Do not use syntax coloring for source code blocks
      208:       org_files = File.expand_path(File.join(code_syntax_examples_directory, "*-no-color.org"))
      209:     end
      210:     files = Dir.glob(org_files)
      211: 
      212:     files.each do |file|
      213:       basename = File.basename(file, ".org")
>>>   214:       org_filename = File.join(code_syntax_examples_directory, basename + ".html")
      215:       org_filename = File.expand_path(org_filename)
      216: 
      217:       it "should convert #{basename}.org to HTML" do
      218:         expected = IO.read(org_filename)
      219:         expected.should be_kind_of(String)
      220:         parser = Orgmode::Parser.new(IO.read(file), :allow_include_files => true)
      221:         actual = parser.to_html

FP #4: bdewey__org-ruby__e4a6e7d: spec/parser_spec.rb:242

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

  describe "Export to Markdown test cases" do
    data_directory = File.join(File.dirname(__FILE__), "markdown_examples")
    org_files = File.expand_path(File.join(data_directory, "*.org" ))
    files = Dir.glob(org_files)
    files.each do |file|
      basename = File.basename(file, ".org")
      markdown_name = File.join(data_directory, basename + ".md")
      markdown_name = File.expand_path(markdown_name)

      it "should convert #{basename}.org to Markdown" do
        expected = IO.read(markdown_name)
        expected.should be_kind_of(String)
        parser = Orgmode::Parser.new(IO.read(file), :allow_include_files => false)
        actual = parser.to_markdown

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      212:     files.each do |file|
      213:       basename = File.basename(file, ".org")
      214:       org_filename = File.join(code_syntax_examples_directory, basename + ".html")
      215:       org_filename = File.expand_path(org_filename)
      216: 
      217:       it "should convert #{basename}.org to HTML" do
      218:         expected = IO.read(org_filename)
      219:         expected.should be_kind_of(String)
      220:         parser = Orgmode::Parser.new(IO.read(file), :allow_include_files => true)
      221:         actual = parser.to_html
      222:         actual.should be_kind_of(String)
      223:         actual.should == expected
      224:       end
      225: 
      226:       it "should render #{basename}.org to HTML using Tilt templates" do
      227:         ENV['ORG_RUBY_ENABLE_INCLUDE_FILES'] = 'true'
      228:         expected = IO.read(org_filename)
      229:         template = Tilt.new(file).render
      230:         template.should == expected
      231:         ENV['ORG_RUBY_ENABLE_INCLUDE_FILES'] = ''
      232:       end
      233:     end
      234:   end
      235: 
      236:   describe "Export to Markdown test cases" do
      237:     data_directory = File.join(File.dirname(__FILE__), "markdown_examples")
      238:     org_files = File.expand_path(File.join(data_directory, "*.org" ))
      239:     files = Dir.glob(org_files)
      240:     files.each do |file|
      241:       basename = File.basename(file, ".org")
>>>   242:       markdown_name = File.join(data_directory, basename + ".md")
      243:       markdown_name = File.expand_path(markdown_name)
      244: 
      245:       it "should convert #{basename}.org to Markdown" do
      246:         expected = IO.read(markdown_name)
      247:         expected.should be_kind_of(String)
      248:         parser = Orgmode::Parser.new(IO.read(file), :allow_include_files => false)
      249:         actual = parser.to_markdown

FP #5: department-of-veterans-affairs__vets-api__038d1df: modules/appeals_api/spec/docs/notice_of_disagreements/v0_spec.rb:423

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

      it_behaves_like 'rswag 500 response'
    end
  end

  path '/evidence-submissions/{id}' do
    get 'Returns all of the data associated with a specific Notice of Disagreement Evidence Submission.' do
      scopes = AppealsApi::NoticeOfDisagreements::V0::NoticeOfDisagreementsController::OAUTH_SCOPES[:GET]
      tags 'Notice of Disagreements'
      operationId 'getNoticeOfDisagreementEvidenceSubmission'
      description 'Returns all of the data associated with a specific Notice of Disagreement Evidence Submission.'
      security DocHelpers.oauth_security_config(scopes)
      produces 'application/json'
      parameter name: :id,
                in: :path,

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

      393:                xml: { name: 'Error' },
      394:                properties: {
      395:                  Code: {
      396:                    type: :string, description: 'Error code', example: 'Bad Digest'
      397:                  },
      398:                  Message: {
      399:                    type: :string, description: 'Error detail',
      400:                    example: 'A client error (InvalidDigest) occurred when calling the PutObject operation - The Content-MD5 you specified was invalid.'
      401:                  },
      402:                  Resource: {
      403:                    type: :string, description: 'Resource description', example: '/example_path_here/6d8433c1-cd55-4c24-affd-f592287a7572.upload'
      404:                  },
      405:                  RequestId: {
      406:                    type: :string, description: 'Identifier for debug purposes'
      407:                  }
      408:                }
      409: 
      410:         # rubocop:disable RSpec/NoExpectationExample
      411:         it 'returns a 400 response' do |example|
      412:           # noop
      413:         end
      414:         # rubocop:enable RSpec/NoExpectationExample
      415:       end
      416: 
      417:       it_behaves_like 'rswag 500 response'
      418:     end
      419:   end
      420: 
      421:   path '/evidence-submissions/{id}' do
      422:     get 'Returns all of the data associated with a specific Notice of Disagreement Evidence Submission.' do
>>>   423:       scopes = AppealsApi::NoticeOfDisagreements::V0::NoticeOfDisagreementsController::OAUTH_SCOPES[:GET]
      424:       tags 'Notice of Disagreements'
      425:       operationId 'getNoticeOfDisagreementEvidenceSubmission'
      426:       description 'Returns all of the data associated with a specific Notice of Disagreement Evidence Submission.'
      427:       security DocHelpers.oauth_security_config(scopes)
      428:       produces 'application/json'
      429:       parameter name: :id,
      430:                 in: :path,

FP #6: iftheshoefritz__solargraph-rails__2c284b9: spec/solargraph-rails/rails_spec.rb:4

NOT REPRODUCED — CONFIG/CONTEXT issue
nitrocop does not flag this in isolation or in the full file
(with default config). The FP is caused by the target repo's
config (e.g., different Max value, Include/Exclude patterns).

Source context:

require 'spec_helper'

RSpec.describe 'Rails API completion' do
  filename = nil
  it 'it provides Rails controller api' do
    map =
      rails_workspace do |root|
        filename = root.write_file 'app/controllers/things_controller.rb', <<~EOS
          class ThingsController < ActionController::Base
            res
            def index

Message: Do not use local variables defined outside of examples inside of them.

Full file context (30 lines before offense):

        1: require 'spec_helper'
        2: 
        3: RSpec.describe 'Rails API completion' do
>>>     4:   filename = nil
        5:   it 'it provides Rails controller api' do
        6:     map =
        7:       rails_workspace do |root|
        8:         filename = root.write_file 'app/controllers/things_controller.rb', <<~EOS
        9:           class ThingsController < ActionController::Base
       10:             res
       11:             def index

@6 6 bot force-pushed the fix/rspec-leaky_local_variable-23729874503 branch from bf1a8f7 to 527aeda Compare March 30, 2026 06:04
@6 6 bot marked this pull request as ready for review March 30, 2026 06:04
@6 6 bot enabled auto-merge (squash) March 30, 2026 06:04
@github-actions
Copy link
Copy Markdown
Contributor

Cop-check results (8 shards)

Cop Baseline FP Baseline FN Local FP Local FN FP Δ FN Δ Result
RSpec/LeakyLocalVariable 1 13 1 13 0 0
RSpec/LeakyLocalVariable 0 7 0 7 0 0
RSpec/LeakyLocalVariable 0 11 0 11 0 0
RSpec/LeakyLocalVariable 5 14 5 12 0 -2
RSpec/LeakyLocalVariable 0 3 0 3 0 0
RSpec/LeakyLocalVariable 0 7 0 7 0 0
RSpec/LeakyLocalVariable 2 24 2 24 0 0
RSpec/LeakyLocalVariable 2 19 2 16 0 -3

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