rubocop-rspec_rails 2.32.0 → 2.33.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a750fc4e562218ed87ba5242d34797589b5df5598ad0cac33799546bf2a0230
4
- data.tar.gz: 76e8e5c35753d371fa4fd4fdb41ced69adb33c2168d0232f6b2ba5f73a82b5da
3
+ metadata.gz: a42fb50c1cc6fac2f7c5a5c81dcf9573d4cbc27390157cc0063b907c880b12ae
4
+ data.tar.gz: b4e95f46c045360b80c76177b975ba2932c247974e63d56cd7e376b176831288
5
5
  SHA512:
6
- metadata.gz: 8929c0661a8c342e41d652597c24549eefef4a3fad2ad566588df2bae35aeed79d68c44ea2a1d7270ee91d089c698095889e41b3f9012b3cddad2fadba6a4d48
7
- data.tar.gz: a4a1dfde481062a1d2445100d753bfb66df082537c0157a2189ba6af9ab4f5dd8a459561278db7e2b80c434009455b908927549f201a7aa692f66fc4b28f1adc
6
+ metadata.gz: 594c53b8d30e097e94063476fd9d42d014ae3f4913a029956b66a9c25be1064d8136bedeea8247e7845bfd6278b614a8a7ea7da2a71e1677739500c44ce24782
7
+ data.tar.gz: 7318471ee0e4062142c4a64ca3ad5a718d7d00c6653d6210d7345bbf60208fe51f8b4c815849e369daecf3810d59d351c69b330ddb7130c028b89cbeb21af152
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.33.0 (2026-10-07)
6
+
7
+ - Add `RSpecRails/FileFixture` cop to prefer `file_fixture` over hardcoded fixture paths. ([@ydah])
8
+ - Fix `RSpecRails/HttpStatus` to detect unknown symbolic HTTP statuses and handle custom numeric string statuses consistently. ([@ydah])
9
+ - Speed up loading rubocop-rspec_rails by lazily loading only the cops needed for a run. This requires RuboCop 1.89.0+. ([@koic])
10
+ - Fix offense message for `RSpecRails/HttpStatusNameConsistency` cop. ([@fatkodima])
11
+ - Fix `RSpecRails/MinitestAssertions` autocorrection for quoted predicate symbols. ([@ydah])
12
+ - Fix `RSpecRails/AvoidSetupHook` to only check setup hooks inside example groups. ([@ydah])
13
+ - Fix a false positive for `RspecRails/NegationBeValid` when use `to_not`. ([@ydah])
14
+ - Fix false positives for `RSpecRails/TravelAround` when travel does not directly wrap the around example. ([@ydah])
15
+ - Supporting correcting `assest_redirected_to` in `RSpec/Rails/MinitestAssertions`. ([@nzlaura])
16
+ - Add new `RSpecRails/ReceivePerformLater` cop. ([@ydah])
17
+ - Fix `RSpecRails/ReceivePerformLater` offense range to exclude chains after the expectation. ([@ydah])
18
+ - Fix a false positive for `RSpecRails/ReceivePerformLater` when `receive(:perform_later)` is used inside another matcher. ([@ydah])
19
+ - Support `described_class` for `RSpecRails/ReceivePerformLater`. ([@ydah])
20
+ - Fix `RSpecRails/TravelAround` autocorrection to keep the indentation of the surrounding `around` block. ([@eugeneius])
21
+ - Fix `RSpecRails/ReceivePerformLater` offense messages for negative expectations. ([@ydah])
22
+
5
23
  ## 2.32.0 (2025-11-12)
6
24
 
7
25
  - Add `RSpecRails/HttpStatusNameConsistency` cop. ([@taketo1113])
@@ -83,8 +101,11 @@
83
101
  [@anthony-robin]: https://github.com/anthony-robin
84
102
  [@bquorning]: https://github.com/bquorning
85
103
  [@corydiamand]: https://github.com/corydiamand
104
+ [@eugeneius]: https://github.com/eugeneius
105
+ [@fatkodima]: https://github.com/fatkodima
86
106
  [@g-rath]: https://github.com/G-Rath
87
107
  [@jojos003]: https://github.com/jojos003
108
+ [@koic]: https://github.com/koic
88
109
  [@mothonmars]: https://github.com/MothOnMars
89
110
  [@mvz]: https://github.com/mvz
90
111
  [@nzlaura]: https://github.com/nzlaura
data/config/default.yml CHANGED
@@ -7,11 +7,18 @@ RSpecRails:
7
7
  - "**/spec/**/*"
8
8
 
9
9
  RSpecRails/AvoidSetupHook:
10
- Description: Checks that tests use RSpec `before` hook over Rails `setup` method.
10
+ Description: Checks that tests use RSpec `before` instead of Rails `setup`.
11
11
  Enabled: pending
12
12
  VersionAdded: '2.4'
13
13
  Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/AvoidSetupHook
14
14
 
15
+ RSpecRails/FileFixture:
16
+ Description: Prefer `file_fixture` over hardcoded paths into `spec/fixtures`.
17
+ Enabled: pending
18
+ Safe: false
19
+ VersionAdded: '2.33'
20
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/FileFixture
21
+
15
22
  RSpecRails/HaveHttpStatus:
16
23
  Description: Checks that tests use `have_http_status` instead of equality matchers.
17
24
  Enabled: pending
@@ -65,7 +72,7 @@ RSpecRails/InferredSpecType:
65
72
  views: view
66
73
 
67
74
  RSpecRails/MinitestAssertions:
68
- Description: Check if using Minitest-like matchers.
75
+ Description: Detects Minitest-like matchers.
69
76
  Enabled: pending
70
77
  VersionAdded: '2.17'
