rubocop-rspec 2.25.0 → 2.27.1

Sign up to get free protection for your applications and to get access to all the features.
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
  )