-
-
Notifications
You must be signed in to change notification settings - Fork 277
Add new RSpec/ChangeWithoutExpect
cop
#2074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module RSpec | ||
# Checks for `change` matcher usage without an `expect` block. | ||
# | ||
# This cop only considered a matcher if it is chained with | ||
# `from`, `by`, `by_at_least`, or `by_at_most`. | ||
# | ||
# @example | ||
# # bad | ||
# it 'changes the count' do | ||
# change(Counter, :count).by(1) | ||
# end | ||
# | ||
# # bad | ||
# it 'changes the count' do | ||
# change(Counter, :count).by_at_least(1) | ||
# end | ||
# | ||
# # bad | ||
# it 'changes the count' do | ||
# change(Counter, :count).by_at_most(1) | ||
# end | ||
# | ||
# # bad | ||
# it 'changes the count' do | ||
# change(Counter, :count).from(0) | ||
# end | ||
# | ||
# # bad | ||
# it 'changes the count' do | ||
# change(Counter, :count).from(0).to(1) | ||
# end | ||
# | ||
# # good - no chained matchers | ||
# it 'changes the count' do | ||
# change(Counter, :count) | ||
# end | ||
# | ||
# # good - ignore not change matcher | ||
# it 'changes the count' do | ||
# change(Counter, :count).from(0).by(1) | ||
# end | ||
# | ||
# # good - within expect block | ||
# it 'changes the count' do | ||
# expect { subject }.to change(Counter, :count).by(1) | ||
# end | ||
# | ||
class ChangeWithoutExpect < RuboCop::Cop::Base | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, why only block matchers? |
||
include RangeHelp | ||
|
||
MSG = 'Use `change` matcher within an `expect` block.' | ||
RESTRICT_ON_SEND = [:change].freeze | ||
SINGLE_RESTRICTED_METHODS = %i[by by_at_least by_at_most from].freeze | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
# @!method expectation?(node) | ||
def_node_search :expectation?, <<~PATTERN | ||
(send | ||
{ | ||
(block (send nil? :expect ...) ...) | ||
(send nil? :expect ...) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we match the non-block form here? |
||
} | ||
{:to :not_to} | ||
...) | ||
PATTERN | ||
|
||
def on_send(node) | ||
return if within_expectation?(node) | ||
return unless offensive_chain?(node) | ||
|
||
add_offense(node) | ||
end | ||
|
||
private | ||
|
||
def offensive_chain?(node) | ||
chain_methods = chain_methods(node) | ||
return false if chain_methods.empty? | ||
|
||
# Case 1: single restricted method (by, by_at_least, by_at_most, from) | ||
if chain_methods.size == 1 && | ||
SINGLE_RESTRICTED_METHODS.include?(chain_methods.first) | ||
return true | ||
end | ||
|
||
# Case 2: exactly from().to() pattern | ||
if chain_methods.size == 2 && | ||
chain_methods.first == :from && | ||
chain_methods.last == :to | ||
return true | ||
end | ||
|
||
false | ||
end | ||
|
||
def chain_methods(node) | ||
methods = [] | ||
chain = node | ||
|
||
while chain.parent.send_type? | ||
chain = chain.parent | ||
methods << chain.method_name | ||
end | ||
|
||
methods | ||
end | ||
|
||
def within_expectation?(node) | ||
node.each_ancestor(:send).any? do |ancestor| | ||
expectation?(ancestor) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::RSpec::ChangeWithoutExpect, :config do | ||
it 'registers an offense for a `change` call with only `by` ' \ | ||
'without an `expect` block' do | ||
expect_offense(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).by(1) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `change` matcher within an `expect` block. | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers an offense for a `change` call with only `by_at_least` ' \ | ||
'without an `expect` block' do | ||
expect_offense(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).by_at_least(1) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `change` matcher within an `expect` block. | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers an offense for a `change` call with only `by_at_most` ' \ | ||
'without an `expect` block' do | ||
expect_offense(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).by_at_most(1) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `change` matcher within an `expect` block. | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers an offense for a `change` call with only `from` ' \ | ||
'without an `expect` block' do | ||
expect_offense(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).from(0) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `change` matcher within an `expect` block. | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers an offense for a `change` call with exactly `from().to()` ' \ | ||
'without an `expect` block' do | ||
expect_offense(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).from(0).to(1) | ||
^^^^^^^^^^^^^^^^^^^^^^^ Use `change` matcher within an `expect` block. | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for a `change` call with `from().by()` ' \ | ||
'without an `expect` block' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).from(0).by(1) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for a simple `change` call ' \ | ||
'without chains' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why doesn’t it register an offence? |
||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for a `change` call with unsupported ' \ | ||
'chain method' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'changes the count' do | ||
change(Counter, :count).some_other_method(123) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for `change` with chains within an ' \ | ||
'`expect` block' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'changes the count' do | ||
expect { subject }.to change(Counter, :count).by(1) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for `change` with from-to chain ' \ | ||
'within an `expect` block' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'changes the count' do | ||
expect { subject }.to change(Counter, :count).from(0).to(1) | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense for `change` with chains ' \ | ||
'within an `expect` with not_to' do | ||
expect_no_offenses(<<~RUBY) | ||
it 'does not change the count' do | ||
expect { subject }.not_to change(Counter, :count).by(1) | ||
end | ||
RUBY | ||
end | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does it behave with a case with two statements? Like “subject; change(:foo, :bar)”? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What it the matcher is assigned to a variable, and then used multiple times? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though this would detect a number of offences, wouldn’t it make more sense to make a more broad cop that would detect all built-in block matchers? “output”, “raise_error”, …