Skip to content

Commit 9c86416

Browse files
committed
Fix false negatives/positives
1 parent 3dd584c commit 9c86416

File tree

3 files changed

+57
-33
lines changed

3 files changed

+57
-33
lines changed

config/default.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,7 @@ RSpec/DiscardedMatcher:
315315
Description: Checks for matchers that are used in void context.
316316
Enabled: pending
317317
VersionAdded: "<<next>>"
318-
MatcherMethods:
319-
- change
320-
- have_received
321-
- output
322-
- receive
323-
- receive_messages
324-
- receive_message_chain
318+
CustomMatcherMethods: []
325319
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DiscardedMatcher
326320

327321
RSpec/DuplicatedMetadata:

lib/rubocop/cop/rspec/discarded_matcher.rb

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ module RSpec
99
# standalone expressions have their result silently discarded.
1010
# This usually means a missing `.and` to chain compound matchers.
1111
#
12-
# The list of matcher methods can be configured with `MatcherMethods`.
12+
# The list of matcher methods can be configured
13+
# with `CustomMatcherMethods`.
1314
#
1415
# @example
1516
# # bad
@@ -37,10 +38,10 @@ class DiscardedMatcher < Base
3738
MSG = 'The result of `%<method>s` is not used. ' \
3839
'Did you mean to chain it with `.and`?'
3940

40-
# @!method includes_expectation?(node)
41-
def_node_search :includes_expectation?, <<~PATTERN
42-
(send nil? #Expectations.all ...)
43-
PATTERN
41+
MATCHER_METHODS = %i[
42+
change have_received output
43+
receive receive_messages receive_message_chain
44+
].to_set.freeze
4445

4546
def on_send(node)
4647
check_discarded_matcher(node, node)
@@ -55,31 +56,50 @@ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
5556
def check_discarded_matcher(send_node, node)
5657
return unless matcher_call?(send_node)
5758
return unless inside_example?(node)
59+
return unless example_with_matcher_expectation?(node)
5860

5961
target = find_outermost_chain(node)
6062
return unless void_value?(target)
6163

6264
add_offense(target, message: format(MSG, method: node.method_name))
6365
end
6466

67+
def example_with_matcher_expectation?(node)
68+
example_node =
69+
node.each_ancestor(:block).find { |ancestor| example?(ancestor) }
70+
71+
example_node.each_descendant(:send).any? do |send_node|
72+
expectation_with_matcher?(send_node)
73+
end
74+
end
75+
76+
def expectation_with_matcher?(node)
77+
%i[to to_not not_to].include?(node.method_name) &&
78+
node.arguments.any? do |arg|
79+
arg.each_node(:send).any? { |s| matcher_call?(s) }
80+
end
81+
end
82+
6583
def void_value?(node)
6684
case node.parent.type
67-
when :begin
68-
true
6985
when :block
7086
example?(node.parent)
71-
when :when, :case
87+
when :begin, :case, :when
7288
void_value?(node.parent)
7389
end
7490
end
7591

7692
def matcher_call?(node)
77-
node.receiver.nil? && matcher_methods.include?(node.method_name)
93+
node.receiver.nil? && all_matcher_methods.include?(node.method_name)
94+
end
95+
96+
def all_matcher_methods
97+
@all_matcher_methods ||=
98+
(MATCHER_METHODS + custom_matcher_methods).freeze
7899
end
79100

80-
def matcher_methods
81-
@matcher_methods ||=
82-
cop_config.fetch('MatcherMethods', []).to_set(&:to_sym).freeze
101+
def custom_matcher_methods
102+
cop_config.fetch('CustomMatcherMethods', []).map(&:to_sym)
83103
end
84104

85105
def find_outermost_chain(node)

spec/rubocop/cop/rspec/discarded_matcher_spec.rb

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@
6060
RUBY
6161
end
6262

63+
it 'does not register for `output` unrelated to matcher' do
64+
expect_no_offenses(<<~RUBY)
65+
specify do
66+
output.rewind
67+
parsed = JSON.parse(output.read.chomp)
68+
expect(parsed["message"]).to eq("error message")
69+
end
70+
RUBY
71+
end
72+
6373
it 'registers an offense for standalone `have_received`' do
6474
expect_offense(<<~RUBY)
6575
specify do
@@ -104,6 +114,12 @@
104114
RUBY
105115
end
106116

117+
it 'does not register an offense for `change` in parenthesized arg' do
118+
expect_no_offenses(<<~RUBY)
119+
expect { result }.to (change { obj.bar })
120+
RUBY
121+
end
122+
107123
it 'does not register an offense when `receive` is used with `allow`' do
108124
expect_no_offenses(<<~RUBY)
109125
specify do
@@ -183,7 +199,8 @@
183199
RUBY
184200
end
185201

186-
it 'does not register an offense for `change` in non-void `if` and `else` branches' do
202+
it 'does not register an offense ' \
203+
'for `change` in non-void `if` and `else` branches' do
187204
expect_no_offenses(<<~RUBY)
188205
specify do
189206
result = if condition
@@ -285,25 +302,18 @@ def expect_action
285302
RUBY
286303
end
287304

288-
context 'with custom MatcherMethods' do
305+
context 'with custom CustomMatcherMethods' do
289306
let(:cop_config) do
290-
{ 'MatcherMethods' => %w[custom_matcher] }
307+
{ 'CustomMatcherMethods' => %w[custom_matcher] }
291308
end
292309

293310
it 'registers an offense for configured custom matcher' do
294311
expect_offense(<<~RUBY)
295312
specify do
296-
expect(foo).to receive(:bar)
297-
custom_matcher(:foo)
298-
^^^^^^^^^^^^^^^^^^^^ The result of `custom_matcher` is not used. Did you mean to chain it with `.and`?
299-
end
300-
RUBY
301-
end
302-
303-
it 'does not register an offense for default matchers not in config' do
304-
expect_no_offenses(<<~RUBY)
305-
specify do
306-
change { obj.bar }
313+
expect(foo).to \\
314+
receive(:bar)
315+
custom_matcher(:foo)
316+
^^^^^^^^^^^^^^^^^^^^ The result of `custom_matcher` is not used. Did you mean to chain it with `.and`?
307317
end
308318
RUBY
309319
end

0 commit comments

Comments
 (0)