Skip to content

Commit 984b9a2

Browse files
committed
Add NegatedMatcher support to RSpec/ExpectChange
1 parent fd1bef6 commit 984b9a2

File tree

5 files changed

+195
-17
lines changed

5 files changed

+195
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Master (Unreleased)
44

5+
- Add `NegatedMatcher` configuration option `RSpec/ExpectChange`. ([@Darhazer])
56
- `RSpec/ScatteredLet` now preserves the order of `let`s during auto-correction. ([@Darhazer])
67
- Fix a false negative for `RSpec/EmptyLineAfterFinalLet` inside `shared_examples` / `include_examples` / `it_behaves_like` blocks. ([@Darhazer])
78

config/default.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ RSpec/ChangeByZero:
202202
Description: Prefer negated matchers over `to change.by(0)`.
203203
Enabled: true
204204
VersionAdded: '2.11'
205-
VersionChanged: '2.14'
205+
VersionChanged: "<<next>>"
206206
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ChangeByZero
207207
NegatedMatcher: ~
208208

@@ -454,6 +454,7 @@ RSpec/ExpectChange:
454454
SafeAutoCorrect: false
455455
VersionAdded: '1.22'
456456
VersionChanged: '2.5'
457+
NegatedMatcher: ~
457458
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectChange
458459

