Skip to content

Commit eb0add8

Browse files
authored
Merge pull request #1265 from ydah/add_change_by_zero
Add new `RSpec/ChangeByZero` cop
2 parents 7c25b00 + 7645439 commit eb0add8

File tree

7 files changed

+204
-0
lines changed

7 files changed

+204
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Master (Unreleased)
44

55
* Drop Ruby 2.5 support. ([@ydah][])
6+
* Add new `RSpec/ChangeByZero` cop. ([@ydah][])
67

78
## 2.10.0 (2022-04-19)
89

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ RSpec/BeforeAfterAll:
176176
StyleGuide: https://rspec.rubystyle.guide/#avoid-hooks-with-context-scope
177177
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/BeforeAfterAll
178178

179+
RSpec/ChangeByZero:
180+
Description: Prefer negated matchers over `to change.by(0)`.
181+
Enabled: pending
182+
VersionAdded: 2.11.0
183+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
184+
179185
RSpec/ContextMethod:
180186
Description: "`context` should not be used for specifying methods."
181187
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* xref:cops_rspec.adoc#rspecbeeql[RSpec/BeEql]
1212
* xref:cops_rspec.adoc#rspecbenil[RSpec/BeNil]
1313
* xref:cops_rspec.adoc#rspecbeforeafterall[RSpec/BeforeAfterAll]
14+
* xref:cops_rspec.adoc#rspecchangebyzero[RSpec/ChangeByZero]
1415
* xref:cops_rspec.adoc#rspeccontextmethod[RSpec/ContextMethod]
1516
* xref:cops_rspec.adoc#rspeccontextwording[RSpec/ContextWording]
1617
* xref:cops_rspec.adoc#rspecdescribeclass[RSpec/DescribeClass]

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,49 @@ end
383383
* https://rspec.rubystyle.guide/#avoid-hooks-with-context-scope
384384
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/BeforeAfterAll
385385

386+
== RSpec/ChangeByZero
387+
388+
|===
389+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
390+
391+
| Pending
392+
| Yes
393+
| Yes
394+
| 2.11.0
395+
| -
396+
|===
397+
398+
Prefer negated matchers over `to change.by(0)`.
399+
400+
=== Examples
401+
402+
[source,ruby]
403+
----
404+
# bad
405+
expect { run }.to change(Foo, :bar).by(0)
406+
expect { run }.to change { Foo.bar }.by(0)
407+
expect { run }
408+
.to change(Foo, :bar).by(0)
409+
.and change(Foo, :baz).by(0)
410+
expect { run }
411+
.to change { Foo.bar }.by(0)
412+
.and change { Foo.baz }.by(0)
413+
414+
# good
415+
expect { run }.not_to change(Foo, :bar)
416+
expect { run }.not_to change { Foo.bar }
417+
expect { run }
418+
.to not_change(Foo, :bar)
419+
.and not_change(Foo, :baz)
420+
expect { run }
421+
.to not_change { Foo.bar }
422+
.and not_change { Foo.baz }
423+
----
424+
425+
=== References
426+
427+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
428+
386429
== RSpec/ContextMethod
387430

