rubocop-rspec 2.26.0 → 2.27.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: 5f2e44d1764d0cea711adf5ea346e895d6d6ff805de65da2a85ea842a046fce0
4
- data.tar.gz: 382cbaeabb62b1fb6e11298d99800c0db8de3350eeec154194d8a018ac74f1ac
3
+ metadata.gz: 66ac0fecdd858c4b85a06c9e32bdb680d2dd92aa98100740302702a1737a3a9c
4
+ data.tar.gz: 5f688cf15390149d3a7dc6e2d3f67e041248ee43a3dce5e47d2370d2813f86d7
5
5
  SHA512:
6
- metadata.gz: ef7d2b26f8cee852c069f9d2f847c6041ae083fcb4a4940adde810687c8745a612d9abe7445ee3a8548b2047b7d13ac050f8589821f478d0443939ba38830901
7
- data.tar.gz: ef690c9604c7d21cf95f997112052af66190569945a2065d8ac299f5b3b1aa960130c8f03a337482646983ec0b7632390074837723db23bd21047c51a1984ae8
6
+ metadata.gz: 9fc186d4d8191c461dee977e64a2f0a295389be99a8054bb7ba75258e429c5c3dc624b3d6da0a19b65a024648f1922ef9646af5bac44d0b7c3255a99ac8e14bc
7
+ data.tar.gz: 1d603f51d43de587cd17939252604f1d86d371e0e6883a4289c219eed97d0862bd4ba29d2c3ba205ceed3c4157692d4fbbf52b63d020facfd0d2798bef284e55
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
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
+
17
+ ## 2.26.1 (2024-01-05)
18
+
19
+ - Fix an error for `RSpec/SharedExamples` when using examples without argument. ([@ydah])
20
+
5
21
  ## 2.26.0 (2024-01-04)
6
22
 
7
23
  - Add new `RSpec/RedundantPredicateMatcher` cop. ([@ydah])
@@ -841,6 +857,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
841
857
  [@deivid-rodriguez]: https://github.com/deivid-rodriguez
842
858
  [@dgollahon]: https://github.com/dgollahon
843
859
  [@dmitrytsepelev]: https://github.com/dmitrytsepelev
860
+ [@drcapulet]: https://github.com/drcapulet
844
861
  [@drowze]: https://github.com/Drowze
845
862
  [@dswij]: https://github.com/dswij
846
863
  [@dvandersluis]: https://github.com/dvandersluis
@@ -888,6 +905,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
888
905
  [@mockdeep]: https://github.com/mockdeep
889
906
  [@mothonmars]: https://github.com/MothOnMars
890
907
  [@mvz]: https://github.com/mvz
908
+ [@naveg]: https://github.com/naveg
891
909
  [@nc-holodakg]: https://github.com/nc-holodakg
892
910
  [@nevir]: https://github.com/nevir
893
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
@@ -46,14 +46,13 @@ module RuboCop
46
46
  # @!method shared_examples(node)
47
47
  def_node_matcher :shared_examples, <<~PATTERN
48
48
  {
49
- (send #rspec? #SharedGroups.all ...)
50
- (send nil? #Includes.all ...)
49
+ (send #rspec? #SharedGroups.all $_ ...)
50
+ (send nil? #Includes.all $_ ...)
51
51
  }
52
52
  PATTERN
53
53
 
54
54
  def on_send(node)
55
- shared_examples(node) do
56
- ast_node = node.first_argument
55
+ shared_examples(node) do |ast_node|
57
56
  next unless offense?(ast_node)
58
57
 
59
58
  checker = new_checker(ast_node)
@@ -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.0'
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.0
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-04 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