71
78
  Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/MinitestAssertions
@@ -83,8 +90,14 @@ RSpecRails/NegationBeValid:
83
90
  VersionChanged: '2.29'
84
91
  Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid
85
92
 
93
+ RSpecRails/ReceivePerformLater:
94
+ Description: Prefer `have_enqueued_job` over `receive(:perform_later)`.
95
+ Enabled: pending
96
+ VersionAdded: '2.33'
97
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater
98
+
86
99
  RSpecRails/TravelAround:
87
- Description: Prefer to travel in `before` rather than `around`.
100
+ Description: Checks for time travel helpers in `around` hooks.
88
101
  Enabled: pending
89
102
  Safe: false
90
103
  VersionAdded: '2.19'
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpecRails
6
- # Checks that tests use RSpec `before` hook over Rails `setup` method.
6
+ # Checks that tests use RSpec `before` instead of Rails `setup`.
7
7
  #
8
8
  # @example
9
9
  # # bad
@@ -16,8 +16,9 @@ module RuboCop
16
16
  # allow(foo).to receive(:bar)
17
17
  # end
18
18
  #
19
- class AvoidSetupHook < ::RuboCop::Cop::Base
19
+ class AvoidSetupHook < RuboCop::Cop::RSpec::Base
20
20
  extend AutoCorrector
21
+ include RuboCop::Cop::RSpec::InsideExampleGroup
21
22
 
22
23
  MSG = 'Use `before` instead of `setup`.'
23
24
 
@@ -28,7 +29,9 @@ module RuboCop
28
29
  (args) _)
29
30
  PATTERN
30
31
 
31
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
32
+ def on_block(node) # rubocop:disable InternalAffairs/ItblockHandler, InternalAffairs/NumblockHandler
33
+ return unless inside_example_group?(node)
34
+
32
35
  setup_call(node) do |setup|
33
36
  add_offense(node) do |corrector|
34
37
  corrector.replace setup, 'before'
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpecRails
6
+ # Prefer `file_fixture` over hardcoded paths into `spec/fixtures`.
7
+ #
8
+ # @safety
9
+ # `file_fixture` uses the configured `file_fixture_path`, which defaults
10
+ # to `spec/fixtures/files`. A file under `spec/fixtures` may need to be
11
+ # moved or `file_fixture_path` configured before replacing the path.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # Rails.root.join('spec', 'fixtures', 'files', 'example.pdf')
16
+ #
17
+ # # good
18
+ # file_fixture('example.pdf')
19
+ class FileFixture < RuboCop::Cop::Base
20
+ MSG = 'Prefer `file_fixture` for files under `spec/fixtures`.'
21
+ RESTRICT_ON_SEND = %i[join].freeze
22
+
23
+ # @!method fixture_path?(node)
24
+ def_node_matcher :fixture_path?, <<~PATTERN
25
+ (send
26
+ (send (const {nil? cbase} :Rails) :root)
27
+ :join (str "spec") (str "fixtures") _ ...)
28
+ PATTERN
29
+
30
+ # @!method combined_fixture_path?(node)
31
+ def_node_matcher :combined_fixture_path?, <<~PATTERN
32
+ (send
33
+ (send (const {nil? cbase} :Rails) :root)
34
+ :join (str "spec/fixtures") _ ...)
35
+ PATTERN
36
+
37
+ def on_send(node)
38
+ return unless fixture_path?(node) || combined_fixture_path?(node)
39
+
40
+ add_offense(node.loc.selector)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -5,7 +5,11 @@ module RuboCop
5
5
  module RSpecRails
6
6
  # Checks that tests use `have_http_status` instead of equality matchers.
7
7
  #
8
- # @example ResponseMethods: ['response', 'last_response'] (default)
8
+ # @safety
9
+ # The autocorrection is marked as unsafe because
10
+ # `response.status` response is not always an HTTP response status.
11
+ #
12
+ # @example `ResponseMethods: ['response', 'last_response']` (default)
9
13
  # # bad
10
14
  # expect(response.status).to be(200)
11
15
  # expect(last_response.code).to eq("200")
@@ -14,7 +18,7 @@ module RuboCop
14
18
  # expect(response).to have_http_status(200)
15
19
  # expect(last_response).to have_http_status(200)
16
20
  #
17
- # @example ResponseMethods: ['foo_response']
21
+ # @example `ResponseMethods: ['foo_response']`
18
22
  # # bad
19
23
  # expect(foo_response.status).to be(200)
20
24
  #
@@ -25,7 +29,7 @@ module RuboCop
25
29
  # expect(response).to have_http_status(200)
26
30
  # expect(last_response).to have_http_status(200)
27
31
  #
28
- class HaveHttpStatus < ::RuboCop::Cop::Base
32
+ class HaveHttpStatus < RuboCop::Cop::Base
29
33
  extend AutoCorrector
30
34
 
31
35
  MSG =
@@ -64,9 +64,10 @@ module RuboCop
64
64
  #
65
65
  # # good
66
66
  # it { is_expected.to have_http_status :ok }
67
- class HttpStatus < ::RuboCop::Cop::Base
67
+ class HttpStatus < RuboCop::Cop::Base
68
68
  extend AutoCorrector
69
69
  include ConfigurableEnforcedStyle
70
+
70
71
  RESTRICT_ON_SEND = %i[have_http_status].freeze
71
72
 
72
73
  # @!method http_status(node)
@@ -142,16 +143,44 @@ module RuboCop
142
143
  node.sym_type? && ALLOWED_STATUSES.include?(node.value)
143
144
  end
144
145
 
