rubocop-rspec 2.26.1 → 2.27.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bf3d2ebea2c15361963f61079a377b069b01ed6ebb4979b0229471b38866098
4
- data.tar.gz: 1910cd808996e04802802dd138e297c98191fd9e8a5bf0c35644ebf589e7bf2e
3
+ metadata.gz: 66ac0fecdd858c4b85a06c9e32bdb680d2dd92aa98100740302702a1737a3a9c
4
+ data.tar.gz: 5f688cf15390149d3a7dc6e2d3f67e041248ee43a3dce5e47d2370d2813f86d7
5
5
  SHA512:
6
- metadata.gz: 1b94923f79321f29b16c763f995a36c547068a8f69b52e1a5dbe99999f38052f609743fd7132aa672aea7a57726a135d99c171406774a0cc05c30114602dd3d7
7
- data.tar.gz: 0fffcfe3e0f089c416bf6fa61dfd89217ccfe7203f0c7bbf4185a30d94a08a7146848ffb592f374f0aa100067d8ca7eec80986310069b9d6c9b16f73c2f72e29
6
+ metadata.gz: 9fc186d4d8191c461dee977e64a2f0a295389be99a8054bb7ba75258e429c5c3dc624b3d6da0a19b65a024648f1922ef9646af5bac44d0b7c3255a99ac8e14bc
7
+ data.tar.gz: 1d603f51d43de587cd17939252604f1d86d371e0e6883a4289c219eed97d0862bd4ba29d2c3ba205ceed3c4157692d4fbbf52b63d020facfd0d2798bef284e55
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.27.0 (2024-03-01)
6
+
7
+ - Add new `RSpec/IsExpectedSpecify` cop. ([@ydah])
8
+ - Add new `RSpec/RepeatedSubjectCall` cop. ([@drcapulet])
9
+ - Add support for `assert_true`, `assert_false`, `assert_not_equal`, `assert_not_nil`, `*_empty`, `*_predicate`, `*_kind_of`, `*_in_delta`, `*_match`, `*_instance_of` and `*_includes` assertions in `RSpec/Rails/MinitestAssertions`. ([@ydah], [@G-Rath])
10
+ - Support asserts with messages in `Rspec/BeEmpty`. ([@G-Rath])
11
+ - Fix a false positive for `RSpec/ExpectActual` when used with rspec-rails routing matchers. ([@naveg])
12
+ - Add configuration option `ResponseMethods` to `RSpec/Rails/HaveHttpStatus`. ([@ydah])
13
+ - Fix a false negative for `RSpec/DescribedClass` when class with constant. ([@ydah])
14
+ - Fix a false positive for `RSpec/ExampleWithoutDescription` when `specify` with multi-line block and missing description. ([@ydah])
15
+ - Fix an incorrect autocorrect for `RSpec/ChangeByZero` when compound expectations with line break before `.by(0)`. ([@ydah])
16
+
5
17
  ## 2.26.1 (2024-01-05)
6
18
 
7
19
  - Fix an error for `RSpec/SharedExamples` when using examples without argument. ([@ydah])
@@ -845,6 +857,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
845
857
  [@deivid-rodriguez]: https://github.com/deivid-rodriguez
846
858
  [@dgollahon]: https://github.com/dgollahon
847
859
  [@dmitrytsepelev]: https://github.com/dmitrytsepelev
860
+ [@drcapulet]: https://github.com/drcapulet
848
861
  [@drowze]: https://github.com/Drowze
849
862
  [@dswij]: https://github.com/dswij
850
863
  [@dvandersluis]: https://github.com/dvandersluis
@@ -892,6 +905,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
892
905
  [@mockdeep]: https://github.com/mockdeep
893
906
  [@mothonmars]: https://github.com/MothOnMars
894
907
  [@mvz]: https://github.com/mvz
908
+ [@naveg]: https://github.com/naveg
895
909
  [@nc-holodakg]: https://github.com/nc-holodakg
896
910
  [@nevir]: https://github.com/nevir
897
911
  [@ngouy]: https://github.com/ngouy
data/config/default.yml CHANGED
@@ -548,6 +548,13 @@ RSpec/InstanceVariable:
548
548
  StyleGuide: https://rspec.rubystyle.guide/#instance-variables
549
549
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/InstanceVariable
550
550
 
