rubocop-rspec 2.22.0 → 2.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -9
  3. data/README.md +1 -1
  4. data/config/default.yml +126 -21
  5. data/lib/rubocop/cop/rspec/around_block.rb +3 -3
  6. data/lib/rubocop/cop/rspec/be.rb +1 -1
  7. data/lib/rubocop/cop/rspec/be_empty.rb +1 -0
  8. data/lib/rubocop/cop/rspec/be_eq.rb +1 -1
  9. data/lib/rubocop/cop/rspec/be_eql.rb +1 -1
  10. data/lib/rubocop/cop/rspec/be_nil.rb +2 -2
  11. data/lib/rubocop/cop/rspec/before_after_all.rb +7 -13
  12. data/lib/rubocop/cop/rspec/capybara/feature_methods.rb +2 -2
  13. data/lib/rubocop/cop/rspec/change_by_zero.rb +30 -4
  14. data/lib/rubocop/cop/rspec/context_method.rb +2 -2
  15. data/lib/rubocop/cop/rspec/context_wording.rb +1 -1
  16. data/lib/rubocop/cop/rspec/describe_symbol.rb +1 -1
  17. data/lib/rubocop/cop/rspec/described_class.rb +33 -11
  18. data/lib/rubocop/cop/rspec/duplicated_metadata.rb +1 -1
  19. data/lib/rubocop/cop/rspec/empty_example_group.rb +4 -1
  20. data/lib/rubocop/cop/rspec/empty_line_after_example.rb +2 -2
  21. data/lib/rubocop/cop/rspec/empty_metadata.rb +46 -0
  22. data/lib/rubocop/cop/rspec/eq.rb +47 -0
  23. data/lib/rubocop/cop/rspec/example_length.rb +11 -5
  24. data/lib/rubocop/cop/rspec/example_without_description.rb +11 -2
  25. data/lib/rubocop/cop/rspec/example_wording.rb +11 -2
  26. data/lib/rubocop/cop/rspec/excessive_docstring_spacing.rb +14 -5
  27. data/lib/rubocop/cop/rspec/expect_actual.rb +7 -4
  28. data/lib/rubocop/cop/rspec/expect_change.rb +2 -2
  29. data/lib/rubocop/cop/rspec/expect_output.rb +1 -4
  30. data/lib/rubocop/cop/rspec/file_path.rb +6 -0
  31. data/lib/rubocop/cop/rspec/focus.rb +17 -2
  32. data/lib/rubocop/cop/rspec/hook_argument.rb +2 -2
  33. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +1 -1
  34. data/lib/rubocop/cop/rspec/implicit_block_expectation.rb +2 -2
  35. data/lib/rubocop/cop/rspec/implicit_expect.rb +1 -1
  36. data/lib/rubocop/cop/rspec/implicit_subject.rb +2 -2
  37. data/lib/rubocop/cop/rspec/indexed_let.rb +32 -1
  38. data/lib/rubocop/cop/rspec/instance_spy.rb +2 -2
  39. data/lib/rubocop/cop/rspec/instance_variable.rb +3 -3
  40. data/lib/rubocop/cop/rspec/is_expected_specify.rb +45 -0
  41. data/lib/rubocop/cop/rspec/iterated_expectation.rb +3 -3
  42. data/lib/rubocop/cop/rspec/leaky_constant_declaration.rb +1 -1
  43. data/lib/rubocop/cop/rspec/let_before_examples.rb +5 -1
  44. data/lib/rubocop/cop/rspec/let_setup.rb +1 -1
  45. data/lib/rubocop/cop/rspec/message_expectation.rb +1 -2
  46. data/lib/rubocop/cop/rspec/message_spies.rb +0 -2
  47. data/lib/rubocop/cop/rspec/metadata_style.rb +202 -0
  48. data/lib/rubocop/cop/rspec/mixin/file_help.rb +14 -0
  49. data/lib/rubocop/cop/rspec/mixin/metadata.rb +21 -7
  50. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +2 -2
  51. data/lib/rubocop/cop/rspec/multiple_expectations.rb +12 -7
  52. data/lib/rubocop/cop/rspec/named_subject.rb +1 -1
  53. data/lib/rubocop/cop/rspec/pending.rb +12 -2
  54. data/lib/rubocop/cop/rspec/pending_without_reason.rb +1 -1
  55. data/lib/rubocop/cop/rspec/predicate_matcher.rb +9 -9
  56. data/lib/rubocop/cop/rspec/rails/avoid_setup_hook.rb +1 -1
  57. data/lib/rubocop/cop/rspec/rails/have_http_status.rb +34 -10
  58. data/lib/rubocop/cop/rspec/rails/http_status.rb +29 -18
  59. data/lib/rubocop/cop/rspec/rails/minitest_assertions.rb +314 -22
  60. data/lib/rubocop/cop/rspec/rails/negation_be_valid.rb +102 -0
  61. data/lib/rubocop/cop/rspec/receive_counts.rb +1 -1
  62. data/lib/rubocop/cop/rspec/receive_messages.rb +161 -0
  63. data/lib/rubocop/cop/rspec/redundant_predicate_matcher.rb +67 -0
  64. data/lib/rubocop/cop/rspec/remove_const.rb +40 -0
  65. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +1 -1
  66. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +2 -2
  67. data/lib/rubocop/cop/rspec/repeated_include_example.rb +1 -1
  68. data/lib/rubocop/cop/rspec/repeated_subject_call.rb +124 -0
  69. data/lib/rubocop/cop/rspec/return_from_stub.rb +1 -1
  70. data/lib/rubocop/cop/rspec/shared_context.rb +1 -1
  71. data/lib/rubocop/cop/rspec/shared_examples.rb +66 -20
  72. data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +2 -3
  73. data/lib/rubocop/cop/rspec/sort_metadata.rb +2 -1
  74. data/lib/rubocop/cop/rspec/spec_file_path_format.rb +133 -0
  75. data/lib/rubocop/cop/rspec/spec_file_path_suffix.rb +40 -0
  76. data/lib/rubocop/cop/rspec/stubbed_mock.rb +1 -1
  77. data/lib/rubocop/cop/rspec/subject_stub.rb +4 -4
  78. data/lib/rubocop/cop/rspec/unspecified_exception.rb +2 -2
  79. data/lib/rubocop/cop/rspec/variable_definition.rb +4 -4
  80. data/lib/rubocop/cop/rspec/verified_double_reference.rb +6 -6
  81. data/lib/rubocop/cop/rspec/verified_doubles.rb +2 -2
  82. data/lib/rubocop/cop/rspec/void_expect.rb +4 -3
  83. data/lib/rubocop/cop/rspec_cops.rb +11 -0
  84. data/lib/rubocop/rspec/language.rb +8 -8
  85. data/lib/rubocop/rspec/version.rb +1 -1
  86. data/lib/rubocop/rspec/wording.rb +8 -0
  87. data/lib/rubocop-rspec.rb +1 -0
  88. metadata +20 -8