459460
RSpec/ExpectInHook:

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ end
467467
| Yes
468468
| Always
469469
| 2.11
470-
| 2.14
470+
| <<next>>
471471
|===
472472
473473
Prefer negated matchers over `to change.by(0)`.
@@ -2151,6 +2151,10 @@ or a block.
21512151
21522152
This cop can be configured using the `EnforcedStyle` option.
21532153
2154+
When using compound expectations with `change` and a negated matcher
2155+
(e.g., `not_change`), you can configure the `NegatedMatcher` option
2156+
to ensure consistent style enforcement across both matchers.
2157+
21542158
[#safety-rspecexpectchange]
21552159
=== Safety
21562160
@@ -2204,6 +2208,18 @@ expect { run }.to change(Foo, :bar)
22042208
expect { run }.to change { Foo.bar }
22052209
----
22062210
2211+
[#_negatedmatcher_-not_change_-_with-compound-expectations_-rspecexpectchange]
2212+
==== `NegatedMatcher: not_change` (with compound expectations)
2213+
2214+
[source,ruby]
2215+
----
2216+
# bad
2217+
expect { run }.to change(Foo, :bar).and not_change { Foo.baz }
2218+
2219+
# good
2220+
expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
2221+
----
2222+
22072223
[#configurable-attributes-rspecexpectchange]
22082224
=== Configurable attributes
22092225
@@ -2213,6 +2229,10 @@ expect { run }.to change { Foo.bar }
22132229
| EnforcedStyle
22142230
| `method_call`
22152231
| `method_call`, `block`
2232+
2233+
| NegatedMatcher
2234+
| `<none>`
2235+
|
22162236
|===
22172237
22182238
[#references-rspecexpectchange]

lib/rubocop/cop/rspec/expect_change.rb

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ module RSpec
1010
#
1111
# This cop can be configured using the `EnforcedStyle` option.
1212
#
13+
# When using compound expectations with `change` and a negated matcher
14+
# (e.g., `not_change`), you can configure the `NegatedMatcher` option
15+
# to ensure consistent style enforcement across both matchers.
16+
#
1317
# @safety
1418
# Autocorrection is unsafe because `method_call` style calls the
1519
# receiver *once* and sends the message to it before and after
@@ -48,23 +52,29 @@ module RSpec
4852
# # good
4953
# expect { run }.to change { Foo.bar }
5054
#
55+
# @example `NegatedMatcher: not_change` (with compound expectations)
56+
# # bad
57+
# expect { run }.to change(Foo, :bar).and not_change { Foo.baz }
58+
#
59+
# # good
60+
# expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
61+
#
5162
class ExpectChange < Base
5263
extend AutoCorrector
5364
include ConfigurableEnforcedStyle
5465

55-
MSG_BLOCK = 'Prefer `change(%<obj>s, :%<attr>s)`.'
56-
MSG_CALL = 'Prefer `change { %<obj>s.%<attr>s }`.'
57-
RESTRICT_ON_SEND = %i[change].freeze
66+
MSG_BLOCK = 'Prefer `%<matcher>s(%<obj>s, :%<attr>s)`.'
67+
MSG_CALL = 'Prefer `%<matcher>s { %<obj>s.%<attr>s }`.'
5868

59-
# @!method expect_change_with_arguments(node)
60-
def_node_matcher :expect_change_with_arguments, <<~PATTERN
61-
(send nil? :change $_ ({sym str} $_))
69+
# @!method expect_matcher_with_arguments(node)
70+
def_node_matcher :expect_matcher_with_arguments, <<~PATTERN
71+
(send nil? _ $_ ({sym str} $_))
6272
PATTERN
6373

64-
# @!method expect_change_with_block(node)
65-
def_node_matcher :expect_change_with_block, <<~PATTERN
74+
# @!method expect_matcher_with_block(node)
75+
def_node_matcher :expect_matcher_with_block, <<~PATTERN
6676
(block
67-
(send nil? :change)
77+
(send nil? _)
6878
(args)
6979
(send
7080
${
@@ -78,27 +88,51 @@ class ExpectChange < Base
7888

7989
def on_send(node)
8090
return unless style == :block
91+
return unless matcher_method?(node.method_name)
8192

82-
expect_change_with_arguments(node) do |receiver, message|
83-
msg = format(MSG_CALL, obj: receiver.source, attr: message)
93+
expect_matcher_with_arguments(node) do |receiver, message|
94+
matcher_name = node.method_name.to_s
95+
msg = format(MSG_CALL, matcher: matcher_name,
96+
obj: receiver.source, attr: message)
8497
add_offense(node, message: msg) do |corrector|
85-
replacement = "change { #{receiver.source}.#{message} }"
98+
replacement = "#{matcher_name} { #{receiver.source}.#{message} }"
8699
corrector.replace(node, replacement)
87100
end
88101
end
89102
end
90103

91104
def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
92105
return unless style == :method_call
106+
return unless matcher_method?(node.method_name)
93107

94-
expect_change_with_block(node) do |receiver, message|
95-
msg = format(MSG_BLOCK, obj: receiver.source, attr: message)
108+
expect_matcher_with_block(node) do |receiver, message|
109+
matcher_name = node.method_name.to_s
110+
msg = format(MSG_BLOCK, matcher: matcher_name,
111+
obj: receiver.source, attr: message)
96112
add_offense(node, message: msg) do |corrector|
97-
replacement = "change(#{receiver.source}, :#{message})"
113+
replacement = "#{matcher_name}(#{receiver.source}, :#{message})"
98114
corrector.replace(node, replacement)
99115
end
100116
end
101117
end
118+
119+
private
120+
121+
def matcher_method_names
122+
@matcher_method_names ||= begin
123+
names = [:change]
124+
names << negated_matcher.to_sym if negated_matcher
125+
names
126+
end
127+
end
128+
129+
def matcher_method?(method_name)
130+
matcher_method_names.include?(method_name)
131+
end
132+
133+
def negated_matcher
134+
cop_config['NegatedMatcher']
135+
end
102136
end
103137
end
104138
end

spec/rubocop/cop/rspec/expect_change_spec.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,126 @@
270270
RUBY
271271
end
272272
end
273+
274+
context "with `NegatedMatcher: 'not_change'`" do
275+
let(:cop_config) do
276+
{ 'EnforcedStyle' => enforced_style, 'NegatedMatcher' => 'not_change' }
277+
end
278+
279+
context 'with EnforcedStyle `method_call`' do
280+
let(:enforced_style) { 'method_call' }
281+
282+
it 'flags negated matcher with block style' do
283+
expect_offense(<<~RUBY)
284+
it do
285+
expect { run }.to not_change { User.count }
286+
^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change(User, :count)`.
287+
end
288+
RUBY
289+
290+
expect_correction(<<~RUBY)
291+
it do
292+
expect { run }.to not_change(User, :count)
293+
end
294+
RUBY
295+
end
296+
297+
it 'flags negated matcher in compound expectations' do
298+
expect_offense(<<~RUBY)
299+
it do
300+
expect { run }.to change(Foo, :bar).and not_change { Foo.baz }
301+
^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change(Foo, :baz)`.
302+
end
303+
RUBY
304+
305+
expect_correction(<<~RUBY)
306+
it do
307+
expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
308+
end
309+
RUBY
310+
end
311+
312+
it 'flags both change and negated matcher in compound expectations' do
313+
expect_offense(<<~RUBY)
314+
it do
315+
expect { run }.to change { Foo.bar }.and not_change { Foo.baz }
316+
^^^^^^^^^^^^^^^^^^ Prefer `change(Foo, :bar)`.
317+
^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change(Foo, :baz)`.
318+
end
319+
RUBY
320+
321+
expect_correction(<<~RUBY)
322+
it do
323+
expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
324+
end
325+
RUBY
326+
end
327+
328+
it 'ignores when both use method_call style' do
329+
expect_no_offenses(<<~RUBY)
330+
it do
331+
expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
332+
end
333+
RUBY
334+
end
335+
end
336+
337+
context 'with EnforcedStyle `block`' do
338+
let(:enforced_style) { 'block' }
339+
340+
it 'flags negated matcher with method call style' do
341+
expect_offense(<<~RUBY)
342+
it do
343+
expect { run }.to not_change(User, :count)
344+
^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change { User.count }`.
345+
end
346+
RUBY
347+
348+
expect_correction(<<~RUBY)
349+
it do
350+
expect { run }.to not_change { User.count }
351+
end
352+
RUBY
353+
end
354+
355+
it 'flags negated matcher in compound expectations' do
356+
expect_offense(<<~RUBY)
357+
it do
358+
expect { run }.to change { Foo.bar }.and not_change(Foo, :baz)
359+
^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change { Foo.baz }`.
360+
end
361+
RUBY
362+
363+
expect_correction(<<~RUBY)
364+
it do
365+
expect { run }.to change { Foo.bar }.and not_change { Foo.baz }
366+
end
367+
RUBY
368+
end
369+
370+
it 'flags both change and negated matcher in compound expectations' do
371+
expect_offense(<<~RUBY)
372+
it do
373+
expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
374+
^^^^^^^^^^^^^^^^^ Prefer `change { Foo.bar }`.
375+
^^^^^^^^^^^^^^^^^^^^^ Prefer `not_change { Foo.baz }`.
376+
end
377+
RUBY
378+
379+
expect_correction(<<~RUBY)
380+
it do
381+
expect { run }.to change { Foo.bar }.and not_change { Foo.baz }
382+
end
383+
RUBY
384+
end
385+
386+
it 'ignores when both use block style' do
387+
expect_no_offenses(<<~RUBY)
388+
it do
389+
expect { run }.to change { Foo.bar }.and not_change { Foo.baz }
390+
end
391+
RUBY
392+
end
393+
end
394+
end
273395
end

0 commit comments

Comments
 (0)