388431
|===
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
# Prefer negated matchers over `to change.by(0)`.
7+
#
8+
# @example
9+
# # bad
10+
# expect { run }.to change(Foo, :bar).by(0)
11+
# expect { run }.to change { Foo.bar }.by(0)
12+
# expect { run }
13+
# .to change(Foo, :bar).by(0)
14+
# .and change(Foo, :baz).by(0)
15+
# expect { run }
16+
# .to change { Foo.bar }.by(0)
17+
# .and change { Foo.baz }.by(0)
18+
#
19+
# # good
20+
# expect { run }.not_to change(Foo, :bar)
21+
# expect { run }.not_to change { Foo.bar }
22+
# expect { run }
23+
# .to not_change(Foo, :bar)
24+
# .and not_change(Foo, :baz)
25+
# expect { run }
26+
# .to not_change { Foo.bar }
27+
# .and not_change { Foo.baz }
28+
#
29+
class ChangeByZero < Base
30+
extend AutoCorrector
31+
MSG = 'Prefer `not_to change` over `to change.by(0)`.'
32+
MSG_COMPOUND = 'Prefer negated matchers with compound expectations ' \
33+
'over `change.by(0)`.'
34+
RESTRICT_ON_SEND = %i[change].freeze
35+
36+
# @!method expect_change_with_arguments(node)
37+
def_node_matcher :expect_change_with_arguments, <<-PATTERN
38+
(send
39+
(send nil? :change ...) :by
40+
(int 0))
41+
PATTERN
42+
43+
# @!method expect_change_with_block(node)
44+
def_node_matcher :expect_change_with_block, <<-PATTERN
45+
(send
46+
(block
47+
(send nil? :change)
48+
(args)
49+
(send (...) $_)) :by
50+
(int 0))
51+
PATTERN
52+
53+
def on_send(node)
54+
expect_change_with_arguments(node.parent) do
55+
check_offence(node.parent)
56+
end
57+
58+
expect_change_with_block(node.parent.parent) do
59+
check_offence(node.parent.parent)
60+
end
61+
end
62+
63+
private
64+
65+
def check_offence(node)
66+
expression = node.loc.expression
67+
if compound_expectations?(node)
68+
add_offense(expression, message: MSG_COMPOUND)
69+
else
70+
add_offense(expression) do |corrector|
71+
autocorrect(corrector, node)
72+
end
73+
end
74+
end
75+
76+
def compound_expectations?(node)
77+
%i[and or].include?(node.parent.method_name)
78+
end
79+
80+
def autocorrect(corrector, node)
81+
corrector.replace(node.parent.loc.selector, 'not_to')
82+
range = node.loc.dot.with(end_pos: node.loc.expression.end_pos)
83+
corrector.remove(range)
84+
end
85+
end
86+
end
87+
end
88+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require_relative 'rspec/be_eql'
2626
require_relative 'rspec/be_nil'
2727
require_relative 'rspec/before_after_all'
28+
require_relative 'rspec/change_by_zero'
2829
require_relative 'rspec/context_method'
2930
require_relative 'rspec/context_wording'
3031
require_relative 'rspec/describe_class'
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpec::ChangeByZero do
4+
it 'registers an offense when the argument to `by` is zero' do
5+
expect_offense(<<-RUBY)
6+
it do
7+
expect { foo }.to change(Foo, :bar).by(0)
8+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_to change` over `to change.by(0)`.
9+
expect { foo }.to change(::Foo, :bar).by(0)
10+
^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_to change` over `to change.by(0)`.
11+
expect { foo }.to change { Foo.bar }.by(0)
12+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_to change` over `to change.by(0)`.
13+
expect { foo }.to change(Foo, :bar).by 0
14+
^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_to change` over `to change.by(0)`.
15+
end
16+
RUBY
17+
18+
expect_correction(<<-RUBY)
19+
it do
20+
expect { foo }.not_to change(Foo, :bar)
21+
expect { foo }.not_to change(::Foo, :bar)
22+
expect { foo }.not_to change { Foo.bar }
23+
expect { foo }.not_to change(Foo, :bar)
24+
end
25+
RUBY
26+
end
27+
28+
it 'registers an offense when the argument to `by` is zero ' \
29+
'with compound expectations' do
30+
expect_offense(<<-RUBY)
31+
it do
32+
expect { foo }
33+
.to change(Foo, :bar).by(0)
34+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
35+
.and change(Foo, :baz).by(0)
36+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
37+
expect { foo }
38+
.to change { Foo.bar }.by(0)
39+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
40+
.and change { Foo.baz }.by(0)
41+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
42+
expect { foo }
43+
.to change(Foo, :bar).by(0)
44+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
45+
.or change(Foo, :baz).by(0)
46+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
47+
expect { foo }
48+
.to change { Foo.bar }.by(0)
49+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
50+
.or change { Foo.baz }.by(0)
51+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer negated matchers with compound expectations over `change.by(0)`.
52+
end
53+
RUBY
54+
end
55+
56+
it 'does not register an offense when the argument to `by` is not zero' do
57+
expect_no_offenses(<<-RUBY)
58+
it do
59+
expect { foo }.to change(Foo, :bar).by(1)
60+
expect { foo }.to change { Foo.bar }.by(1)
61+
end
62+
RUBY
63+
end
64+
end

0 commit comments

Comments
 (0)