551
+ RSpec/IsExpectedSpecify:
552
+ Description: Check for `specify` with `is_expected` and one-liner expectations.
553
+ Enabled: pending
554
+ VersionAdded: '2.27'
555
+ StyleGuide: https://rspec.rubystyle.guide/#it-and-specify
556
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/IsExpectedSpecify
557
+
551
558
  RSpec/ItBehavesLike:
552
559
  Description: Checks that only one `it_behaves_like` style is used.
553
560
  Enabled: true
@@ -813,6 +820,12 @@ RSpec/RepeatedIncludeExample:
813
820
  VersionAdded: '1.44'
814
821
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedIncludeExample
815
822
 
823
+ RSpec/RepeatedSubjectCall:
824
+ Description: Checks for repeated calls to subject missing that it is memoized.
825
+ Enabled: pending
826
+ VersionAdded: '2.27'
827
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedSubjectCall
828
+
816
829
  RSpec/ReturnFromStub:
817
830
  Description: Checks for consistent style of stub's return setting.
818
831
  Enabled: true
@@ -1126,8 +1139,12 @@ RSpec/Rails/AvoidSetupHook:
1126
1139
  RSpec/Rails/HaveHttpStatus:
1127
1140
  Description: Checks that tests use `have_http_status` instead of equality matchers.
1128
1141
  Enabled: pending
1142
+ ResponseMethods:
1143
+ - response
1144
+ - last_response
1129
1145
  SafeAutoCorrect: false
1130
1146
  VersionAdded: '2.12'
1147
+ VersionChanged: '2.27'
1131
1148
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/HaveHttpStatus
1132
1149
 
1133
1150
  RSpec/Rails/HttpStatus:
@@ -1166,7 +1183,7 @@ RSpec/Rails/InferredSpecType:
1166
1183
  views: view
1167
1184
 
1168
1185
  RSpec/Rails/MinitestAssertions:
1169
- Description: Check if using Minitest matchers.
1186
+ Description: Check if using Minitest-like matchers.
1170
1187
  Enabled: pending
1171
1188
  VersionAdded: '2.17'
1172
1189
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/MinitestAssertions
@@ -28,6 +28,7 @@ module RuboCop
28
28
  (send nil? :match_array (array))
29
29
  (send nil? :contain_exactly)
30
30
  }
31
+ _?
31
32
  )
32
33
  PATTERN
33
34
 
@@ -59,6 +59,8 @@ module RuboCop
59
59
  #
60
60
  class ChangeByZero < Base
61
61
  extend AutoCorrector
62
+ include RangeHelp
63
+
62
64
  MSG = 'Prefer `not_to change` over `to %<method>s.by(0)`.'
63
65
  MSG_COMPOUND = 'Prefer %<preferred>s with compound expectations ' \
64
66
  'over `%<method>s.by(0)`.'
@@ -140,8 +142,32 @@ module RuboCop
140
142
 
141
143
  change_nodes(node) do |change_node|
142
144
  corrector.replace(change_node.loc.selector, negated_matcher)
143
- range = node.loc.dot.with(end_pos: node.source_range.end_pos)
145
+ insert_operator(corrector, node, change_node)
146
+ remove_by_zero(corrector, node, change_node)
147
+ end
148
+ end
149
+
150
+ def insert_operator(corrector, node, change_node)
151
+ operator = node.right_siblings.first
152
+ return unless %i[& |].include?(operator)
153
+
154
+ corrector.insert_after(
155
+ replace_node(node, change_node), " #{operator}"
156
+ )
157
+ end
158
+
159
+ def replace_node(node, change_node)
160
+ expect_change_with_arguments(node) ? change_node : change_node.parent
161
+ end
162
+
163
+ def remove_by_zero(corrector, node, change_node)
164
+ range = node.loc.dot.with(end_pos: node.source_range.end_pos)
165
+ if change_node.loc.line == range.line
144
166
  corrector.remove(range)
167
+ else
168
+ corrector.remove(
169
+ range_by_whole_lines(range, include_final_newline: true)
170
+ )
145
171
  end
146
172
  end
147
173
 
@@ -113,7 +113,7 @@ module RuboCop
113
113
  def find_usage(node, &block)
114
114
  yield(node) if offensive?(node)
115
115
 
116
- return if scope_change?(node) || node.const_type?
116
+ return if scope_change?(node)
117
117
 
118
118
  node.each_child_node do |child|