@@ -4,54 +4,346 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Rails
7
- # Check if using Minitest matchers.
7
+ # Check if using Minitest-like matchers.
8
+ #
9
+ # Check the use of minitest-like matchers
10
+ # starting with `assert_` or `refute_`.
8
11
  #
9
12
  # @example
10
13
  # # bad
11
14
  # assert_equal(a, b)
12
15
  # assert_equal a, b, "must be equal"
16
+ # assert_not_includes a, b
13
17
  # refute_equal(a, b)
18
+ # assert_nil a
19
+ # refute_empty(b)
20
+ # assert_true(a)
21
+ # assert_false(a)
14
22
  #
15
23
  # # good
16
24
  # expect(b).to eq(a)
17
25
  # expect(b).to(eq(a), "must be equal")
26
+ # expect(a).not_to include(b)
18
27
  # expect(b).not_to eq(a)
28
+ # expect(a).to eq(nil)
29
+ # expect(a).not_to be_empty
30
+ # expect(a).to be(true)
31
+ # expect(a).to be(false)
19
32
  #
20
33
  class MinitestAssertions < Base
21
34
  extend AutoCorrector
22
35
 
36
+ # :nodoc:
37
+ class BasicAssertion
38
+ extend NodePattern::Macros
39
+
40
+ attr_reader :expected, :actual, :failure_message
41
+
42
+ def self.minitest_assertion
43
+ raise NotImplementedError
44
+ end
45
+
46
+ def initialize(expected, actual, failure_message)
47
+ @expected = expected&.source
48
+ @actual = actual.source
49
+ @failure_message = failure_message&.source
50
+ end
51
+
52
+ def replaced(node)
53
+ runner = negated?(node) ? 'not_to' : 'to'
54
+ if failure_message.nil?
55
+ "expect(#{actual}).#{runner} #{assertion}"
56
+ else
57
+ "expect(#{actual}).#{runner}(#{assertion}, #{failure_message})"
58
+ end
59
+ end
60
+
61
+ def negated?(node)
62
+ node.method_name.start_with?('assert_not_', 'refute_')
63
+ end
64
+
65
+ def assertion
66
+ raise NotImplementedError
67
+ end
68
+ end
69
+
70
+ # :nodoc:
71
+ class EqualAssertion < BasicAssertion
72
+ MATCHERS = %i[
73
+ assert_equal
74
+ assert_not_equal
75
+ refute_equal
76
+ ].freeze
77
+
78
+ # @!method self.minitest_assertion(node)
79
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
80
+ (send nil? {:assert_equal :assert_not_equal :refute_equal} $_ $_ $_?)
81
+ PATTERN
82
+
83
+ def self.match(expected, actual, failure_message)
84
+ new(expected, actual, failure_message.first)
85
+ end
86
+
87
+ def assertion
88
+ "eq(#{expected})"
89
+ end
90
+ end
91
+
92
+ # :nodoc:
93
+ class KindOfAssertion < BasicAssertion
94
+ MATCHERS = %i[
95
+ assert_kind_of
96
+ assert_not_kind_of
97
+ refute_kind_of
98
+ ].freeze
99
+
100
+ # @!method self.minitest_assertion(node)
101
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
102
+ (send nil? {:assert_kind_of :assert_not_kind_of :refute_kind_of} $_ $_ $_?)
103
+ PATTERN
104
+
105
+ def self.match(expected, actual, failure_message)
106
+ new(expected, actual, failure_message.first)
107
+ end
108
+
109
+ def assertion
110
+ "be_a_kind_of(#{expected})"
111
+ end
112
+ end
113
+
114
+ # :nodoc:
115
+ class InstanceOfAssertion < BasicAssertion
116
+ MATCHERS = %i[
117
+ assert_instance_of
118
+ assert_not_instance_of
119
+ refute_instance_of
120
+ ].freeze
121
+
122
+ # @!method self.minitest_assertion(node)
123
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
124
+ (send nil? {:assert_instance_of :assert_not_instance_of :refute_instance_of} $_ $_ $_?)
125
+ PATTERN
126
+
127
+ def self.match(expected, actual, failure_message)
128
+ new(expected, actual, failure_message.first)
129
+ end
130
+
131
+ def assertion
132
+ "be_an_instance_of(#{expected})"
133
+ end
134
+ end
135
+
136
+ # :nodoc:
137
+ class IncludesAssertion < BasicAssertion
138
+ MATCHERS = %i[
139
+ assert_includes
140
+ assert_not_includes
141
+ refute_includes
142
+ ].freeze
143
+
144
+ # @!method self.minitest_assertion(node)
145
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
146
+ (send nil? {:assert_includes :assert_not_includes :refute_includes} $_ $_ $_?)
147
+ PATTERN
148
+
149
+ def self.match(collection, expected, failure_message)
150
+ new(expected, collection, failure_message.first)
151
+ end
152
+
153
+ def assertion
154
+ "include(#{expected})"
155
+ end
156
+ end
157
+
158
+ # :nodoc:
159
+ class InDeltaAssertion < BasicAssertion
160
+ MATCHERS = %i[
161
+ assert_in_delta
162
+ assert_not_in_delta
163
+ refute_in_delta
164
+ ].freeze
165
+
166
+ # @!method self.minitest_assertion(node)
167
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
168
+ (send nil? {:assert_in_delta :assert_not_in_delta :refute_in_delta} $_ $_ $_? $_?)
169
+ PATTERN
170
+
171
+ def self.match(expected, actual, delta, failure_message)
172
+ new(expected, actual, delta.first, failure_message.first)
173
+ end
174
+
175
+ def initialize(expected, actual, delta, fail_message)
176
+ super(expected, actual, fail_message)
177
+
178
+ @delta = delta&.source || '0.001'
179
+ end
180
+
181
+ def assertion
182
+ "be_within(#{@delta}).of(#{expected})"
183
+ end
184
+ end
185
+
186
+ # :nodoc:
187
+ class PredicateAssertion < BasicAssertion
188
+ MATCHERS = %i[
189
+ assert_predicate
190
+ assert_not_predicate
191
+ refute_predicate
192
+ ].freeze
193
+
194
+ # @!method self.minitest_assertion(node)
195
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
196
+ (send nil? {:assert_predicate :assert_not_predicate :refute_predicate} $_ ${sym} $_?)
197
+ PATTERN
198
+
199
+ def self.match(subject, predicate, failure_message)
200
+ return nil unless predicate.value.end_with?('?')
201
+
202
+ new(predicate, subject, failure_message.first)
203
+ end
204
+
205
+ def assertion
206
+ "be_#{expected.delete_prefix(':').delete_suffix('?')}"
207
+ end
208
+ end
209
+
210
+ # :nodoc:
211
+ class MatchAssertion < BasicAssertion
212
+ MATCHERS = %i[
213
+ assert_match
214
+ refute_match
215
+ ].freeze
216
+
217
+ # @!method self.minitest_assertion(node)
218
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
219
+ (send nil? {:assert_match :refute_match} $_ $_ $_?)
220
+ PATTERN
221
+
222
+ def self.match(matcher, actual, failure_message)
223
+ new(matcher, actual, failure_message.first)
224
+ end
225
+
226
+ def assertion
227
+ "match(#{expected})"
228
+ end
229
+ end
230
+
231
+ # :nodoc:
232
+ class NilAssertion < BasicAssertion
233
+ MATCHERS = %i[
234
+ assert_nil
235
+ assert_not_nil
236
+ refute_nil
237
+ ].freeze
238
+
239
+ # @!method self.minitest_assertion(node)
240
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
241
+ (send nil? {:assert_nil :assert_not_nil :refute_nil} $_ $_?)
242
+ PATTERN
243
+
244
+ def self.match(actual, failure_message)
245
+ new(nil, actual, failure_message.first)
246
+ end
247
+
248
+ def assertion
249
+ 'eq(nil)'
250
+ end
251
+ end
252
+
253
+ # :nodoc:
254
+ class EmptyAssertion < BasicAssertion
255
+ MATCHERS = %i[
256
+ assert_empty
257
+ assert_not_empty
258
+ refute_empty
259
+ ].freeze
260
+
261
+ # @!method self.minitest_assertion(node)
262
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
263
+ (send nil? {:assert_empty :assert_not_empty :refute_empty} $_ $_?)
264
+ PATTERN
265
+
266
+ def self.match(actual, failure_message)
267
+ new(nil, actual, failure_message.first)
268
+ end
269
+
270
+ def assertion
271
+ 'be_empty'
272
+ end
273
+ end
274
+
275
+ # :nodoc:
276
+ class TrueAssertion < BasicAssertion
277
+ MATCHERS = %i[
278
+ assert_true
279
+ ].freeze
280
+
281
+ # @!method self.minitest_assertion(node)
282
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
283
+ (send nil? {:assert_true} $_ $_?)
284
+ PATTERN
285
+
286
+ def self.match(actual, failure_message)
287
+ new(nil, actual, failure_message.first)
288
+ end
289
+
290
+ def assertion
291
+ 'be(true)'
292
+ end
293
+ end
294
+
295
+ # :nodoc:
296
+ class FalseAssertion < BasicAssertion
297
+ MATCHERS = %i[
298
+ assert_false
299
+ ].freeze
300
+
301
+ # @!method self.minitest_assertion(node)
302
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
303
+ (send nil? {:assert_false} $_ $_?)
304
+ PATTERN
305
+
306
+ def self.match(actual, failure_message)
307
+ new(nil, actual, failure_message.first)
308
+ end
309
+
310
+ def assertion
311
+ 'be(false)'
312
+ end
313
+ end
314
+
23
315
  MSG = 'Use `%<prefer>s`.'
