Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Fix a false positive for `RSpec/LeakyConstantDeclaration` when defining constants in explicit namespaces. ([@naveg])
- Add support for error matchers (`raise_exception` and `raise_error`) to `RSpec/Dialect`. ([@lovro-bikic])
- Don't register offenses for `RSpec/DescribedClass` within `Data.define` blocks. ([@lovro-bikic])
- Add autocorrection support for `RSpec/IteratedExpectation` for single expectations. ([@lovro-bikic])

## 3.6.0 (2025-04-18)

Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3191,7 +3191,7 @@ it_should_behave_like 'a foo'

| Enabled
| Yes
| No
| Always
| 1.14
| -
|===
Expand Down
42 changes: 32 additions & 10 deletions lib/rubocop/cop/rspec/iterated_expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module RSpec
# end
#
class IteratedExpectation < Base
extend AutoCorrector

MSG = 'Prefer using the `all` matcher instead ' \
'of iterating over an array.'

Expand All @@ -25,14 +27,14 @@ class IteratedExpectation < Base
(block
(send ... :each)
(args (arg $_))
$(...)
(...)
)
PATTERN

# @!method each_numblock?(node)
def_node_matcher :each_numblock?, <<~PATTERN
(numblock
(send ... :each) _ $(...)
(send ... :each) _ (...)
)
PATTERN

Expand All @@ -42,23 +44,43 @@ class IteratedExpectation < Base
PATTERN

def on_block(node)
each?(node) do |arg, body|
if single_expectation?(body, arg) || only_expectations?(body, arg)
add_offense(node.send_node)
end
each?(node) do |arg|
check_offense(node, arg)
end
end

def on_numblock(node)
each_numblock?(node) do |body|
if single_expectation?(body, :_1) || only_expectations?(body, :_1)
add_offense(node.send_node)
end
each_numblock?(node) do
check_offense(node, :_1)
end
end

private

def check_offense(node, argument)
if single_expectation?(node.body, argument)
add_offense(node.send_node) do |corrector|
next unless node.body.arguments.one?
next if uses_argument_in_matcher?(node, argument)

corrector.replace(node, single_expectation_replacement(node))
end
elsif only_expectations?(node.body, argument)
add_offense(node.send_node)
end
end

def single_expectation_replacement(node)
collection = node.receiver.source
matcher = node.body.first_argument.source

"expect(#{collection}).to all(#{matcher})"
end

def uses_argument_in_matcher?(node, argument)
node.body.first_argument.each_descendant.any?(s(:lvar, argument))
end

def single_expectation?(body, arg)
expectation?(body, arg)
end
Expand Down
51 changes: 51 additions & 0 deletions spec/rubocop/cop/rspec/iterated_expectation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_correction(<<~RUBY)
it 'validates users' do
expect([user1, user2, user3]).to all(be_valid)
end
RUBY
end

it 'flags `each` when expectation calls method with arguments' do
Expand All @@ -17,6 +23,35 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_correction(<<~RUBY)
it 'validates users' do
expect([user1, user2, user3]).to all(be_a(User))
end
RUBY
end

it 'flags `each` when the expectation specifies an error message, but ' \
'does not correct' do
expect_offense(<<~RUBY)
it 'validates users' do
[user1, user2, user3].each { |user| expect(user).to be_a(User), "user is not a User" }
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_no_corrections
end

it 'flags `each` when matcher uses block argument, but does not correct' do
expect_offense(<<~RUBY)
it 'validates users' do
[user1, user2, user3].each { |user| expect(user).to receive(:flag).and_return(user.flag) }
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_no_corrections
end

it 'ignores `each` without expectation' do
Expand Down Expand Up @@ -45,6 +80,8 @@
end
end
RUBY

expect_no_corrections
end

it 'ignore `each` when the body does not contain only expectations' do
Expand Down Expand Up @@ -94,6 +131,12 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_correction(<<~RUBY)
it 'validates users' do
expect([user1, user2, user3]).to all(be_valid)
end
RUBY
end

it 'flags `each` when expectation calls method with arguments' do
Expand All @@ -103,6 +146,12 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using the `all` matcher instead of iterating over an array.
end
RUBY

expect_correction(<<~RUBY)
it 'validates users' do
expect([user1, user2, user3]).to all(be_a(User))
end
RUBY
end

it 'ignores `each` without expectation' do
Expand All @@ -123,6 +172,8 @@
end
end
RUBY

expect_no_corrections
end

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