119
119
  find_usage(child, &block)
@@ -194,7 +194,8 @@ module RuboCop
194
194
  # const_name(s(:const, s(:const, nil, :M), :C)) # => [:M, :C]
195
195
  # const_name(s(:const, s(:cbase), :C)) # => [nil, :C]
196
196
  def const_name(node)
197
- namespace, name = *node # rubocop:disable InternalAffairs/NodeDestructuring
197
+ namespace = node.namespace
198
+ name = node.short_name
198
199
  if !namespace
199
200
  [name]
200
201
  elsif namespace.const_type?
@@ -7,6 +7,7 @@ module RuboCop
7
7
  #
8
8
  # RSpec allows for auto-generated example descriptions when there is no
9
9
  # description provided or the description is an empty one.
10
+ # It is acceptable to use `specify` without a description
10
11
  #
11
12
  # This cop removes empty descriptions.
12
13
  # It also defines whether auto-generated description is allowed, based
@@ -14,17 +15,24 @@ module RuboCop
14
15
  #
15
16
  # This cop can be configured using the `EnforcedStyle` option
16
17
  #
18
+ # @example
19
+ # # always good
20
+ # specify do
21
+ # result = service.call
22
+ # expect(result).to be(true)
23
+ # end
24
+ #
17
25
  # @example `EnforcedStyle: always_allow` (default)
18
26
  # # bad
19
27
  # it('') { is_expected.to be_good }
20
- # it '' do
28
+ # specify '' do
21
29
  # result = service.call
22
30
  # expect(result).to be(true)
23
31
  # end
24
32
  #
25
33
  # # good
26
34
  # it { is_expected.to be_good }
27
- # it do
35
+ # specify do
28
36
  # result = service.call
29
37
  # expect(result).to be(true)
30
38
  # end
@@ -75,6 +83,7 @@ module RuboCop
75
83
  def check_example_without_description(node)
76
84
  return if node.arguments?
77
85
  return unless disallow_empty_description?(node)
86
+ return if node.method?(:specify) && node.parent.multiline?
78
87
 
79
88
  add_offense(node, message: MSG_ADD_DESCRIPTION)
80
89
  end
@@ -50,7 +50,8 @@ module RuboCop
50
50
  regexp
51
51
  ].freeze
52
52
 
53
- SUPPORTED_MATCHERS = %i[eq eql equal be].freeze
53
+ SKIPPED_MATCHERS = %i[route_to be_routable].freeze
54
+ CORRECTABLE_MATCHERS = %i[eq eql equal be].freeze
54
55
 
55
56
  # @!method expect_literal(node)
56
57
  def_node_matcher :expect_literal, <<~PATTERN
@@ -66,8 +67,10 @@ module RuboCop
66
67
 
67
68
  def on_send(node)
68
69
  expect_literal(node) do |actual, matcher, expected|
70
+ next if SKIPPED_MATCHERS.include?(matcher)
71
+
69
72
  add_offense(actual.source_range) do |corrector|
70
- next unless SUPPORTED_MATCHERS.include?(matcher)
73
+ next unless CORRECTABLE_MATCHERS.include?(matcher)
71
74
  next if literal?(expected)
72
75
 
73
76
  swap(corrector, actual, expected)
@@ -22,10 +22,7 @@ module RuboCop
22
22
  def on_gvasgn(node)
23
23
  return unless inside_example_scope?(node)
24
24
 
25
- # rubocop:disable InternalAffairs/NodeDestructuring
26
- variable_name, _rhs = *node
27
- # rubocop:enable InternalAffairs/NodeDestructuring
28
- name = variable_name[1..]
25
+ name = node.name[1..]
29
26
  return unless name.eql?('stdout') || name.eql?('stderr')
30
27
 