24
- RESTRICT_ON_SEND = %i[assert_equal refute_equal].freeze
25
316
 
26
- # @!method minitest_assertion(node)
27
- def_node_matcher :minitest_assertion, <<-PATTERN
28
- (send nil? {:assert_equal :refute_equal} $_ $_ $_?)
29
- PATTERN
317
+ # TODO: replace with `BasicAssertion.subclasses` in Ruby 3.1+
318
+ ASSERTION_MATCHERS = constants(false).filter_map do |c|
319
+ const = const_get(c)
320
+
321
+ const if const.is_a?(Class) && const.superclass == BasicAssertion
322
+ end
323
+
324
+ RESTRICT_ON_SEND = ASSERTION_MATCHERS.flat_map { |m| m::MATCHERS }
30
325
 
31
326
  def on_send(node)
32
- minitest_assertion(node) do |expected, actual, failure_message|
33
- prefer = replacement(node, expected, actual,
34
- failure_message.first)
35
- add_offense(node, message: message(prefer)) do |corrector|
36
- corrector.replace(node, prefer)
327
+ ASSERTION_MATCHERS.each do |m|
328
+ m.minitest_assertion(node) do |*args|
329
+ assertion = m.match(*args)
330
+
331
+ next if assertion.nil?
332
+
333
+ on_assertion(node, assertion)
37
334
  end
