@@ -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
0 commit comments