rubocop-rspec 2.25.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/config/default.yml +39 -4
  4. data/lib/rubocop/cop/rspec/around_block.rb +3 -3
  5. data/lib/rubocop/cop/rspec/be.rb +1 -1
  6. data/lib/rubocop/cop/rspec/be_empty.rb +1 -0
  7. data/lib/rubocop/cop/rspec/be_eq.rb +1 -1
  8. data/lib/rubocop/cop/rspec/be_eql.rb +1 -1
  9. data/lib/rubocop/cop/rspec/be_nil.rb +2 -2
  10. data/lib/rubocop/cop/rspec/before_after_all.rb +7 -13
  11. data/lib/rubocop/cop/rspec/capybara/feature_methods.rb +2 -2
  12. data/lib/rubocop/cop/rspec/change_by_zero.rb +30 -4
  13. data/lib/rubocop/cop/rspec/context_method.rb +2 -2
  14. data/lib/rubocop/cop/rspec/context_wording.rb +1 -1
  15. data/lib/rubocop/cop/rspec/describe_symbol.rb +1 -1
  16. data/lib/rubocop/cop/rspec/described_class.rb +33 -11
  17. data/lib/rubocop/cop/rspec/empty_example_group.rb +2 -2
  18. data/lib/rubocop/cop/rspec/empty_line_after_example.rb +2 -2
  19. data/lib/rubocop/cop/rspec/example_without_description.rb +11 -2
  20. data/lib/rubocop/cop/rspec/example_wording.rb +11 -2
  21. data/lib/rubocop/cop/rspec/excessive_docstring_spacing.rb +1 -1
  22. data/lib/rubocop/cop/rspec/expect_actual.rb +5 -2
  23. data/lib/rubocop/cop/rspec/expect_change.rb +2 -2
  24. data/lib/rubocop/cop/rspec/expect_output.rb +1 -4
  25. data/lib/rubocop/cop/rspec/focus.rb +2 -2
  26. data/lib/rubocop/cop/rspec/hook_argument.rb +2 -2
  27. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +1 -1
  28. data/lib/rubocop/cop/rspec/implicit_block_expectation.rb +2 -2
  29. data/lib/rubocop/cop/rspec/implicit_expect.rb +1 -1
  30. data/lib/rubocop/cop/rspec/implicit_subject.rb +2 -2
  31. data/lib/rubocop/cop/rspec/instance_spy.rb +2 -2
  32. data/lib/rubocop/cop/rspec/instance_variable.rb +2 -2
  33. data/lib/rubocop/cop/rspec/is_expected_specify.rb +45 -0
  34. data/lib/rubocop/cop/rspec/iterated_expectation.rb +3 -3
  35. data/lib/rubocop/cop/rspec/let_before_examples.rb +1 -1
  36. data/lib/rubocop/cop/rspec/let_setup.rb +1 -1
  37. data/lib/rubocop/cop/rspec/message_expectation.rb +1 -2
  38. data/lib/rubocop/cop/rspec/message_spies.rb +0 -2
  39. data/lib/rubocop/cop/rspec/mixin/skip_or_pending.rb +2 -2
  40. data/lib/rubocop/cop/rspec/multiple_expectations.rb +12 -7
  41. data/lib/rubocop/cop/rspec/pending.rb +1 -1
  42. data/lib/rubocop/cop/rspec/pending_without_reason.rb +1 -1
  43. data/lib/rubocop/cop/rspec/predicate_matcher.rb +6 -6
  44. data/lib/rubocop/cop/rspec/rails/avoid_setup_hook.rb +1 -1
  45. data/lib/rubocop/cop/rspec/rails/have_http_status.rb +34 -10
  46. data/lib/rubocop/cop/rspec/rails/http_status.rb +1 -1
  47. data/lib/rubocop/cop/rspec/rails/minitest_assertions.rb +314 -22
  48. data/lib/rubocop/cop/rspec/receive_counts.rb +1 -1
  49. data/lib/rubocop/cop/rspec/receive_messages.rb +1 -1
  50. data/lib/rubocop/cop/rspec/redundant_predicate_matcher.rb +67 -0
  51. data/lib/rubocop/cop/rspec/remove_const.rb +40 -0
  52. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +1 -1
  53. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +2 -2
  54. data/lib/rubocop/cop/rspec/repeated_include_example.rb +1 -1
  55. data/lib/rubocop/cop/rspec/repeated_subject_call.rb +124 -0
  56. data/lib/rubocop/cop/rspec/return_from_stub.rb +1 -1
  57. data/lib/rubocop/cop/rspec/shared_context.rb +1 -1
  58. data/lib/rubocop/cop/rspec/shared_examples.rb +66 -20
  59. data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +2 -3
  60. data/lib/rubocop/cop/rspec/stubbed_mock.rb +1 -1
  61. data/lib/rubocop/cop/rspec/subject_stub.rb +4 -4
  62. data/lib/rubocop/cop/rspec/unspecified_exception.rb +2 -2
  63. data/lib/rubocop/cop/rspec/variable_definition.rb +2 -2
  64. data/lib/rubocop/cop/rspec/verified_doubles.rb +1 -1
  65. data/lib/rubocop/cop/rspec/void_expect.rb +2 -2
  66. data/lib/rubocop/cop/rspec_cops.rb +4 -0
  67. data/lib/rubocop/rspec/language.rb +8 -8
  68. data/lib/rubocop/rspec/version.rb +1 -1
  69. data/lib/rubocop/rspec/wording.rb +8 -0
  70. metadata +10 -6