38
335
  end
39
336
  end
40
337
 
41
- private
42
-
43
- def replacement(node, expected, actual, failure_message)
44
- runner = node.method?(:assert_equal) ? 'to' : 'not_to'
45
- if failure_message.nil?
46
- "expect(#{actual.source}).#{runner} eq(#{expected.source})"
47
- else
48
- "expect(#{actual.source}).#{runner}(eq(#{expected.source}), " \
49
- "#{failure_message.source})"
338
+ def on_assertion(node, assertion)
339
+ preferred = assertion.replaced(node)
340
+ add_offense(node, message: message(preferred)) do |corrector|
341
+ corrector.replace(node, preferred)
50
342
  end
51
343
  end
52
344
 
53
- def message(prefer)
54
- format(MSG, prefer: prefer)
345
+ def message(preferred)
346
+ format(MSG, prefer: preferred)
55
347
  end
56
348
  end
57
349
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Rails
7
+ # Enforces use of `be_invalid` or `not_to` for negated be_valid.
8
+ #
9
+ # @safety
10
+ # This cop is unsafe because it cannot guarantee that
11
+ # the test target is an instance of `ActiveModel::Validations``.
12
+ #
13
+ # @example EnforcedStyle: not_to (default)
14
+ # # bad
15
+ # expect(foo).to be_invalid
16
+ #
17
+ # # good
18
+ # expect(foo).not_to be_valid
19
+ #
20
+ # # good (with method chain)
21
+ # expect(foo).to be_invalid.and be_odd
22
+ #
23
+ # @example EnforcedStyle: be_invalid
24
+ # # bad
25
+ # expect(foo).not_to be_valid
26
+ #
27
+ # # good
28
+ # expect(foo).to be_invalid
29
+ #
30
+ # # good (with method chain)
31
+ # expect(foo).to be_invalid.or be_even
32
+ #
33
+ class NegationBeValid < Base
34
+ extend AutoCorrector
35
+ include ConfigurableEnforcedStyle
36
+
37
+ MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
38
+ RESTRICT_ON_SEND = %i[be_valid be_invalid].freeze
39
+
40
+ # @!method not_to?(node)
41
+ def_node_matcher :not_to?, <<~PATTERN
42
+ (send ... :not_to (send nil? :be_valid ...))
43
+ PATTERN
44
+
45
+ # @!method be_invalid?(node)
46
+ def_node_matcher :be_invalid?, <<~PATTERN
47
+ (send ... :to (send nil? :be_invalid ...))
48
+ PATTERN
49
+
50
+ def on_send(node)
51
+ return unless offense?(node.parent)
52
+
53
+ add_offense(offense_range(node),
54
+ message: message(node.method_name)) do |corrector|
55
+ corrector.replace(node.parent.loc.selector, replaced_runner)
56
+ corrector.replace(node.loc.selector, replaced_matcher)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def offense?(node)
63
+ case style
64
+ when :not_to
65
+ be_invalid?(node)
66
+ when :be_invalid
67
+ not_to?(node)
68
+ end
69
+ end
70
+
71
+ def offense_range(node)
72
+ node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
73
+ end
74
+
75
+ def message(_matcher)
76
+ format(MSG,
77
+ runner: replaced_runner,
78
+ matcher: replaced_matcher)
79
+ end
80
+
81
+ def replaced_runner
82
+ case style
83
+ when :not_to
84
+ 'not_to'
85
+ when :be_invalid
86
+ 'to'
87
+ end
88
+ end
89
+
90
+ def replaced_matcher
91
+ case style
92
+ when :not_to
93
+ 'be_valid'
94
+ when :be_invalid
95
+ 'be_invalid'
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -30,7 +30,7 @@ module RuboCop
30
30
  RESTRICT_ON_SEND = %i[times].freeze
