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 +4 -4
- data/CHANGELOG.md +14 -0
- data/config/default.yml +18 -1
- data/lib/rubocop/cop/rspec/be_empty.rb +1 -0
- data/lib/rubocop/cop/rspec/change_by_zero.rb +27 -1
- data/lib/rubocop/cop/rspec/described_class.rb +3 -2
- data/lib/rubocop/cop/rspec/example_without_description.rb +11 -2
- data/lib/rubocop/cop/rspec/expect_actual.rb +5 -2
- data/lib/rubocop/cop/rspec/expect_output.rb +1 -4
- data/lib/rubocop/cop/rspec/is_expected_specify.rb +45 -0
- data/lib/rubocop/cop/rspec/message_expectation.rb +0 -1
- data/lib/rubocop/cop/rspec/message_spies.rb +0 -2
- data/lib/rubocop/cop/rspec/multiple_expectations.rb +10 -5
- data/lib/rubocop/cop/rspec/rails/have_http_status.rb +33 -9
- data/lib/rubocop/cop/rspec/rails/minitest_assertions.rb +314 -22
- data/lib/rubocop/cop/rspec/redundant_predicate_matcher.rb +4 -2
- data/lib/rubocop/cop/rspec/repeated_subject_call.rb +120 -0
- data/lib/rubocop/cop/rspec/single_argument_message_chain.rb +1 -2
- data/lib/rubocop/cop/rspec_cops.rb +2 -0
- data/lib/rubocop/rspec/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66ac0fecdd858c4b85a06c9e32bdb680d2dd92aa98100740302702a1737a3a9c
|
4
|
+
data.tar.gz: 5f688cf15390149d3a7dc6e2d3f67e041248ee43a3dce5e47d2370d2813f86d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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
|
-
|
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)
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
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
|
-
|
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
|
@@ -48,12 +48,17 @@ module RuboCop
|
|
48
48
|
# end
|
49
49
|
# end
|
50
50
|
#
|
51
|
-
# @example
|
52
|
-
# #
|
53
|
-
#
|
54
|
-
#
|
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
|
-
#
|
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(
|
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
|
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?
|
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
|
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,
|
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,
|
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
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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(
|
54
|
-
format(MSG, 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
|
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
|
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
|
@@ -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'
|
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.
|
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-
|
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.
|
256
|
+
rubygems_version: 3.5.3
|
255
257
|
signing_key:
|
256
258
|
specification_version: 4
|
257
259
|
summary: Code style checking for RSpec files
|