@@ -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
@@ -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
 
@@ -148,7 +148,7 @@ module RuboCop
148
148
  end
149
149
 
150
150
  def heredoc_or_splat?(node)
151
- (node.str_type? || node.dstr_type?) && node.heredoc? ||
151
+ ((node.str_type? || node.dstr_type?) && node.heredoc?) ||
152
152
  node.splat_type?
153
153
  end
154
154
 
@@ -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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks that `remove_const` is not used in specs.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # it 'does something' do
11
+ # Object.send(:remove_const, :SomeConstant)
12
+ # end
13
+ #
14
+ # before do
15
+ # SomeClass.send(:remove_const, :SomeConstant)
16
+ # end
17
+ #
18
+ class RemoveConst < Base
19
+ include RuboCop::RSpec::Language
20
+ extend RuboCop::RSpec::Language::NodePattern
21
+
22
+ MSG = 'Do not use remove_const in specs. ' \
23
+ 'Consider using e.g. `stub_const`.'
24
+ RESTRICT_ON_SEND = %i[send __send__].freeze
25
+
26
+ # @!method remove_const(node)
27
+ def_node_matcher :remove_const, <<~PATTERN
28
+ (send _ {:send | :__send__} (sym :remove_const) _)
29
+ PATTERN
30
+
31
+ # Check for offenses
32
+ def on_send(node)
33
+ remove_const(node) do
34
+ add_offense(node)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  MSG = 'Repeated %<group>s block body on line(s) %<loc>s'
49
49
 
50
50
  # @!method several_example_groups?(node)