31
31
 
32
32
  # @!method receive_counts(node)
33
- def_node_matcher :receive_counts, <<-PATTERN
33
+ def_node_matcher :receive_counts, <<~PATTERN
34
34
  (send $(send _ {:exactly :at_least :at_most} (int {1 2})) :times)
35
35
  PATTERN
36
36
 
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for multiple messages stubbed on the same object.
7
+ #
8
+ # @safety
9
+ # The autocorrection is marked as unsafe, because it may change the
10
+ # order of stubs. This in turn may cause e.g. variables to be called
11
+ # before they are defined.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # before do
16
+ # allow(Service).to receive(:foo).and_return(bar)
17
+ # allow(Service).to receive(:baz).and_return(qux)
18
+ # end
19
+ #
20
+ # # good
21
+ # before do
22
+ # allow(Service).to receive_messages(foo: bar, baz: qux)
23
+ # end
24
+ #
25
+ # # good - ignore same message
26
+ # before do
27
+ # allow(Service).to receive(:foo).and_return(bar)
28
+ # allow(Service).to receive(:foo).and_return(qux)
29
+ # end
30
+ #
31
+ class ReceiveMessages < Base
32
+ extend AutoCorrector
33
+ include RangeHelp
34
+
35
+ MSG = 'Use `receive_messages` instead of multiple stubs on lines ' \
36
+ '%<loc>s.'
37
+
38
+ # @!method allow_receive_message?(node)
39
+ def_node_matcher :allow_receive_message?, <<~PATTERN
40
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym _)) :and_return !#heredoc_or_splat?))
41
+ PATTERN
42
+
43
+ # @!method allow_argument(node)
44
+ def_node_matcher :allow_argument, <<~PATTERN
45
+ (send (send nil? :allow $_ ...) ...)
46
+ PATTERN
47
+
48
+ # @!method receive_node(node)
49
+ def_node_search :receive_node, <<~PATTERN
50
+ $(send (send nil? :receive ...) ...)
51
+ PATTERN
52
+
53
+ # @!method receive_arg(node)
54
+ def_node_search :receive_arg, <<~PATTERN
55
+ (send (send nil? :receive $_) ...)
56
+ PATTERN
57
+
58
+ # @!method receive_and_return_argument(node)
59
+ def_node_matcher :receive_and_return_argument, <<~PATTERN
60
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym $_)) :and_return $_))
61
+ PATTERN
62
+
63
+ def on_begin(node)
64
+ repeated_receive_message(node).each do |item, repeated_lines, args|
65
+ next if repeated_lines.empty?
66
+
67
+ register_offense(item, repeated_lines, args)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def repeated_receive_message(node)
74
+ node
75
+ .children
76
+ .select { |child| allow_receive_message?(child) }
77
+ .group_by { |child| allow_argument(child) }
78
+ .values
79
+ .reject(&:one?)
80
+ .flat_map { |items| add_repeated_lines_and_arguments(items) }
81
+ end
82
+
83
+ def add_repeated_lines_and_arguments(items)
84
+ uniq_items = uniq_items(items)
85
+ repeated_lines = uniq_items.map(&:first_line)
86
+ uniq_items.map do |item|
87
+ [item, repeated_lines - [item.first_line], arguments(uniq_items)]
88
+ end
89
+ end
90
+
91
+ def uniq_items(items)
92
+ items.select do |item|
93
+ items.none? do |i|
94
+ receive_arg(item).first == receive_arg(i).first &&
95
+ !same_line?(item, i)
96
+ end
97
+ end
98
+ end
99
+
100
+ def arguments(items)
101
+ items.map do |item|
102
+ receive_and_return_argument(item) do |receive_arg, return_arg|
103
+ "#{normalize_receive_arg(receive_arg)}: " \
104
+ "#{normalize_return_arg(return_arg)}"
105
+ end
106
+ end
107
+ end
108
+
109
+ def normalize_receive_arg(receive_arg)
110
+ if requires_quotes?(receive_arg)
111
+ "'#{receive_arg}'"
112
+ else
113
+ receive_arg
114
+ end
115
+ end
116
+
117
+ def normalize_return_arg(return_arg)
118
+ if return_arg.hash_type? && !return_arg.braces?
119
+ "{ #{return_arg.source} }"
120
+ else
121
+ return_arg.source
122
+ end
123
+ end
124
+
125
+ def register_offense(item, repeated_lines, args)
126
+ add_offense(item, message: message(repeated_lines)) do |corrector|
127
+ if item.loc.line > repeated_lines.max
128
+ replace_to_receive_messages(corrector, item, args)
129
+ else
130
+ corrector.remove(item_range_by_whole_lines(item))
131
+ end
132
+ end
133
+ end
134
+
135
+ def message(repeated_lines)
136
+ format(MSG, loc: repeated_lines)
137
+ end
138
+
139
+ def replace_to_receive_messages(corrector, item, args)
140
+ receive_node(item) do |node|
141
+ corrector.replace(node,
142
+ "receive_messages(#{args.join(', ')})")
143
+ end
144
+ end
145
+
146
+ def item_range_by_whole_lines(item)
147
+ range_by_whole_lines(item.source_range, include_final_newline: true)
148
+ end
149
+
150
+ def heredoc_or_splat?(node)
151
+ ((node.str_type? || node.dstr_type?) && node.heredoc?) ||
152
+ node.splat_type?
153
+ end
154
+
155
+ def requires_quotes?(value)
156
+ value.match?(/^:".*?"|=$|^\W+$/)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for redundant predicate matcher.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # expect(foo).to be_exist(bar)
11
+ # expect(foo).not_to be_include(bar)
12
+ # expect(foo).to be_all(bar)
13
+ #
14
+ # # good
15
+ # expect(foo).to exist(bar)
16
+ # expect(foo).not_to include(bar)
17
+ # expect(foo).to all be(bar)
18
+ #
19
+ class RedundantPredicateMatcher < Base
20
+ extend AutoCorrector
21
+
22
+ MSG = 'Use `%<good>s` instead of `%<bad>s`.'
23
+ RESTRICT_ON_SEND =
24
+ %i[be_all be_cover be_end_with be_eql be_equal
25
+ be_exist be_exists be_include be_match
26
+ be_respond_to be_start_with].freeze
27
+
28
+ def on_send(node)
29
+ return if node.parent.block_type? || node.arguments.empty?
30
+ return unless replaceable_arguments?(node)
31
+
32
+ method_name = node.method_name.to_s
33
+ replaced = replaced_method_name(method_name)
34
+ add_offense(node, message: message(method_name,
35
+ replaced)) do |corrector|
36
+ unless node.method?(:be_all)
37
+ corrector.replace(node.loc.selector, replaced)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def message(bad_method, good_method)
45
+ format(MSG, bad: bad_method, good: good_method)
46
+ end
47
+
48
+ def replaceable_arguments?(node)
49
+ if node.method?(:be_all)
50
+ node.first_argument.send_type?
51
+ else
52
+ true
53
+ end
54
+ end
55
+
56
+ def replaced_method_name(method_name)
57
+ name = method_name.to_s.delete_prefix('be_')
58
+ if name == 'exists'
59
+ 'exist'
60
+ else
61
+ name
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end