31
28
  add_offense(node.loc.name, message: format(MSG, name: name))
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Check for `specify` with `is_expected` and one-liner expectations.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # specify { is_expected.to be_truthy }
11
+ #
12
+ # # good
13
+ # it { is_expected.to be_truthy }
14
+ #
15
+ # # good
16
+ # specify do
17
+ # # ...
18
+ # end
19
+ # specify { expect(sqrt(4)).to eq(2) }
20
+ #
21
+ class IsExpectedSpecify < Base
22
+ extend AutoCorrector
23
+
24
+ RESTRICT_ON_SEND = %i[specify].freeze
25
+ IS_EXPECTED_METHODS = ::Set[:is_expected, :are_expected].freeze
26
+ MSG = 'Use `it` instead of `specify`.'
27
+
28
+ # @!method offense?(node)
29
+ def_node_matcher :offense?, <<~PATTERN
30
+ (block (send _ :specify) _ (send (send _ IS_EXPECTED_METHODS) ...))
31
+ PATTERN
32
+
33
+ def on_send(node)
34
+ block_node = node.parent
35
+ return unless block_node&.single_line? && offense?(block_node)
36
+
37
+ selector = node.loc.selector
38
+ add_offense(selector) do |corrector|
39
+ corrector.replace(selector, 'it')
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -29,7 +29,6 @@ module RuboCop
29
29
 
30
30
  MSG = 'Prefer `%<style>s` for setting message expectations.'
31
31
 
32
- SUPPORTED_STYLES = %w[allow expect].freeze
33
32
  RESTRICT_ON_SEND = %i[to].freeze
34
33
 
35
34
  # @!method message_expectation(node)
@@ -39,8 +39,6 @@ module RuboCop
39
39
  'expectations. Setup `%<source>s` as a spy using ' \
40
40
  '`allow` or `instance_spy`.'
41
41
 
42
- SUPPORTED_STYLES = %w[have_received receive].freeze
43
-
44
42
  RESTRICT_ON_SEND = Runners.all
45
43
 
46
44
  # @!method message_expectation(node)
@@ -48,12 +48,17 @@ module RuboCop
48
48
  # end
49
49
  # end
50
50
  #
51
- # @example configuration
52
- # # .rubocop.yml
53
- # # RSpec/MultipleExpectations:
54
- # # Max: 2
51
+ # @example `Max: 1` (default)
52
+ # # bad
53
+ # describe UserCreator do
54
+ # it 'builds a user' do
55
+ # expect(user.name).to eq("John")
56
+ # expect(user.age).to eq(22)
57
+ # end
58
+ # end
55
59
  #
56
- # # not flagged by rubocop
60
+ # @example `Max: 2`
61
+ # # good
57
62
  # describe UserCreator do
58
63
  # it 'builds a user' do
59
64
  # expect(user.name).to eq("John")
@@ -6,20 +6,32 @@ module RuboCop
6
6
  module Rails
7
7
  # Checks that tests use `have_http_status` instead of equality matchers.
8
8
  #
9
- # @example
9
+ # @example ResponseMethods: ['response', 'last_response'] (default)
10
10
  # # bad
11
11
  # expect(response.status).to be(200)
12
- # expect(response.code).to eq("200")
12
+ # expect(last_response.code).to eq("200")
13
13
  #
14
14
  # # good
15
15
  # expect(response).to have_http_status(200)
16
+ # expect(last_response).to have_http_status(200)
17
+ #
18
+ # @example ResponseMethods: ['foo_response']
19
+ # # bad
20
+ # expect(foo_response.status).to be(200)
21
+ #
22
+ # # good
23
+ # expect(foo_response).to have_http_status(200)
24
+ #
25
+ # # also good
26
+ # expect(response).to have_http_status(200)
27
+ # expect(last_response).to have_http_status(200)
16
28
  #
17
29
  class HaveHttpStatus < ::RuboCop::Cop::Base
18
30
  extend AutoCorrector
19
31
 
20
32
  MSG =
21
- 'Prefer `expect(response).%<to>s have_http_status(%<status>s)` ' \
22
- 'over `%<bad_code>s`.'
33
+ 'Prefer `expect(%<response>s).%<to>s ' \
34
+ 'have_http_status(%<status>s)` over `%<bad_code>s`.'
23
35
 
24
36
  RUNNERS = %i[to to_not not_to].to_set
25
37
  RESTRICT_ON_SEND = RUNNERS
@@ -28,26 +40,38 @@ module RuboCop
28
40
  def_node_matcher :match_status, <<~PATTERN
