Skip to content

Commit

Permalink
Add NegatedMatcher configuration option to RSpec/ChangeByZero
Browse files Browse the repository at this point in the history
This PR is add `NegatedMatcher` option to `RSpec/ChangeByZero`.

In the case of composite expectations, cop suggest
using the negation matchers of `RSpec::Matchers#change`.

By default doe's not support autocorrect,
but if you set the negation matcher of `RSpec::Matchers#change`
defined in `NegatedMatcher` option, will be autocorrect.
  • Loading branch information
ydah committed Aug 7, 2022
1 parent 1a97bcd commit 2d32d0d
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Fix a false positive for `RSpec/Capybara/SpecificMatcher`. ([@ydah][])
* Fix a false negative for `RSpec/Capybara/SpecificMatcher` for `have_field`. ([@ydah][])
* Fix a false positive for `RSpec/Capybara/SpecificMatcher` when may not have a `href` by `have_link`. ([@ydah][])
* Add `NegatedMatcher` configuration option to `RSpec/ChangeByZero`. ([@ydah][])

## 2.12.1 (2022-07-03)

Expand Down
4 changes: 3 additions & 1 deletion config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,10 @@ RSpec/BeforeAfterAll:
RSpec/ChangeByZero:
Description: Prefer negated matchers over `to change.by(0)`.
Enabled: pending
VersionAdded: 2.11.0
VersionAdded: '2.11'
VersionChanged: '2.13'
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
NegatedMatcher: ~

RSpec/ContextMethod:
Description: "`context` should not be used for specifying methods."
Expand Down
46 changes: 43 additions & 3 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -391,16 +391,24 @@ end
| Pending
| Yes
| Yes
| 2.11.0
| -
| 2.11
| 2.13
|===

Prefer negated matchers over `to change.by(0)`.

This cop does not support autocorrection in some cases.
In the case of composite expectations, cop suggest using the
negation matchers of `RSpec::Matchers#change`.

By default the cop does not support autocorrect of
compound expectations, but if you set the
negated matcher for `change`, e.g. `not_change` with
the `NegatedMatcher` option, the cop will perform the autocorrection.

=== Examples

==== NegatedMatcher: ~ (default)

[source,ruby]
----
# bad
Expand Down Expand Up @@ -429,6 +437,38 @@ expect { run }
.and not_change { Foo.baz }
----

==== NegatedMatcher: not_change

[source,ruby]
----
# bad (support autocorrection to good case)
expect { run }
.to change(Foo, :bar).by(0)
.and change(Foo, :baz).by(0)
expect { run }
.to change { Foo.bar }.by(0)
.and change { Foo.baz }.by(0)
# good
define_negated_matcher :not_change, :change
expect { run }
.to not_change(Foo, :bar)
.and not_change(Foo, :baz)
expect { run }
.to not_change { Foo.bar }
.and not_change { Foo.baz }
----

=== Configurable attributes

|===
| Name | Default value | Configurable values

| NegatedMatcher
| `<none>`
|
|===

=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
Expand Down
61 changes: 57 additions & 4 deletions lib/rubocop/cop/rspec/change_by_zero.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ module Cop
module RSpec
# Prefer negated matchers over `to change.by(0)`.
#
# This cop does not support autocorrection in some cases.
# In the case of composite expectations, cop suggest using the
# negation matchers of `RSpec::Matchers#change`.
#
# @example
# By default the cop does not support autocorrect of
# compound expectations, but if you set the
# negated matcher for `change`, e.g. `not_change` with
# the `NegatedMatcher` option, the cop will perform the autocorrection.
#
# @example NegatedMatcher: ~ (default)
# # bad
# expect { run }.to change(Foo, :bar).by(0)
# expect { run }.to change { Foo.bar }.by(0)
Expand All @@ -33,10 +39,28 @@ module RSpec
# .to not_change { Foo.bar }
# .and not_change { Foo.baz }
#
# @example NegatedMatcher: not_change
# # bad (support autocorrection to good case)
# expect { run }
# .to change(Foo, :bar).by(0)
# .and change(Foo, :baz).by(0)
# expect { run }
# .to change { Foo.bar }.by(0)
# .and change { Foo.baz }.by(0)
#
# # good
# define_negated_matcher :not_change, :change
# expect { run }
# .to not_change(Foo, :bar)
# .and not_change(Foo, :baz)
# expect { run }
# .to not_change { Foo.bar }
# .and not_change { Foo.baz }
#
class ChangeByZero < Base
extend AutoCorrector
MSG = 'Prefer `not_to change` over `to change.by(0)`.'
MSG_COMPOUND = 'Prefer negated matchers with compound expectations ' \
MSG_COMPOUND = 'Prefer %<preferred>s with compound expectations ' \
'over `change.by(0)`.'
RESTRICT_ON_SEND = %i[change].freeze

Expand All @@ -57,6 +81,11 @@ class ChangeByZero < Base
(int 0))
PATTERN

# @!method change_nodes(node)
def_node_search :change_nodes, <<-PATTERN
$(send nil? :change ...)
PATTERN

def on_send(node)
expect_change_with_arguments(node.parent) do
check_offense(node.parent)
Expand All @@ -72,7 +101,9 @@ def on_send(node)
def check_offense(node)
expression = node.loc.expression
if compound_expectations?(node)
add_offense(expression, message: MSG_COMPOUND)
add_offense(expression, message: message_compound) do |corrector|
autocorrect_compound(corrector, node)
end
else
add_offense(expression) do |corrector|
autocorrect(corrector, node)
Expand All @@ -89,6 +120,28 @@ def autocorrect(corrector, node)
range = node.loc.dot.with(end_pos: node.loc.expression.end_pos)
corrector.remove(range)
end

def autocorrect_compound(corrector, node)
return unless negated_matcher

change_nodes(node) do |change_node|
corrector.replace(change_node.loc.selector, negated_matcher)
range = node.loc.dot.with(end_pos: node.loc.expression.end_pos)
corrector.remove(range)
end
end

def negated_matcher
cop_config['NegatedMatcher']
end

def message_compound
format(MSG_COMPOUND, preferred: preferred_method)
end

def preferred_method
negated_matcher ? "`#{negated_matcher}`" : 'negated matchers'
end
end
end
end
Expand Down
Loading

0 comments on commit 2d32d0d

Please sign in to comment.