Skip to content

Commit 4af6191

Browse files
committed
Add autocorrection support for RSpec/IteratedExpectation for single expectations
1 parent 027d385 commit 4af6191

File tree

4 files changed

+85
-11
lines changed

4 files changed

+85
-11
lines changed

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 autocorrection support for `RSpec/IteratedExpectation` for single expectations. ([@lovro-bikic])
910

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

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3191,7 +3191,7 @@ it_should_behave_like 'a foo'
31913191
31923192
| Enabled
31933193
| Yes
3194-
| No
3194+
| Always
31953195
| 1.14
31963196
| -
31973197
|===

lib/rubocop/cop/rspec/iterated_expectation.rb

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ module RSpec
1717
# end
1818
#
1919
class IteratedExpectation < Base
20+
extend AutoCorrector
21+
2022
MSG = 'Prefer using the `all` matcher instead ' \
2123
'of iterating over an array.'
2224

@@ -25,14 +27,14 @@ class IteratedExpectation < Base
2527
(block
2628
(send ... :each)
2729
(args (arg $_))
28-
$(...)
30+
(...)
2931
)
3032
PATTERN
3133

3234
# @!method each_numblock?(node)
3335
def_node_matcher :each_numblock?, <<~PATTERN
3436
(numblock
35-
(send ... :each) _ $(...)
37+
(send ... :each) _ (...)
3638
)
3739
PATTERN
3840

@@ -42,23 +44,43 @@ class IteratedExpectation < Base
4244
PATTERN
4345

4446
def on_block(node)
45-
each?(node) do |arg, body|
46-
if single_expectation?(body, arg) || only_expectations?(body, arg)
47-
add_offense(node.send_node)
48-
end
47+
each?(node) do |arg|
48+
check_offense(node, arg)
4949
end
5050
end
5151

5252
def on_numblock(node)
53-
each_numblock?(node) do |body|
54-
if single_expectation?(body, :_1) || only_expectations?(body, :_1)
55-
add_offense(node.send_node)
56-
end
53+
each_numblock?(node) do
54+
check_offense(node, :_1)
5755
end
5856
end
5957

6058
private
6159

60+
def check_offense(node, argument)
61+
if single_expectation?(node.body, argument)
62+
add_offense(node.send_node) do |corrector|
63+
next unless node.body.arguments.one?
64+
next if uses_argument_in_matcher?(node, argument)
65+
66+
corrector.replace(node, single_expectation_replacement(node))
67+
end
68+
elsif only_expectations?(node.body, argument)
69+
add_offense(node.send_node)
70+
end
71+
end
72+
73+
def single_expectation_replacement(node)
74+
collection = node.receiver.source
75+
matcher = node.body.first_argument.source
76+
77+
"expect(#{collection}).to all(#{matcher})"
78+
end
79+
80+
def uses_argument_in_matcher?(node, argument)
81+
node.body.first_argument.each_descendant.any?(s(:lvar, argument))
82+
end
83+
6284
def single_expectation?(body, arg)
6385
expectation?(body, arg)
6486
end

spec/rubocop/cop/rspec/iterated_expectation_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
99
end
1010
RUBY
11+
12+
expect_correction(<<~RUBY)
13+
it 'validates users' do
14+
expect([user1, user2, user3]).to all(be_valid)
15+
end
16+
RUBY
1117
end
1218

1319
it 'flags `each` when expectation calls method with arguments' do
@@ -17,6 +23,35 @@
1723
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
1824
end
1925
RUBY
26+
27+
expect_correction(<<~RUBY)
28+
it 'validates users' do
29+
expect([user1, user2, user3]).to all(be_a(User))
30+
end
31+
RUBY
32+
end
33+
34+
it 'flags `each` when the expectation specifies an error message, but ' \
35+
'does not correct' do
36+
expect_offense(<<~RUBY)
37+
it 'validates users' do
38+
[user1, user2, user3].each { |user| expect(user).to be_a(User), "user is not a User" }
39+
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
40+
end
41+
RUBY
42+
43+
expect_no_corrections
44+
end
45+
46+
it 'flags `each` when matcher uses block argument, but does not correct' do
47+
expect_offense(<<~RUBY)
48+
it 'validates users' do
49+
[user1, user2, user3].each { |user| expect(user).to receive(:flag).and_return(user.flag) }
50+
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
51+
end
52+
RUBY
53+
54+
expect_no_corrections
2055
end
2156

2257
it 'ignores `each` without expectation' do
@@ -45,6 +80,8 @@
4580
end
4681
end
4782
RUBY
83+
84+
expect_no_corrections
4885
end
4986

5087
it 'ignore `each` when the body does not contain only expectations' do
@@ -94,6 +131,12 @@
94131
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
95132
end
96133
RUBY
134+
135+
expect_correction(<<~RUBY)
136+
it 'validates users' do
137+
expect([user1, user2, user3]).to all(be_valid)
138+
end
139+
RUBY
97140
end
98141

99142
it 'flags `each` when expectation calls method with arguments' do
@@ -103,6 +146,12 @@
103146
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
104147
end
105148
RUBY
149+
150+
expect_correction(<<~RUBY)
151+
it 'validates users' do
152+
expect([user1, user2, user3]).to all(be_a(User))
153+
end
154+
RUBY
106155
end
107156

108157
it 'ignores `each` without expectation' do
@@ -123,6 +172,8 @@
123172
end
124173
end
125174
RUBY
175+
176+
expect_no_corrections
126177
end
127178

128179
it 'ignore `each` when the body does not contain only expectations' do

0 commit comments

Comments
 (0)