29
41
  (send
30
42
  (send nil? :expect
31
- $(send (send nil? :response) {:status :code})
43
+ $(send $(send nil? #response_methods?) {:status :code})
32
44
  )
33
45
  $RUNNERS
34
46
  $(send nil? {:be :eq :eql :equal} ({int str} $_))
35
47
  )
36
48
  PATTERN
37
49
 
38
- def on_send(node)
39
- match_status(node) do |response_status, to, match, status|
50
+ def on_send(node) # rubocop:disable Metrics/MethodLength
51
+ match_status(node) do
52
+ |response_status, response_method, to, match, status|
40
53
  return unless status.to_s.match?(/\A\d+\z/)
41
54
 
42
- message = format(MSG, to: to, status: status,
55
+ message = format(MSG, response: response_method.method_name,
56
+ to: to, status: status,
43
57
  bad_code: node.source)
44
58
  add_offense(node, message: message) do |corrector|
45
- corrector.replace(response_status, 'response')
59
+ corrector.replace(response_status, response_method.method_name)
46
60
  corrector.replace(match.loc.selector, 'have_http_status')
47
61
  corrector.replace(match.first_argument, status.to_s)
48
62
  end
49
63
  end
50
64
  end
65
+
66
+ private
67
+
68
+ def response_methods?(name)
69
+ response_methods.include?(name.to_s)
70
+ end
71
+
72
+ def response_methods
73
+ cop_config.fetch('ResponseMethods', [])
74
+ end
51
75
  end
52
76
  end
53
77
  end
@@ -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
@@ -9,10 +9,12 @@ module RuboCop
9
9
  # # bad
10
10
  # expect(foo).to be_exist(bar)
11
11
  # expect(foo).not_to be_include(bar)
12
+ # expect(foo).to be_all(bar)
12
13
  #
13
14
  # # good
14
15
  # expect(foo).to exist(bar)
15
16
  # expect(foo).not_to include(bar)
17
+ # expect(foo).to all be(bar)
16
18
  #
17
19
  class RedundantPredicateMatcher < Base
18
20
  extend AutoCorrector
@@ -25,7 +27,7 @@ module RuboCop
25
27
 
26
28
  def on_send(node)
27
29
  return if node.parent.block_type? || node.arguments.empty?
28
- return unless replacable_arguments?(node)
30
+ return unless replaceable_arguments?(node)
29
31
 
30
32
  method_name = node.method_name.to_s
31
33
  replaced = replaced_method_name(method_name)
@@ -43,7 +45,7 @@ module RuboCop
43
45
  format(MSG, bad: bad_method, good: good_method)
44
46
  end
45
47
 
46
- def replacable_arguments?(node)
48
+ def replaceable_arguments?(node)
47
49
  if node.method?(:be_all)
48
50
  node.first_argument.send_type?
49
51
  else
@@ -0,0 +1,120 @@
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
+ class RepeatedSubjectCall < Base
27
+ include TopLevelGroup
28
+
29
+ MSG = 'Calls to subject are memoized, this block is misleading'
30
+
31
+ # @!method subject?(node)
32
+ # Find a named or unnamed subject definition
33
+ #
34
+ # @example anonymous subject
35
+ # subject?(parse('subject { foo }').ast) do |name|
36
+ # name # => :subject
37
+ # end
38
+ #
39
+ # @example named subject
40
+ # subject?(parse('subject(:thing) { foo }').ast) do |name|
41
+ # name # => :thing
42
+ # end
43
+ #
44
+ # @param node [RuboCop::AST::Node]
45
+ #
46
+ # @yield [Symbol] subject name
47
+ def_node_matcher :subject?, <<-PATTERN
48
+ (block
49
+ (send nil?
50
+ { #Subjects.all (sym $_) | $#Subjects.all }
51
+ ) args ...)
52
+ PATTERN
53
+
54
+ # @!method subject_calls(node, method_name)
55
+ def_node_search :subject_calls, <<~PATTERN
56
+ (send nil? %)
57
+ PATTERN
58
+
59
+ def on_top_level_group(node)
60
+ @subjects_by_node = detect_subjects_in_scope(node)
61
+
62
+ detect_offenses_in_block(node)
63
+ end
64
+
65
+ private
66
+
67
+ def detect_offense(example_node, subject_node)
68
+ walker = subject_node
69
+
70
+ while walker.parent? && walker.parent != example_node.body
71
+ walker = walker.parent
72
+
73
+ if walker.block_type? && walker.method?(:expect)
74
+ add_offense(walker)
75
+ return
76
+ end
77
+ end
78
+ end
79
+
80
+ def detect_offenses_in_block(node, subject_names = [])
81
+ subject_names = [*subject_names, *@subjects_by_node[node]]
82
+
83
+ if example?(node)
84
+ return detect_offenses_in_example(node, subject_names)
85
+ end
86
+
87
+ node.each_child_node(:send, :def, :block, :begin) do |child|
88
+ detect_offenses_in_block(child, subject_names)
89
+ end
90
+ end
91
+
92
+ def detect_offenses_in_example(node, subject_names)
93
+ return unless node.body
94
+
95
+ subjects_used = Hash.new(false)
96
+
97
+ subject_calls(node.body, Set[*subject_names, :subject]).each do |call|
98
+ if subjects_used[call.method_name]
99
+ detect_offense(node, call)
100
+ else
101
+ subjects_used[call.method_name] = true
102
+ end
103
+ end
104
+ end
105
+
106
+ def detect_subjects_in_scope(node)
107
+ node.each_descendant(:block).with_object({}) do |child, h|
108
+ subject?(child) do |name|
109
+ outer_example_group = child.each_ancestor(:block).find do |a|
110
+ example_group?(a)
111
+ end
112
+
113
+ (h[outer_example_group] ||= []) << name
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -81,8 +81,7 @@ module RuboCop
81
81
  end
82
82
 
83
83
  def key_to_arg(node)
84
- key, = *node # rubocop:disable InternalAffairs/NodeDestructuring
85
- node.sym_type? ? ":#{key}" : node.source
84
+ node.sym_type? ? ":#{node.value}" : node.source
86
85
  end
87
86
 
88
87
  def replacement(method)
@@ -78,6 +78,7 @@ require_relative 'rspec/implicit_subject'
78
78
  require_relative 'rspec/indexed_let'
79
79
  require_relative 'rspec/instance_spy'
80
80
  require_relative 'rspec/instance_variable'
81
+ require_relative 'rspec/is_expected_specify'
81
82
  require_relative 'rspec/it_behaves_like'
82
83
  require_relative 'rspec/iterated_expectation'
83
84
  require_relative 'rspec/leading_subject'
@@ -113,6 +114,7 @@ require_relative 'rspec/repeated_example'
113
114
  require_relative 'rspec/repeated_example_group_body'
114
115
  require_relative 'rspec/repeated_example_group_description'
115
116
  require_relative 'rspec/repeated_include_example'
117
+ require_relative 'rspec/repeated_subject_call'
116
118
  require_relative 'rspec/return_from_stub'
117
119
  require_relative 'rspec/scattered_let'
118
120
  require_relative 'rspec/scattered_setup'
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module RSpec
5
5
  # Version information for the RSpec RuboCop plugin.
6
6
  module Version
7
- STRING = '2.26.1'
7
+ STRING = '2.27.0'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.26.1
4
+ version: 2.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Backus
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-01-05 00:00:00.000000000 Z
13
+ date: 2024-02-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rubocop
@@ -139,6 +139,7 @@ files:
139
139
  - lib/rubocop/cop/rspec/indexed_let.rb
140
140
  - lib/rubocop/cop/rspec/instance_spy.rb
141
141
  - lib/rubocop/cop/rspec/instance_variable.rb
142
+ - lib/rubocop/cop/rspec/is_expected_specify.rb
142
143
  - lib/rubocop/cop/rspec/it_behaves_like.rb
143
144
  - lib/rubocop/cop/rspec/iterated_expectation.rb
144
145
  - lib/rubocop/cop/rspec/leading_subject.rb
@@ -192,6 +193,7 @@ files:
192
193
  - lib/rubocop/cop/rspec/repeated_example_group_body.rb
193
194
  - lib/rubocop/cop/rspec/repeated_example_group_description.rb
194
195
  - lib/rubocop/cop/rspec/repeated_include_example.rb
196
+ - lib/rubocop/cop/rspec/repeated_subject_call.rb
195
197
  - lib/rubocop/cop/rspec/return_from_stub.rb
196
198
  - lib/rubocop/cop/rspec/scattered_let.rb
197
199
  - lib/rubocop/cop/rspec/scattered_setup.rb
@@ -251,7 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
253
  - !ruby/object:Gem::Version
252
254
  version: '0'
253
255
  requirements: []
254
- rubygems_version: 3.4.21
256
+ rubygems_version: 3.5.3
255
257
  signing_key:
256
258
  specification_version: 4
257
259
  summary: Code style checking for RSpec files