Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Master (Unreleased)

- Fix a false positive for `RSpec/LeakyLocalVariable` when variables are used only in example metadata (e.g., skip messages). ([@ydah])
- Add autocorrect support for `RSpec/MultipleExpectations`. ([@gildesmarais])

## 3.8.0 (2025-11-12)

Expand Down Expand Up @@ -1006,6 +1007,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@franzliedke]: https://github.com/franzliedke
[@g-rath]: https://github.com/G-Rath
[@geniou]: https://github.com/geniou
[@gildesmarais]: https://github.com/gildesmarais
[@gsamokovarov]: https://github.com/gsamokovarov
[@harry-graham]: https://github.com/harry-graham
[@harrylewis]: https://github.com/harrylewis
Expand Down
63 changes: 58 additions & 5 deletions lib/rubocop/cop/rspec/multiple_expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ module RSpec
# end
# end
#
class MultipleExpectations < Base
class MultipleExpectations < Base # rubocop:disable Metrics/ClassLength
extend AutoCorrector

MSG = 'Example has too many expectations [%<total>d/%<max>d].'

ANYTHING = ->(_node) { true }
Expand All @@ -76,6 +78,13 @@ class MultipleExpectations < Base

# @!method aggregate_failures?(node)
def_node_matcher :aggregate_failures?, <<~PATTERN
(send _ _ <{ (sym :aggregate_failures) (hash <(pair (sym :aggregate_failures) _) ...>) } ...>)
PATTERN
# @!method metadata_present?(node)
def_node_matcher :metadata_present?, '(send _ _ <{sym hash} ...>)'

# @!method aggregate_failures_definition?(node)
def_node_matcher :aggregate_failures_definition?, <<~PATTERN
(block {
(send _ _ <(sym :aggregate_failures) ...>)
(send _ _ ... (hash <(pair (sym :aggregate_failures) %1) ...>))
Expand Down Expand Up @@ -110,12 +119,14 @@ def example_with_aggregate_failures?(example_node)
node_with_aggregate_failures = find_aggregate_failures(example_node)
return false unless node_with_aggregate_failures

aggregate_failures?(node_with_aggregate_failures, TRUE_NODE)
aggregate_failures_definition?(node_with_aggregate_failures,
TRUE_NODE)
end

def find_aggregate_failures(example_node)
example_node.send_node.each_ancestor(:block)
.find { |block_node| aggregate_failures?(block_node, ANYTHING) }
example_node.send_node.each_ancestor(:block).find do |block_node|
aggregate_failures_definition?(block_node, ANYTHING)
end
end

def find_expectation(node, &block)
Expand All @@ -137,12 +148,54 @@ def flag_example(node, expectation_count:)
total: expectation_count,
max: max_expectations
)
)
) do |corrector|
autocorrect_metadata(corrector, node.send_node)
end
end

def max_expectations
Integer(cop_config.fetch('Max', 1))
end

def autocorrect_metadata(corrector, node)
return if aggregate_failures?(node)

if metadata_present?(node)
add_hash_metadata(corrector, node)
else
add_symbol_metadata(corrector, node)
end
end

def add_symbol_metadata(corrector, node)
if node.arguments.empty?
# Handle cases like `it { ... }` vs `it(...) { ... }`
loc, str = if node.loc.begin
[node.loc.begin, ':aggregate_failures']
else
[node.loc.selector, '(:aggregate_failures)']
end
corrector.insert_after(loc, str)
else
corrector.insert_after(node.last_argument,
', :aggregate_failures')
end
end

def add_hash_metadata(corrector, node)
if (hash_node = node.arguments.reverse.find(&:hash_type?))
if hash_node.pairs.empty?
corrector.insert_before(hash_node.loc.end,
' aggregate_failures: true ')
else
corrector.insert_after(hash_node.pairs.last,
', aggregate_failures: true')
end
else
corrector.insert_after(node.last_argument,
', aggregate_failures: true')
end
end
end
end
end
Expand Down
100 changes: 100 additions & 0 deletions spec/rubocop/cop/rspec/multiple_expectations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,106 @@
end
RUBY
end

describe 'autocorrect' do
it 'adds :aggregate_failures when no metadata is present' do
expect_offense(<<~RUBY)
describe Foo do
it 'uses expect twice' do
^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1].
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY

expect_correction(<<~RUBY)
describe Foo do
it 'uses expect twice', :aggregate_failures do
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY
end

it 'adds parentheses when the example has no arguments' do
expect_offense(<<~RUBY)
describe Foo do
it do
^^ Example has too many expectations [2/1].
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY

expect_correction(<<~RUBY)
describe Foo do
it(:aggregate_failures) do
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY
end

it 'appends aggregate_failures to existing metadata hash arguments' do
expect_offense(<<~RUBY)
describe Foo do
it 'uses expect twice', foo: :bar do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1].
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY

expect_correction(<<~RUBY)
describe Foo do
it 'uses expect twice', foo: :bar, aggregate_failures: true do
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY
end

it 'appends aggregate_failures keyword arguments when metadata exists' do
expect_offense(<<~RUBY)
describe Foo do
it 'uses expect twice', :foo do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1].
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY

expect_correction(<<~RUBY)
describe Foo do
it 'uses expect twice', :foo, aggregate_failures: true do
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY
end

it 'does not autocorrect when aggregate_failures metadata exists' do
expect_offense(<<~RUBY)
describe Foo do
it 'uses expect twice', aggregate_failures: false do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1].
expect(foo).to eq(bar)
expect(baz).to eq(bar)
end
end
RUBY

expect_no_corrections
end
end
end

context 'with metadata' do
Expand Down