146
+ def known_status_symbol?
147
+ node.sym_type? &&
148
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(node.value)
149
+ end
150
+
151
+ def known_status_code?
152
+ numeric_status_code? &&
153
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE.value?(status_code_number)
154
+ end
155
+
145
156
  def custom_http_status_code?
146
- node.int_type? &&
147
- !::Rack::Utils::SYMBOL_TO_STATUS_CODE.value?(node.source.to_i)
157
+ numeric_status_code? && !known_status_code?
158
+ end
159
+
160
+ def numeric_status_code?
161
+ node.int_type? || numeric_string?
162
+ end
163
+
164
+ def numeric_string?
165
+ node.str_type? && node.value.match?(/\A\d+\z/)
166
+ end
167
+
168
+ def status_code_number
169
+ node.value.to_i
170
+ end
171
+
172
+ def status_code_symbol
173
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(status_code_number)
148
174
  end
149
175
  end
150
176
 
151
177
  # :nodoc:
152
178
  class SymbolicStyleChecker < StyleCheckerBase
153
179
  def offensive?
154
- !node.sym_type? && !custom_http_status_code?
180
+ return false if allowed_symbol? || known_status_symbol?
181
+ return false if custom_http_status_code?
182
+
183
+ true
155
184
  end
156
185
 
157
186
  def autocorrectable?
@@ -165,18 +194,17 @@ module RuboCop
165
194
  private
166
195
 
167
196
  def symbol
168
- ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(number)
169
- end
170
-
171
- def number
172
- node.value.to_i
197
+ status_code_symbol if known_status_code?
173
198
  end
174
199
  end
175
200
 
176
201
  # :nodoc:
177
202
  class NumericStyleChecker < StyleCheckerBase
178
203
  def offensive?
179
- !node.int_type? && !allowed_symbol?
204
+ return false if node.int_type? || allowed_symbol?
205
+ return false if custom_http_status_code?
206
+
207
+ true
180
208
  end
181
209
 
182
210
  def autocorrectable?
@@ -189,20 +217,19 @@ module RuboCop
189
217
 
190
218
  private
191
219
 
192
- def symbol
193
- node.value
194
- end
195
-
196
220
  def number
197
- ::Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol.to_sym]
221
+ return status_code_number if known_status_code?
222
+
223
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE[node.value.to_sym]
198
224
  end
199
225
  end
200
226
 
201
227
  # :nodoc:
202
228
  class BeStatusStyleChecker < StyleCheckerBase
203
229
  def offensive?
204
- (!node.sym_type? && !custom_http_status_code?) ||
205
- (!node.int_type? && !allowed_symbol?)
230
+ return false if allowed_symbol? || custom_http_status_code?
231
+
232
+ true
206
233
  end
207
234
 
208
235
  def autocorrectable?
@@ -220,30 +247,18 @@ module RuboCop
220
247
  private
221
248
 
222
249
  def status_code
223
- if node.sym_type?
250
+ if known_status_symbol?
224
251
  node.value
225
- elsif node.int_type?
226
- symbol
252
+ elsif known_status_code?
253
+ status_code_symbol
227
254
  else
228
255
  normalize_str
229
256
  end
230
257
  end
231
258
 
232
- def symbol
233
- ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(number)
234
- end
235
-
236
- def number
237
- node.value.to_i
238
- end
239
-
240
259
  def normalize_str
241
260
  str = node.value.to_s
242
- if str.match?(/\A\d+\z/)
243
- ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key(str.to_i)
244
- elsif ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(str.to_sym)
245
- str
246
- end
261
+ str if ::Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(str.to_sym)
247
262
  end
248
263
  end
249
264
  end
@@ -13,12 +13,12 @@ module RuboCop
13
13
  # # good
14
14
  # it { is_expected.to have_http_status :unprocessable_content }
15
15
  #
16
- class HttpStatusNameConsistency < ::RuboCop::Cop::Base
16
+ class HttpStatusNameConsistency < RuboCop::Cop::Base
17
17
  extend AutoCorrector
18
18
 
19
19
  requires_gem 'rack', '>= 3.1.0'
20
20
 
21
- MSG = 'Use `Prefer `:%<preferred>s` over `:%<current>s`.'
21
+ MSG = 'Prefer `:%<preferred>s` over `:%<current>s`.'
22
22
 
23
23
  RESTRICT_ON_SEND = %i[have_http_status].freeze
24
24
 
@@ -30,12 +30,7 @@ module RuboCop
30
30
  # RSpec.describe User, type: :common do
31
31
  # end
32
32
  #
33
- # @example `Inferences` configuration
34
- # # .rubocop.yml
35
- # # RSpecRails/InferredSpecType:
36
- # # Inferences:
37
- # # services: service
38
- #
33
+ # @example `Inferences: {services: service}`
39
34
  # # bad
40
35
  # # spec/services/user_spec.rb
41
36
  # RSpec.describe User, type: :service do
@@ -50,13 +45,13 @@ module RuboCop
50
45
  # # spec/services/user_spec.rb
51
46
  # RSpec.describe User, type: :common do
52
47
  # end
53
- class InferredSpecType < ::RuboCop::Cop::RSpec::Base
48
+ class InferredSpecType < RuboCop::Cop::RSpec::Base
54
49
  extend AutoCorrector
55
50
 
56
51
  MSG = 'Remove redundant spec type.'
57
52
 
58
53
  # @param [RuboCop::AST::BlockNode] node
59
- def on_block(node)
54
+ def on_block(node) # rubocop:disable InternalAffairs/ItblockHandler
60
55
  return unless example_group?(node)
61
56
 
62
57
  pair_node = describe_with_type(node)
@@ -3,10 +3,10 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpecRails
6
- # Check if using Minitest-like matchers.
6
+ # Detects Minitest-like matchers.
7
7
  #
