Skip to content

Commit 2db2779

Browse files
committed
Add new cop RSpec/LeakyLocalVariable
1 parent 027d385 commit 2db2779

File tree

8 files changed

+486
-0
lines changed

8 files changed

+486
-0
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,4 @@ Performance/ZipWithoutBlock: {Enabled: true}
293293
# Enable our own pending cops.
294294

295295
RSpec/IncludeExamples: {Enabled: true}
296+
RSpec/LeakyLocalVariable: {Enabled: true}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Fix a false positive for `RSpec/LeakyConstantDeclaration` when defining constants in explicit namespaces. ([@naveg])
77
- Add support for error matchers (`raise_exception` and `raise_error`) to `RSpec/Dialect`. ([@lovro-bikic])
88
- Don't register offenses for `RSpec/DescribedClass` within `Data.define` blocks. ([@lovro-bikic])
9+
- Add new cop `RSpec/LeakyLocalVariable`. ([@lovro-bikic])
910

1011
## 3.6.0 (2025-04-18)
1112

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,12 @@ RSpec/LeakyConstantDeclaration:
606606
StyleGuide: https://rspec.rubystyle.guide/#declare-constants
607607
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyConstantDeclaration
608608

609+
RSpec/LeakyLocalVariable:
610+
Description: Checks for outside local variables used in examples.
611+
Enabled: pending
612+
VersionAdded: "<<next>>"
613+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyLocalVariable
614+
609615
RSpec/LetBeforeExamples:
610616
Description: Checks for `let` definitions that come after an example.
611617
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
* xref:cops_rspec.adoc#rspeciteratedexpectation[RSpec/IteratedExpectation]
6060
* xref:cops_rspec.adoc#rspecleadingsubject[RSpec/LeadingSubject]
6161
* xref:cops_rspec.adoc#rspecleakyconstantdeclaration[RSpec/LeakyConstantDeclaration]
62+
* xref:cops_rspec.adoc#rspecleakylocalvariable[RSpec/LeakyLocalVariable]
6263
* xref:cops_rspec.adoc#rspecletbeforeexamples[RSpec/LetBeforeExamples]
6364
* xref:cops_rspec.adoc#rspecletsetup[RSpec/LetSetup]
6465
* xref:cops_rspec.adoc#rspecmatcharray[RSpec/MatchArray]

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,88 @@ end
33913391
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyConstantDeclaration
33923392
* https://rspec.info/features/3-12/rspec-mocks/mutating-constants
33933393
3394+
[#rspecleakylocalvariable]
3395+
== RSpec/LeakyLocalVariable
3396+
3397+
|===
3398+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
3399+
3400+
| Pending
3401+
| Yes
3402+
| No
3403+
| <<next>>
3404+
| -
3405+
|===
3406+
3407+
Checks for outside local variables used in examples.
3408+
3409+
Local variables assigned outside an example but used within it act
3410+
as shared state, which can make tests non-deterministic.
3411+
3412+
[#examples-rspecleakylocalvariable]
3413+
=== Examples
3414+
3415+
[source,ruby]
3416+
----
3417+
# bad - outside variable used in a hook
3418+
user = create(:user)
3419+
3420+
before { user.update(admin: true) }
3421+
3422+
# good
3423+
let(:user) { create(:user) }
3424+
3425+
before { user.update(admin: true) }
3426+
3427+
# bad - outside variable used in an example
3428+
user = create(:user)
3429+
3430+
it 'is persisted' do
3431+
expect(user).to be_persisted
3432+
end
3433+
3434+
# good
3435+
let(:user) { create(:user) }
3436+
3437+
it 'is persisted' do
3438+
expect(user).to be_persisted
3439+
end
3440+
3441+
# also good - assigning the variable within the example
3442+
it 'is persisted' do
3443+
user = create(:user)
3444+
3445+
expect(user).to be_persisted
3446+
end
3447+
3448+
# bad - outside variable passed to included examples
3449+
attrs = ['foo', 'bar']
3450+
3451+
it_behaves_like 'some examples', attrs
3452+
3453+
# good
3454+
it_behaves_like 'some examples' do
3455+
let(:attrs) { ['foo', 'bar'] }
3456+
end
3457+
3458+
# good - when variable is used only as example description
3459+
attribute = 'foo'
3460+
3461+
it "#{attribute} is persisted" do
3462+
expectations
3463+
end
3464+
3465+
# good - when variable is used only to include other examples
3466+
examples = foo ? 'some examples' : 'other examples'
3467+
3468+
it_behaves_like examples, another_argument
3469+
----
3470+
3471+
[#references-rspecleakylocalvariable]
3472+
=== References
3473+
3474+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyLocalVariable
3475+
33943476
[#rspecletbeforeexamples]
33953477
== RSpec/LetBeforeExamples
33963478
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
# Checks for outside local variables used in examples.
7+
#
8+
# Local variables assigned outside an example but used within it act
9+
# as shared state, which can make tests non-deterministic.
10+
#
11+
# @example
12+
# # bad - outside variable used in a hook
13+
# user = create(:user)
14+
#
15+
# before { user.update(admin: true) }
16+
#
17+
# # good
18+
# let(:user) { create(:user) }
19+
#
20+
# before { user.update(admin: true) }
21+
#
22+
# # bad - outside variable used in an example
23+
# user = create(:user)
24+
#
25+
# it 'is persisted' do
26+
# expect(user).to be_persisted
27+
# end
28+
#
29+
# # good
30+
# let(:user) { create(:user) }
31+
#
32+
# it 'is persisted' do
33+
# expect(user).to be_persisted
34+
# end
35+
#
36+
# # also good - assigning the variable within the example
37+
# it 'is persisted' do
38+
# user = create(:user)
39+
#
40+
# expect(user).to be_persisted
41+
# end
42+
#
43+
# # bad - outside variable passed to included examples
44+
# attrs = ['foo', 'bar']
45+
#
46+
# it_behaves_like 'some examples', attrs
47+
#
48+
# # good
49+
# it_behaves_like 'some examples' do
50+
# let(:attrs) { ['foo', 'bar'] }
51+
# end
52+
#
53+
# # good - when variable is used only as example description
54+
# attribute = 'foo'
55+
#
56+
# it "#{attribute} is persisted" do
57+
# expectations
58+
# end
59+
#
60+
# # good - when variable is used only to include other examples
61+
# examples = foo ? 'some examples' : 'other examples'
62+
#
63+
# it_behaves_like examples, another_argument
64+
#
65+
class LeakyLocalVariable < Base
66+
MSG = 'Use `let` instead of a local variable which can leak ' \
67+
'between examples.'
68+
69+
# @!method example_method?(node)
70+
def_node_matcher :example_method?, <<~PATTERN
71+
(send nil? #Examples.all _)
72+
PATTERN
73+
74+
# @!method includes_method?(node)
75+
def_node_matcher :includes_method?, <<~PATTERN
76+
(send nil? #Includes.all ...)
77+
PATTERN
78+
79+
def self.joining_forces
80+
VariableForce
81+
end
82+
83+
def after_leaving_scope(scope, _variable_table)
84+
scope.variables.each_value { |variable| check_references(variable) }
85+
end
86+
87+
private
88+
89+
def check_references(variable)
90+
variable.assignments.each do |assignment|
91+
next if part_of_example_scope?(assignment.node)
92+
93+
assignment.references.each do |reference|
94+
next unless part_of_example_scope?(reference)
95+
next if permitted_argument?(reference)
96+
97+
add_offense(assignment.node)
98+
end
99+
end
100+
end
101+
102+
def permitted_argument?(node)
103+
node.each_ancestor.any? do |ancestor|
104+
next true if example_method?(ancestor)
105+
if includes_method?(ancestor)
106+
next includes_arguments_permitted?(ancestor, node)
107+
end
108+
109+
false
110+
end
111+
end
112+
113+
def includes_arguments_permitted?(node, argument)
114+
node.arguments[1..].none? do |argument_node|
115+
unpermitted_include_argument?(argument_node, argument)
116+
end
117+
end
118+
119+
def unpermitted_include_argument?(argument_node, argument)
120+
return false if argument_node.type?(:dstr, :dsym)
121+
122+
argument_node == argument ||
123+
argument_node.each_descendant.any?(argument)
124+
end
125+
126+
def part_of_example_scope?(node)
127+
node.each_ancestor.any? { |ancestor| example_scope?(ancestor) }
128+
end
129+
130+
def example_scope?(node)
131+
subject?(node) || let?(node) || hook?(node) || example?(node) ||
132+
include?(node)
133+
end
134+
end
135+
end
136+
end
137+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
require_relative 'rspec/iterated_expectation'
5858
require_relative 'rspec/leading_subject'
5959
require_relative 'rspec/leaky_constant_declaration'
60+
require_relative 'rspec/leaky_local_variable'
6061
require_relative 'rspec/let_before_examples'
6162
require_relative 'rspec/let_setup'
6263
require_relative 'rspec/match_array'

0 commit comments

Comments
 (0)