51
- def_node_matcher :several_example_groups?, <<-PATTERN
51
+ def_node_matcher :several_example_groups?, <<~PATTERN
52
52
  (begin <#example_group_with_body? #example_group_with_body? ...>)
53
53
  PATTERN
54
54
 
@@ -48,12 +48,12 @@ module RuboCop
48
48
  MSG = 'Repeated %<group>s block description on line(s) %<loc>s'
49
49
 
50
50
  # @!method several_example_groups?(node)
51
- def_node_matcher :several_example_groups?, <<-PATTERN
51
+ def_node_matcher :several_example_groups?, <<~PATTERN
52
52
  (begin <#example_group? #example_group? ...>)
53
53
  PATTERN
54
54
 
55
55
  # @!method doc_string_and_metadata(node)
56
- def_node_matcher :doc_string_and_metadata, <<-PATTERN
56
+ def_node_matcher :doc_string_and_metadata, <<~PATTERN
57
57
  (block (send _ _ $_ $...) ...)
58
58
  PATTERN
59
59
 
@@ -50,7 +50,7 @@ module RuboCop
50
50
  'on line(s) %<repeat>s'
51
51
 
52
52
  # @!method several_include_examples?(node)
53
- def_node_matcher :several_include_examples?, <<-PATTERN
53
+ def_node_matcher :several_include_examples?, <<~PATTERN
54
54
  (begin <#include_examples? #include_examples? ...>)
55
55
  PATTERN
56
56
 
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for repeated calls to subject missing that it is memoized.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # it do
11
+ # subject
12
+ # expect { subject }.to not_change { A.count }
13
+ # end
14
+ #
15
+ # it do
16
+ # expect { subject }.to change { A.count }
17
+ # expect { subject }.to not_change { A.count }
18
+ # end
19
+ #
20
+ # # good
21
+ # it do
22
+ # expect { my_method }.to change { A.count }
23
+ # expect { my_method }.to not_change { A.count }
24
+ # end
25
+ #
26
+ # # also good
27
+ # it do
28
+ # expect { subject.a }.to change { A.count }
29
+ # expect { subject.b }.to not_change { A.count }
30
+ # end
31
+ #
32
+ class RepeatedSubjectCall < Base
33
+ include TopLevelGroup
34
+
35
+ MSG = 'Calls to subject are memoized, this block is misleading'
36
+
37
+ # @!method subject?(node)
38
+ # Find a named or unnamed subject definition
39
+ #
40
+ # @example anonymous subject
41
+ # subject?(parse('subject { foo }').ast) do |name|
42
+ # name # => :subject
43
+ # end
44
+ #
45
+ # @example named subject
46
+ # subject?(parse('subject(:thing) { foo }').ast) do |name|
47
+ # name # => :thing
48
+ # end
49
+ #
50
+ # @param node [RuboCop::AST::Node]
51
+ #
52
+ # @yield [Symbol] subject name
53
+ def_node_matcher :subject?, <<-PATTERN
54
+ (block
55
+ (send nil?
56
+ { #Subjects.all (sym $_) | $#Subjects.all }
57
+ ) args ...)
58
+ PATTERN
59
+
60
+ # @!method subject_calls(node, method_name)
61
+ def_node_search :subject_calls, <<~PATTERN
62
+ (send nil? %)
63
+ PATTERN
64
+
65
+ def on_top_level_group(node)
66
+ @subjects_by_node = detect_subjects_in_scope(node)
67
+
68
+ detect_offenses_in_block(node)
69
+ end
70
+
71
+ private
72
+
73
+ def detect_offense(subject_node)
74
+ return if subject_node.chained?
75
+ return unless (block_node = expect_block(subject_node))
76
+
77
+ add_offense(block_node)
78
+ end
79
+
80
+ def expect_block(node)
81
+ node.each_ancestor(:block).find { |block| block.method?(:expect) }
82
+ end
83
+
84
+ def detect_offenses_in_block(node, subject_names = [])
85
+ subject_names = [*subject_names, *@subjects_by_node[node]]
86
+
87
+ if example?(node)
88
+ return detect_offenses_in_example(node, subject_names)
89
+ end
90
+
91
+ node.each_child_node(:send, :def, :block, :begin) do |child|
92
+ detect_offenses_in_block(child, subject_names)
93
+ end
94
+ end
95
+
96
+ def detect_offenses_in_example(node, subject_names)
97
+ return unless node.body
98
+
99
+ subjects_used = Hash.new(false)
100
+
101
+ subject_calls(node.body, Set[*subject_names, :subject]).each do |call|
102
+ if subjects_used[call.method_name]
103
+ detect_offense(call)
104
+ else
105
+ subjects_used[call.method_name] = true
106
+ end
107
+ end
108
+ end
109
+
110
+ def detect_subjects_in_scope(node)
111
+ node.each_descendant(:block).with_object({}) do |child, h|
112
+ subject?(child) do |name|
113
+ outer_example_group = child.each_ancestor(:block).find do |a|
114
+ example_group?(a)
115
+ end
116
+
117
+ (h[outer_example_group] ||= []) << name
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  def_node_matcher :stub_with_block?, '(block #contains_stub? ...)'
49
49
 
50
50
  # @!method and_return_value(node)
51
- def_node_search :and_return_value, <<-PATTERN
51
+ def_node_search :and_return_value, <<~PATTERN
52
52
  $(send _ :and_return $(...))
53
53
  PATTERN
54
54
 
@@ -62,7 +62,7 @@ module RuboCop
62
62
  PATTERN
63
63
 
64
64
  # @!method context?(node)
65
- def_node_search :context?, <<-PATTERN
65
+ def_node_search :context?, <<~PATTERN
66
66
  (send nil?
67
67
  {#Subjects.all #Helpers.all #Includes.context #Hooks.all} ...
68
68
  )