From e73d74d9fcc2d5efdcd777a06a09c4576309cb6f Mon Sep 17 00:00:00 2001 From: ydah Date: Mon, 24 Nov 2025 14:48:32 +0900 Subject: [PATCH] Fix false positive in `RSpec/LetSetup` cop when `let!` variables are used in blocks declared in an outer context --- CHANGELOG.md | 2 + lib/rubocop/cop/rspec/let_setup.rb | 21 +++++- spec/rubocop/cop/rspec/let_setup_spec.rb | 95 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bdc941f..49312437d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Master (Unreleased) +- Fix false positive in `RSpec/LetSetup` cop when `let!` variables are used in blocks declared in an outer context. ([@ydah]) + ## 3.8.0 (2025-11-12) - Add new cop `RSpec/LeakyLocalVariable`. ([@lovro-bikic]) diff --git a/lib/rubocop/cop/rspec/let_setup.rb b/lib/rubocop/cop/rspec/let_setup.rb index 4c1f4a8dd..47575b44f 100644 --- a/lib/rubocop/cop/rspec/let_setup.rb +++ b/lib/rubocop/cop/rspec/let_setup.rb @@ -72,8 +72,9 @@ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler def unused_let_bang(node) child_let_bang(node) do |method_send, method_name| next if overrides_outer_let_bang?(node, method_name) + next if method_called_in_scope?(node, method_name.to_sym) - yield(method_send) unless method_called?(node, method_name.to_sym) + yield(method_send) end end @@ -98,6 +99,24 @@ def outer_let_bang?(ancestor_node, method_name) end end end + + def method_called_in_scope?(node, method_name) + method_called?(node, method_name) || + method_called_in_parent_hooks?(node, method_name) + end + + def method_called_in_parent_hooks?(node, method_name) + node.each_ancestor(:block).any? do |ancestor| + example_or_shared_group_or_including?(ancestor) && + method_called_in_hooks?(ancestor, method_name) + end + end + + def method_called_in_hooks?(node, method_name) + RuboCop::RSpec::ExampleGroup.new(node).hooks.any? do |hook| + method_called?(hook.to_node, method_name) + end + end end end end diff --git a/spec/rubocop/cop/rspec/let_setup_spec.rb b/spec/rubocop/cop/rspec/let_setup_spec.rb index 33aa63843..6e1038769 100644 --- a/spec/rubocop/cop/rspec/let_setup_spec.rb +++ b/spec/rubocop/cop/rspec/let_setup_spec.rb @@ -257,4 +257,99 @@ end RUBY end + + it 'ignores let! used in parent after hook' do + expect_no_offenses(<<~RUBY) + describe 'Parent' do + after { variable.cleanup } + + context 'Child' do + let!(:variable) { setup } + it { expect(true).to be true } + end + end + RUBY + end + + it 'ignores let! used in parent before hook' do + expect_no_offenses(<<~RUBY) + describe 'Parent' do + before { variable.setup } + + context 'Child' do + let!(:variable) { create } + it { expect(true).to be true } + end + end + RUBY + end + + it 'ignores let! used in parent around hook' do + expect_no_offenses(<<~RUBY) + describe 'Parent' do + around { |example| variable.wrap { example.run } } + + context 'Child' do + let!(:variable) { create } + it { expect(true).to be true } + end + end + RUBY + end + + it 'complains when let! is not used anywhere' do + expect_offense(<<~RUBY) + describe 'Parent' do + context 'Child' do + let!(:variable) { setup } + ^^^^^^^^^^^^^^^ Do not use `let!` to setup objects not referenced in tests. + it { expect(true).to be true } + end + end + RUBY + end + + it 'ignores let! used in nested example' do + expect_no_offenses(<<~RUBY) + describe 'Parent' do + context 'Child' do + let!(:variable) { setup } + it { expect(variable).to be_present } + end + end + RUBY + end + + it 'ignores let! used in parent after hook with method call' do + expect_no_offenses(<<~RUBY) + describe 'Example' do + after do + authenticator.remove! + end + + context 'nested' do + let!(:authenticator) { create_authenticator } + + it 'does something' do + expect(true).to be true + end + end + end + RUBY + end + + it 'ignores let! used in multiple parent levels' do + expect_no_offenses(<<~RUBY) + describe 'Level 0' do + after { variable.cleanup } + + context 'Level 1' do + context 'Level 2' do + let!(:variable) { setup } + it { expect(true).to be true } + end + end + end + RUBY + end end