8
- # Check the use of minitest-like matchers
9
- # starting with `assert_` or `refute_`.
8
+ # Detects the use of Minitest-like matchers
9
+ # (those starting with `assert_` or `refute_`).
10
10
  #
11
11
  # @example
12
12
  # # bad
@@ -19,6 +19,7 @@ module RuboCop
19
19
  # assert_true(a)
20
20
  # assert_false(a)
21
21
  # assert_response :ok
22
+ # assert_redirected_to '/users'
22
23
  #
23
24
  # # good
24
25
  # expect(b).to eq(a)
@@ -30,8 +31,9 @@ module RuboCop
30
31
  # expect(a).to be(true)
31
32
  # expect(a).to be(false)
32
33
  # expect(response).to have_http_status(:ok)
34
+ # expect(response).to redirect_to('/users')
33
35
  #
34
- class MinitestAssertions < ::RuboCop::Cop::Base
36
+ class MinitestAssertions < RuboCop::Cop::Base
35
37
  extend AutoCorrector
36
38
 
37
39
  # :nodoc:
@@ -40,12 +42,25 @@ module RuboCop
40
42
 
41
43
  attr_reader :expected, :actual, :failure_message
42
44
 
45
+ def self.match_assertions(*matchers, captures:)
46
+ const_set(:MATCHERS, matchers.freeze)
47
+
48
+ matcher_pattern = matchers.map { |matcher| ":#{matcher}" }.join(' ')
49
+ def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
50
+ (send nil? {#{matcher_pattern}} #{captures})
51
+ PATTERN
52
+ end
53
+
43
54
  def self.minitest_assertion
44
55
  # :nocov:
45
56
  raise NotImplementedError
46
57
  # :nocov:
47
58
  end
48
59
 
60
+ def self.match(expected, actual, failure_message)
61
+ new(expected, actual, failure_message.first)
62
+ end
63
+
49
64
  def initialize(expected, actual, failure_message)
50
65
  @expected = expected&.source
51
66
  @actual = actual.source
@@ -74,20 +89,10 @@ module RuboCop
74
89
 
75
90
  # :nodoc:
76
91
  class EqualAssertion < BasicAssertion
77
- MATCHERS = %i[
78
- assert_equal
79
- assert_not_equal
80
- refute_equal
81
- ].freeze
82
-
83
- # @!method self.minitest_assertion(node)
84
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
85
- (send nil? {:assert_equal :assert_not_equal :refute_equal} $_ $_ $_?)
86
- PATTERN
87
-
88
- def self.match(expected, actual, failure_message)
89
- new(expected, actual, failure_message.first)
90
- end
92
+ match_assertions :assert_equal,
93
+ :assert_not_equal,
94
+ :refute_equal,
95
+ captures: '$_ $_ $_?'
91
96
 
92
97
  def assertion
93
98
  "eq(#{expected})"
@@ -96,20 +101,10 @@ module RuboCop
96
101
 
97
102
  # :nodoc:
98
103
  class KindOfAssertion < BasicAssertion
99
- MATCHERS = %i[
100
- assert_kind_of
101
- assert_not_kind_of
102
- refute_kind_of
103
- ].freeze
104
-
105
- # @!method self.minitest_assertion(node)
106
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
107
- (send nil? {:assert_kind_of :assert_not_kind_of :refute_kind_of} $_ $_ $_?)
108
- PATTERN
109
-
110
- def self.match(expected, actual, failure_message)
111
- new(expected, actual, failure_message.first)
112
- end
104
+ match_assertions :assert_kind_of,
105
+ :assert_not_kind_of,
106
+ :refute_kind_of,
107
+ captures: '$_ $_ $_?'
113
108
 
114
109
  def assertion
115
110
  "be_a_kind_of(#{expected})"
@@ -118,20 +113,10 @@ module RuboCop
118
113
 
119
114
  # :nodoc:
120
115
  class InstanceOfAssertion < BasicAssertion
121
- MATCHERS = %i[
122
- assert_instance_of
123
- assert_not_instance_of
124
- refute_instance_of
125
- ].freeze
126
-
127
- # @!method self.minitest_assertion(node)
128
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
129
- (send nil? {:assert_instance_of :assert_not_instance_of :refute_instance_of} $_ $_ $_?)
130
- PATTERN
131
-
132
- def self.match(expected, actual, failure_message)
133
- new(expected, actual, failure_message.first)
134
- end
116
+ match_assertions :assert_instance_of,
117
+ :assert_not_instance_of,
118
+ :refute_instance_of,
119
+ captures: '$_ $_ $_?'
135
120
 
136
121
  def assertion
137
122
  "be_an_instance_of(#{expected})"
@@ -140,16 +125,10 @@ module RuboCop
140
125
 
141
126
  # :nodoc:
142
127
  class IncludesAssertion < BasicAssertion
143
- MATCHERS = %i[
144
- assert_includes
145
- assert_not_includes
146
- refute_includes
147
- ].freeze
148
-
149
- # @!method self.minitest_assertion(node)
150
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
151
- (send nil? {:assert_includes :assert_not_includes :refute_includes} $_ $_ $_?)
152
- PATTERN
128
+ match_assertions :assert_includes,
129
+ :assert_not_includes,
130
+ :refute_includes,
131
+ captures: '$_ $_ $_?'
153
132
 
154
133
  def self.match(collection, expected, failure_message)
155
134
  new(expected, collection, failure_message.first)
@@ -162,16 +141,10 @@ module RuboCop
162
141
 
163
142
  # :nodoc:
164
143
  class InDeltaAssertion < BasicAssertion
165
- MATCHERS = %i[
166
- assert_in_delta
167
- assert_not_in_delta
168
- refute_in_delta
169
- ].freeze
170
-
171
- # @!method self.minitest_assertion(node)
172
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
173
- (send nil? {:assert_in_delta :assert_not_in_delta :refute_in_delta} $_ $_ $_? $_?)
174
- PATTERN
144
+ match_assertions :assert_in_delta,
145
+ :assert_not_in_delta,
146
+ :refute_in_delta,
147
+ captures: '$_ $_ $_? $_?'
175
148
 
176
149
  def self.match(expected, actual, delta, failure_message)
177
150
  new(expected, actual, delta.first, failure_message.first)
@@ -190,43 +163,41 @@ module RuboCop
190
163
 
191
164
  # :nodoc:
192
165
  class PredicateAssertion < BasicAssertion
193
- MATCHERS = %i[
194
- assert_predicate
195
- assert_not_predicate
196
- refute_predicate
197
- ].freeze
198
-
199
- # @!method self.minitest_assertion(node)
200
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
201
- (send nil? {:assert_predicate :assert_not_predicate :refute_predicate} $_ ${sym} $_?)
202
- PATTERN
166
+ match_assertions :assert_predicate,
167
+ :assert_not_predicate,
168
+ :refute_predicate,
169
+ captures: '$_ ${sym} $_?'
203
170
 
204
171
  def self.match(subject, predicate, failure_message)
205
- return nil unless predicate.value.end_with?('?')
172
+ predicate_name = predicate.value.to_s
173
+ return nil unless predicate_name.end_with?('?')
174
+
175
+ matcher_name = predicate_name.delete_suffix('?')
176
+ return nil unless valid_matcher_name?(matcher_name)
206
177
 
207
- new(predicate, subject, failure_message.first)
178
+ new(predicate, subject, failure_message.first, matcher_name)
179
+ end
180
+
181
+ def self.valid_matcher_name?(matcher_name)
182
+ matcher_name.match?(/\A\w+\z/)
183
+ end
184
+
185
+ def initialize(expected, actual, failure_message, matcher_name)
186
+ super(expected, actual, failure_message)
187
+
188
+ @matcher_name = matcher_name
208
189
  end
209
190
 
210
191
  def assertion
211
- "be_#{expected.delete_prefix(':').delete_suffix('?')}"
192
+ "be_#{@matcher_name}"
212
193
  end
213
194
  end
214
195
 
215
196
  # :nodoc:
216
197
  class MatchAssertion < BasicAssertion
217
- MATCHERS = %i[
218
- assert_match
219
- refute_match
220
- ].freeze
221
-
222
- # @!method self.minitest_assertion(node)
223
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
224
- (send nil? {:assert_match :refute_match} $_ $_ $_?)
225
- PATTERN
226
-
227
- def self.match(matcher, actual, failure_message)
228
- new(matcher, actual, failure_message.first)
229
- end
198
+ match_assertions :assert_match,
199
+ :refute_match,
200
+ captures: '$_ $_ $_?'
230
201
 
231
202
  def assertion
232
203
  "match(#{expected})"
@@ -235,16 +206,10 @@ module RuboCop
235
206
 
236
207
  # :nodoc:
237
208
  class NilAssertion < BasicAssertion
238
- MATCHERS = %i[
239
- assert_nil
240
- assert_not_nil
241
- refute_nil
242
- ].freeze
243
-
244
- # @!method self.minitest_assertion(node)
245
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
246
- (send nil? {:assert_nil :assert_not_nil :refute_nil} $_ $_?)
247
- PATTERN
209
+ match_assertions :assert_nil,
210
+ :assert_not_nil,
211
+ :refute_nil,
212
+ captures: '$_ $_?'
248
213
 
249
214
  def self.match(actual, failure_message)
250
215
  new(nil, actual, failure_message.first)
@@ -257,16 +222,10 @@ module RuboCop
257
222
 
258
223
  # :nodoc:
259
224
  class EmptyAssertion < BasicAssertion
260
- MATCHERS = %i[
261
- assert_empty
262
- assert_not_empty
263
- refute_empty
264
- ].freeze
265
-
266
- # @!method self.minitest_assertion(node)
267
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
268
- (send nil? {:assert_empty :assert_not_empty :refute_empty} $_ $_?)
269
- PATTERN
225
+ match_assertions :assert_empty,
226
+ :assert_not_empty,
227
+ :refute_empty,
228
+ captures: '$_ $_?'
270
229
 
271
230
  def self.match(actual, failure_message)
272
231
  new(nil, actual, failure_message.first)
@@ -279,14 +238,7 @@ module RuboCop
279
238
 
280
239
  # :nodoc:
281
240
  class TrueAssertion < BasicAssertion
282
- MATCHERS = %i[
283
- assert_true
284
- ].freeze
285
-
286
- # @!method self.minitest_assertion(node)
287
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
288
- (send nil? {:assert_true} $_ $_?)
289
- PATTERN
241
+ match_assertions :assert_true, captures: '$_ $_?'
290
242
 
291
243
  def self.match(actual, failure_message)
292
244
  new(nil, actual, failure_message.first)
@@ -299,14 +251,7 @@ module RuboCop
299
251
 
300
252
  # :nodoc:
301
253
  class FalseAssertion < BasicAssertion
302
- MATCHERS = %i[
303
- assert_false
304
- ].freeze
305
-
306
- # @!method self.minitest_assertion(node)
307
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
308
- (send nil? {:assert_false} $_ $_?)
309
- PATTERN
254
+ match_assertions :assert_false, captures: '$_ $_?'
310
255
 
311
256
  def self.match(actual, failure_message)
312
257
  new(nil, actual, failure_message.first)
@@ -318,27 +263,36 @@ module RuboCop
318
263
  end
319
264
 
320
265
  # :nodoc:
321
- class ResponseAssertion < BasicAssertion
322
- MATCHERS = %i[
323
- assert_response
324
- ].freeze
266
+ module ResponseAssertionActual
267
+ RESPONSE_NODE = Struct.new(:source).new('response')
325
268
 
326
- ASSERT_ACTUAL_NODE = Struct.new(:source).new('response')
269
+ def match(expected, failure_message)
270
+ new(expected, RESPONSE_NODE, failure_message.first)
271
+ end
272
+ end
327
273
 
328
- # @!method self.minitest_assertion(node)
329
- def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
330
- (send nil? {:assert_response} $_ $_?)
331
- PATTERN
274
+ # :nodoc:
275
+ class ResponseAssertion < BasicAssertion
276
+ extend ResponseAssertionActual
332
277
 
333
- def self.match(expected, failure_message)
334
- new(expected, ASSERT_ACTUAL_NODE, failure_message.first)
335
- end
278
+ match_assertions :assert_response, captures: '$_ $_?'
336
279
 
337
280
  def assertion
338
281
  "have_http_status(#{expected})"
339
282
  end
340
283
  end
341
284
 
285
+ # :nodoc:
286
+ class RedirectAssertion < BasicAssertion
287
+ extend ResponseAssertionActual
288
+
289
+ match_assertions :assert_redirected_to, captures: '$_ $_?'
290
+
291
+ def assertion
292
+ "redirect_to(#{expected})"
293
+ end
294
+ end
295
+
342
296
  MSG = 'Use `%<prefer>s`.'
343
297
 
344
298
  # TODO: replace with `BasicAssertion.subclasses` in Ruby 3.1+
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop is unsafe because it cannot guarantee that
10
10
  # the test target is an instance of `ActiveModel::Validations``.
11
11
  #
12
- # @example EnforcedStyle: not_to (default)
12
+ # @example `EnforcedStyle: not_to` (default)
13
13
  # # bad
14
14
  # expect(foo).to be_invalid
15
15
  #
@@ -19,7 +19,7 @@ module RuboCop
19
19
  # # good (with method chain)
20
20
  # expect(foo).to be_invalid.and be_odd
21
21
  #
22
- # @example EnforcedStyle: be_invalid
22
+ # @example `EnforcedStyle: be_invalid`
23
23
  # # bad
24
24
  # expect(foo).not_to be_valid
25
25
  #
@@ -29,7 +29,7 @@ module RuboCop
29
29
  # # good (with method chain)
30
30
  # expect(foo).to be_invalid.or be_even
31
31
  #
32
- class NegationBeValid < ::RuboCop::Cop::Base
32
+ class NegationBeValid < RuboCop::Cop::Base
33
33
  extend AutoCorrector
34
34
  include ConfigurableEnforcedStyle
35
35
 
@@ -38,7 +38,7 @@ module RuboCop
38
38
 
39
39
  # @!method not_to?(node)
40
40
  def_node_matcher :not_to?, <<~PATTERN
41
- (send ... :not_to (send nil? :be_valid ...))
41
+ (send ... {:not_to :to_not} (send nil? :be_valid ...))
42
42
  PATTERN
43
43
 
44
44
  # @!method be_invalid?(node)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpecRails
6
+ # Prefer `have_enqueued_job` over `receive(:perform_later)`.
7
+ #
8
+ # The `have_enqueued_job` matcher is preferred for testing ActiveJob
9
+ # enqueuing. It is more explicit and provides better clarity than
10
+ # using `receive(:perform_later)`.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # expect(MyJob).to receive(:perform_later)
15
+ # do_something
16
+ #
17
+ # # bad
18
+ # allow(MyJob).to receive(:perform_later)
19
+ # do_something
20
+ # expect(MyJob).to have_received(:perform_later)
21
+ #
22
+ # # bad
23
+ # expect(MyJob).to receive(:perform_later).with(user, order)
24
+ #
25
+ # # good
26
+ # expect { do_something }.to have_enqueued_job(MyJob)
27
+ #
28
+ # # good
29
+ # expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
30
+ #
31
+ # # good
32
+ # expect { do_something }
33
+ # .to have_enqueued_job(MyJob)
34
+ # .on_queue('mailers')
35
+ # .at(Date.tomorrow.noon)
36
+ #
37
+ class ReceivePerformLater < RuboCop::Cop::Base
38
+ MSG = 'Prefer `expect { ... }.%<runner>s ' \
39
+ 'have_enqueued_job(%<job_class>s)` ' \
40
+ 'over `%<receiver>s(%<job_class>s).%<to>s ' \
41
+ '%<matcher>s(:perform_later)`.'
42
+
43
+ RESTRICT_ON_SEND = %i[receive have_received].to_set
44
+ RUNNERS = %i[to to_not not_to].freeze
45
+
46
+ # @!method receive_perform_later?(node)
47
+ def_node_matcher :receive_perform_later?, <<~PATTERN
48
+ (send nil? {:receive :have_received}
49
+ (sym :perform_later))
50
+ PATTERN
51
+
52
+ # @!method expect_or_allow?(node)
53
+ def_node_matcher :expect_or_allow?, <<~PATTERN
54
+ (send nil? {:expect :allow} {const_type? (send nil? :described_class)})
55
+ PATTERN
56
+
57
+ def on_send(node)
58
+ return unless receive_perform_later?(node)
59
+ return unless (runner_node = find_runner_node(node))
60
+ return unless runner_matcher?(node, runner_node)
61
+
62
+ register_offense(node, runner_node)
63
+ end
64
+
65
+ private
66
+
67
+ def register_offense(matcher_node, runner_node)
68
+ expect_node = runner_node.receiver
69
+ return unless expect_or_allow?(expect_node)
70
+ return if allow_receive_combination?(expect_node, matcher_node)
71
+
72
+ job_class = expect_node.first_argument
73
+ add_offense(runner_node,
74
+ message: offense_message(expect_node, job_class,
75
+ runner_node, matcher_node))
76
+ end
77
+
78
+ def allow_receive_combination?(expect_node, matcher_node)
79
+ expect_node.method?(:allow) && matcher_node.method?(:receive)
80
+ end
81
+
82
+ def offense_message(expect_node, job_class, runner_node, matcher_node)
83
+ format(MSG,
84
+ receiver: expect_node.method_name,
85
+ job_class: job_class.source,
86
+ runner: runner_node.method_name,
87
+ to: runner_node.method_name,
88
+ matcher: matcher_node.method_name)
89
+ end
90
+
91
+ def find_runner_node(node)
92
+ node.each_ancestor(:send).find { |ancestor| runner?(ancestor) }
93
+ end
94
+
95
+ def runner_matcher?(matcher_node, runner_node)
96
+ node = runner_node.first_argument
97
+ node = node.receiver while node&.send_type? && node != matcher_node
98
+ node == matcher_node
99
+ end
100
+
101
+ def runner?(node)
102
+ RUNNERS.include?(node.method_name)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -3,15 +3,26 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpecRails
6
- # Prefer to travel in `before` rather than `around`.
6
+ # Checks for time travel helpers in `around` hooks.
7
+ #
8
+ # In Rails example groups, `freeze_time` in a `before` hook does not need
9
+ # a block: Rails' `after_teardown` calls `travel_back` after each example.
10
+ # This also allows an example to use block-form time travel, which Rails
11
+ # does not allow inside another time travel block.
12
+ # See https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html
7
13
  #
8
14
  # @safety
9
- # This cop is unsafe because the automatic `travel_back` is only run
10
- # on test cases that are considered as Rails related.
15
+ # The automatic `travel_back` requires both
16
+ # `ActiveSupport::Testing::TimeHelpers` and RSpec Rails' lifecycle
17
+ # hooks. RSpec Rails applies those hooks to example groups with Rails
18
+ # `type:` metadata. File paths set that metadata only when
19
+ # `infer_spec_type_from_file_location!` is enabled. In a plain RSpec
20
+ # group that only includes TimeHelpers, use a block or call
21
+ # `travel_back` explicitly.
22
+ # See https://rspec.info/features/7-1/rspec-rails/directory-structure/
11
23
  #
12
- # And also, this cop's autocorrection is unsafe because the order of
13
- # execution will change if other steps exist before traveling in
14
- # `around`.
24
+ # Autocorrection can change execution order: `around` starts before
25
+ # all `before` hooks, while `before` runs in normal hook order.
15
26
  #
16
27
  # @example
17
28
  # # bad
@@ -29,7 +40,7 @@ module RuboCop
29
40
  # # good
30
41
  # before { freeze_time }
31
42
  #
32
- class TravelAround < ::RuboCop::Cop::Base
43
+ class TravelAround < RuboCop::Cop::Base
33
44
  extend AutoCorrector
34
45
 
35
46
  MSG = 'Prefer to travel in `before` rather than `around`.'
@@ -45,7 +56,7 @@ module RuboCop
45
56
  (block
46
57
  $(send nil? TRAVEL_METHOD_NAMES ...)
47
58
  (args ...)
48
- (send _ :run)
59
+ (send (lvar _) :run)
49
60
  )
50
61
  PATTERN
51
62
 
@@ -64,7 +75,7 @@ module RuboCop
64
75
  )
65
76
  PATTERN
66
77
 
67
- def on_block(node)
78
+ def on_block(node) # rubocop:disable InternalAffairs/ItblockHandler
68
79
  extract_run_in_travel(node) do |run_node|
69
80
  run_in_travel(node, run_node)
70
81
  end
@@ -76,26 +87,30 @@ module RuboCop
76
87
 
77
88
  private
78
89
 
79
- def run_in_travel(node, run_node)
80
- around_node = extract_surrounding_around_block(run_node)
90
+ def run_in_travel(node, travel_node)
91
+ around_node = extract_surrounding_around_block(node)
81
92
  return unless around_node
93
+ return unless matching_run_receiver?(node.body, around_node)
82
94
 
83
95
  add_offense(node) do |corrector|
84
96
  corrector.replace(node, node.body.source)
85
- corrector.insert_before(around_node,
86
- "before { #{run_node.source} }\n\n")
97
+ corrector.insert_before(
98
+ around_node,
99
+ "before { #{travel_node.source} }\n\n#{indent(around_node)}"
100
+ )
87
101
  end
88
102
  end
89
103
 
90
104
  def travel_with_block_pass(node, lvar)
91
105
  around_node = extract_surrounding_around_block(node)
92
106
  return unless around_node
107
+ return unless lvar.name == around_argument_name(around_node)
93
108
 
94
109
  add_offense(node) do |corrector|
95
110
  corrector.replace(node, "#{lvar.name}.run")
96
111
  corrector.insert_before(
97
112
  around_node,
98
- "before { #{node.method_name} }\n\n"
113
+ "before { #{node.method_name} }\n\n#{indent(around_node)}"
99
114
  )
100
115
  end
101
116
  end
@@ -104,9 +119,23 @@ module RuboCop
104
119
  # @return [RuboCop::AST::BlockNode, nil]
105
120
  def extract_surrounding_around_block(node)
106
121
  node.each_ancestor(:block).find do |ancestor|
107
- match_around_each?(ancestor)
122
+ match_around_each?(ancestor) &&
123
+ direct_child_of_around_body?(node, ancestor)
108
124
  end
109
125
  end
126
+
127
+ def direct_child_of_around_body?(node, around_node)
128
+ node.parent == around_node ||
129
+ (node.parent.begin_type? && node.parent.parent == around_node)
130
+ end
131
+
132
+ def matching_run_receiver?(run_node, around_node)
133
+ run_node.receiver.children.first == around_argument_name(around_node)
134
+ end
135
+
136
+ def around_argument_name(around_node)
137
+ around_node.first_argument&.name
138
+ end
110
139
  end
111
140
  end
112
141
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Cops for the `RSpecRails` department. The department's cops are
6
+ # registered for lazy loading and their files are loaded on demand.
7
+ module RSpecRails
8
+ extend LazyLoader
9
+
10
+ register_cop :AvoidSetupHook, "#{__dir__}/rspec_rails/avoid_setup_hook"
11
+ register_cop :FileFixture, "#{__dir__}/rspec_rails/file_fixture"
12
+ register_cop :HaveHttpStatus, "#{__dir__}/rspec_rails/have_http_status"
13
+ register_cop :HttpStatus, "#{__dir__}/rspec_rails/http_status"
14
+ register_cop :HttpStatusNameConsistency, "#{__dir__}/rspec_rails/http_status_name_consistency"
15
+ register_cop :InferredSpecType, "#{__dir__}/rspec_rails/inferred_spec_type"
16
+ register_cop :MinitestAssertions, "#{__dir__}/rspec_rails/minitest_assertions"
17
+ register_cop :NegationBeValid, "#{__dir__}/rspec_rails/negation_be_valid"
18
+ register_cop :ReceivePerformLater, "#{__dir__}/rspec_rails/receive_perform_later"
19
+ register_cop :TravelAround, "#{__dir__}/rspec_rails/travel_around"
20
+ end
21
+ end
22
+ end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'rspec_rails/avoid_setup_hook'
4
- require_relative 'rspec_rails/have_http_status'
5
- require_relative 'rspec_rails/http_status'
6
- require_relative 'rspec_rails/http_status_name_consistency'
7
- require_relative 'rspec_rails/inferred_spec_type'
8
- require_relative 'rspec_rails/minitest_assertions'
9
- require_relative 'rspec_rails/negation_be_valid'
10
- require_relative 'rspec_rails/travel_around'
3
+ # @deprecated This file is deprecated. Cops are registered for lazy loading in
4
+ # `rubocop/cop/rspec_rails`; this file is kept for compatibility with code
5
+ # that requires it directly.
6
+ require_relative 'rspec_rails'
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module RSpecRails
5
5
  # Version information for the RSpec Rails RuboCop plugin.
6
6
  module Version
7
- STRING = '2.32.0'
7
+ STRING = '2.33.0'
8
8
  end
9
9
  end
10
10
  end
@@ -10,4 +10,4 @@ require_relative 'rubocop/rspec_rails/plugin'
10
10
  require_relative 'rubocop/rspec_rails/version'
11
11
 
12
12
  require 'rubocop/cop/rspec/base'
13
- require_relative 'rubocop/cop/rspec_rails_cops'
13
+ require_relative 'rubocop/cop/rspec_rails'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rspec_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.32.0
4
+ version: 2.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Quorning
@@ -32,20 +32,14 @@ dependencies:
32
32
  requirements:
33
33
  - - "~>"
34
34
  - !ruby/object:Gem::Version
35
- version: '1.72'
36
- - - ">="
37
- - !ruby/object:Gem::Version
38
- version: 1.72.1
35
+ version: '1.89'
39
36
  type: :runtime
40
37
  prerelease: false
41
38
  version_requirements: !ruby/object:Gem::Requirement
42
39
  requirements:
43
40
  - - "~>"
44
41
  - !ruby/object:Gem::Version
45
- version: '1.72'
46
- - - ">="
47
- - !ruby/object:Gem::Version
48
- version: 1.72.1
42
+ version: '1.89'
49
43
  - !ruby/object:Gem::Dependency
50
44
  name: rubocop-rspec
51
45
  requirement: !ruby/object:Gem::Requirement
@@ -75,13 +69,16 @@ files:
75
69
  - README.md
76
70
  - config/default.yml
77
71
  - lib/rubocop-rspec_rails.rb
72
+ - lib/rubocop/cop/rspec_rails.rb
78
73
  - lib/rubocop/cop/rspec_rails/avoid_setup_hook.rb
74
+ - lib/rubocop/cop/rspec_rails/file_fixture.rb
79
75
  - lib/rubocop/cop/rspec_rails/have_http_status.rb
80
76
  - lib/rubocop/cop/rspec_rails/http_status.rb
81
77
  - lib/rubocop/cop/rspec_rails/http_status_name_consistency.rb
82
78
  - lib/rubocop/cop/rspec_rails/inferred_spec_type.rb
83
79
  - lib/rubocop/cop/rspec_rails/minitest_assertions.rb
84
80
  - lib/rubocop/cop/rspec_rails/negation_be_valid.rb
81
+ - lib/rubocop/cop/rspec_rails/receive_perform_later.rb
85
82
  - lib/rubocop/cop/rspec_rails/travel_around.rb
86
83
  - lib/rubocop/cop/rspec_rails_cops.rb
87
84
  - lib/rubocop/rspec_rails/config_formatter.rb
@@ -111,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
108
  - !ruby/object:Gem::Version
112
109
  version: '0'
113
110
  requirements: []
114
- rubygems_version: 3.6.9
111
+ rubygems_version: 4.0.20
115
112
  specification_version: 4
116
113
  summary: Code style checking for RSpec Rails files
117
114
  test_files: []