rubocop-rspec 2.22.0 → 2.23.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: e786386e218833ac7f78af9ffcadadb6bf6c822a31b455cac1567152fd3bc9c0
4
- data.tar.gz: 384170b4ae3a06c05b30855ae9361057c53226b40b924d4ec1d49c0d15195658
3
+ metadata.gz: e0eebbfb772c1598b0ee4cfe88c74d8b81171e197dbd4d5dc6020df95005440e
4
+ data.tar.gz: 2936be502e0062b94a0346bd98696194fd33fe6f7693f8c92bc38a952ee31b26
5
5
  SHA512:
6
- metadata.gz: 554345811f404b27a39fd522e0085022c39147e0487f04ea77ed7887002451af112e2ddc45ad94841c0a4201a76f37fbfb7a2e64aed3c7efa91cddf1a30843bc
7
- data.tar.gz: 22882a56ddf03c55d6eaf299fb05197c835049054231e6d7798e2ca3eac5cbd9840eeada823a12431b00efd37289a8353792dfe3c8f653b2606f9ccda480615a
6
+ metadata.gz: 14e3e53093b7e104ab212217fdc8226a909fba9bdd84fa44c22963cf8e62d17ce858a2180b8b5b3727b5dd2a74a9b4c6e2139d8d61cbc6595b2c46f9022214af
7
+ data.tar.gz: 9408c50d35ae50e1825fef43f2c6a6def0d87e44bfbbd968bb19996b1a867f215df36ab62d451af892e6855c4330c0d4fb9d09b8400ff5f35b4ecef1cfb86adb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.23.0 (2023-07-30)
6
+
7
+ - Add new `RSpec/Rails/NegationBeValid` cop. ([@ydah])
8
+ - Fix a false negative for `RSpec/ExcessiveDocstringSpacing` when finds description with em space. ([@ydah])
9
+ - Fix a false positive for `RSpec/EmptyExampleGroup` when example group with examples defined in `if` branch inside iterator. ([@ydah])
10
+ - Update the message output of `RSpec/ExpectActual` to include the word 'value'. ([@corydiamand])
11
+ - Fix a false negative for `RSpec/Pending` when `it` without body. ([@ydah])
12
+ - Add new `RSpec/ReceiveMessages` cop. ([@ydah])
13
+ - Change default.yml path to use `**/spec/*` instead of `spec/*`. ([@ydah])
14
+ - Add `AllowedIdentifiers` and `AllowedPatterns` configuration option to `RSpec/IndexedLet`. ([@ydah])
15
+ - Fix `RSpec/NamedSubject` when block has no body. ([@splattael])
16
+ - Fix `RSpec/LetBeforeExamples` autocorrect incompatible with `RSpec/ScatteredLet` autocorrect. ([@ydah])
17
+ - Update `RSpec/Focus` to support `shared_context` and `shared_examples` ([@tmaier])
18
+
5
19
  ## 2.22.0 (2023-05-06)
6
20
 
7
21
  - Extract factory_bot cops to a separate repository, [`rubocop-factory_bot`](https://github.com/rubocop/rubocop-factory_bot). The `rubocop-factory_bot` repository is a dependency of `rubocop-rspec` and the factory_bot cops are aliased (`RSpec/FactoryBot/Foo` == `FactoryBot/Foo`) until v3.0 is released, so the change will be invisible to users until then. ([@ydah])
@@ -781,6 +795,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
781
795
  [@cfabianski]: https://github.com/cfabianski
782
796
  [@clupprich]: https://github.com/clupprich
783
797
  [@composerinteralia]: https://github.com/composerinteralia
798
+ [@corydiamand]: https://github.com/corydiamand
784
799
  [@darhazer]: https://github.com/Darhazer
785
800
  [@daveworth]: https://github.com/daveworth
786
801
  [@dduugg]: https://github.com/dduugg
@@ -859,6 +874,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
859
874
  [@seanpdoyle]: https://github.com/seanpdoyle
860
875
  [@sl4vr]: https://github.com/sl4vr
861
876
  [@smcgivern]: https://github.com/smcgivern
877
+ [@splattael]: https://github.com/splattael
862
878
  [@stephannv]: https://github.com/stephannv
863
879
  [@t3h2mas]: https://github.com/t3h2mas
864
880
  [@tdeo]: https://github.com/tdeo
@@ -866,6 +882,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
866
882
  [@telmofcosta]: https://github.com/telmofcosta
867
883
  [@tietew]: https://github.com/Tietew
868
884
  [@timrogers]: https://github.com/timrogers
885
+ [@tmaier]: https://github.com/tmaier
869
886
  [@topalovic]: https://github.com/topalovic
870
887
  [@twalpole]: https://github.com/twalpole
871
888
  [@vzvu3k6k]: https://github.com/vzvu3k6k
data/README.md CHANGED
@@ -17,7 +17,7 @@ gem install rubocop-rspec
17
17
 
18
18
  or if you use bundler put this in your `Gemfile`
19
19
 
20
- ```
20
+ ```ruby
21
21
  gem 'rubocop-rspec', require: false
22
22
  ```
23
23
 
data/config/default.yml CHANGED
@@ -181,10 +181,11 @@ RSpec/BeforeAfterAll:
181
181
  Description: Check that before/after(:all) isn't being used.
182
182
  Enabled: true
183
183
  Exclude:
184
- - spec/spec_helper.rb
185
- - spec/rails_helper.rb
186
- - spec/support/**/*.rb
184
+ - "**/spec/spec_helper.rb"
185
+ - "**/spec/rails_helper.rb"
186
+ - "**/spec/support/**/*.rb"
187
187
  VersionAdded: '1.12'
188
+ VersionChanged: '2.23'
188
189
  StyleGuide: https://rspec.rubystyle.guide/#avoid-hooks-with-context-scope
189
190
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/BeforeAfterAll
190
191
 
@@ -404,8 +405,9 @@ RSpec/ExpectActual:
404
405
  Description: Checks for `expect(...)` calls containing literal values.
405
406
  Enabled: true
406
407
  Exclude:
407
- - spec/routing/**/*
408
+ - "**/spec/routing/**/*"
408
409
  VersionAdded: '1.7'
410
+ VersionChanged: '2.23'
409
411
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectActual
410
412
 
411
413
  RSpec/ExpectChange:
@@ -513,8 +515,11 @@ RSpec/IndexedLet:
513
515
  Description: Do not set up test data using indexes (e.g., `item_1`, `item_2`).
514
516
  Enabled: pending
515
517
  VersionAdded: '2.20'
518
+ VersionChanged: '2.23'
516
519
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/IndexedLet
517
520
  Max: 1
521
+ AllowedIdentifiers: []
522
+ AllowedPatterns: []
518
523
 
519
524
  RSpec/InstanceSpy:
520
525
  Description: Checks for `instance_double` used with `have_received`.
@@ -725,6 +730,12 @@ RSpec/ReceiveCounts:
725
730
  VersionAdded: '1.26'
726
731
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveCounts
727
732
 
733
+ RSpec/ReceiveMessages:
734
+ Description: Checks for multiple messages stubbed on the same object.
735
+ Enabled: pending
736
+ VersionAdded: '2.23'
737
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveMessages
738
+
728
739
  RSpec/ReceiveNever:
729
740
  Description: Prefer `not_to receive(...)` over `receive(...).never`.
730
741
  Enabled: true
@@ -974,11 +985,11 @@ RSpec/FactoryBot/AttributeDefinedStatically:
974
985
  Description: Always declare attribute values as blocks.
975
986
  Enabled: true
976
987
  Include:
977
- - spec/factories.rb
978
- - spec/factories/**/*.rb
979
- - features/support/factories/**/*.rb
988
+ - "**/spec/factories.rb"
989
+ - "**/spec/factories/**/*.rb"
990
+ - "**/features/support/factories/**/*.rb"
980
991
  VersionAdded: '1.28'
981
- VersionChanged: '2.0'
992
+ VersionChanged: '2.23'
982
993
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically
983
994
 
984
995
  RSpec/FactoryBot/ConsistentParenthesesStyle:
@@ -997,26 +1008,26 @@ RSpec/FactoryBot/CreateList:
997
1008
  Include:
998
1009
  - "**/*_spec.rb"
999
1010
  - "**/spec/**/*"
1000
- - spec/factories.rb
1001
- - spec/factories/**/*.rb
1002
- - features/support/factories/**/*.rb
1011
+ - "**/spec/factories.rb"
1012
+ - "**/spec/factories/**/*.rb"
1013
+ - "**/features/support/factories/**/*.rb"
1003
1014
  EnforcedStyle: create_list
1004
1015
  SupportedStyles:
1005
1016
  - create_list
1006
1017
  - n_times
1007
1018
  VersionAdded: '1.25'
1008
- VersionChanged: '2.0'
1019
+ VersionChanged: '2.23'
1009
1020
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/CreateList
1010
1021
 
1011
1022
  RSpec/FactoryBot/FactoryClassName:
1012
1023
  Description: Use string value when setting the class attribute explicitly.
1013
1024
  Enabled: true
1014
1025
  Include:
1015
- - spec/factories.rb
1016
- - spec/factories/**/*.rb
1017
- - features/support/factories/**/*.rb
1026
+ - "**/spec/factories.rb"
1027
+ - "**/spec/factories/**/*.rb"
1028
+ - "**/features/support/factories/**/*.rb"
1018
1029
  VersionAdded: '1.37'
1019
- VersionChanged: '2.0'
1030
+ VersionChanged: '2.23'
1020
1031
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/FactoryClassName
1021
1032
 
1022
1033
  RSpec/FactoryBot/FactoryNameStyle:
@@ -1095,6 +1106,16 @@ RSpec/Rails/MinitestAssertions:
1095
1106
  VersionAdded: '2.17'
1096
1107
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/MinitestAssertions
1097
1108
 
1109
+ RSpec/Rails/NegationBeValid:
1110
+ Description: Enforces use of `be_invalid` or `not_to` for negated be_valid.
1111
+ EnforcedStyle: not_to
1112
+ SupportedStyles:
1113
+ - not_to
1114
+ - be_invalid
1115
+ Enabled: pending
1116
+ VersionAdded: '2.23'
1117
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/NegationBeValid
1118
+
1098
1119
  RSpec/Rails/TravelAround:
1099
1120
  Description: Prefer to travel in `before` rather than `around`.
1100
1121
  Enabled: pending
@@ -131,6 +131,7 @@ module RuboCop
131
131
  {
132
132
  #examples_directly_or_in_block?
133
133
  (begin <#examples_directly_or_in_block? ...>)
134
+ (begin <#examples_in_branches? ...>)
134
135
  }
135
136
  PATTERN
136
137
 
@@ -169,6 +170,8 @@ module RuboCop
169
170
  end
170
171
 
171
172
  def examples_in_branches?(condition_node)
173
+ return if !condition_node.if_type? && !condition_node.case_type?
174
+
172
175
  condition_node.branches.any? { |branch| examples?(branch) }
173
176
  end
174
177
 
@@ -52,14 +52,21 @@ module RuboCop
52
52
 
53
53
  # @param text [String]
54
54
  def excessive_whitespace?(text)
55
- return true if text.start_with?(' ') || text.end_with?(' ')
56
-
57
- text.match?(/[^\n ] +[^ ]/)
55
+ text.match?(/
56
+ # Leading space
57
+ \A[[:blank:]]
58
+ |
59
+ # Trailing space
60
+ [[:blank:]]\z
61
+ |
62
+ # Two or more consecutive spaces, except if they are leading spaces
63
+ [^[[:space:]]][[:blank:]]{2,}[^[[:blank:]]]
64
+ /x)
58
65
  end
59
66
 
60
67
  # @param text [String]
61
68
  def strip_excessive_whitespace(text)
62
- text.strip.gsub(/ +/, ' ')
69
+ text.strip.gsub(/[[:blank:]]{2,}/, ' ')
63
70
  end
64
71
 
65
72
  # @param node [RuboCop::AST::Node]
@@ -24,7 +24,7 @@ module RuboCop
24
24
  class ExpectActual < Base
25
25
  extend AutoCorrector
26
26
 
27
- MSG = 'Provide the actual you are testing to `expect(...)`.'
27
+ MSG = 'Provide the actual value you are testing to `expect(...)`.'
28
28
 
29
29
  RESTRICT_ON_SEND = Runners.all
30
30
 
@@ -77,7 +77,7 @@ module RuboCop
77
77
 
78
78
  private
79
79
 
80
- # This is not implement using a NodePattern because it seems
80
+ # This is not implemented using a NodePattern because it seems
81
81
  # to not be able to match against an explicit (nil) sexp
82
82
  def literal?(node)
83
83
  node && (simple_literal?(node) || complex_literal?(node))
@@ -34,6 +34,18 @@ module RuboCop
34
34
  # # good
35
35
  # describe 'test' do; end
36
36
  #
37
+ # # bad
38
+ # shared_examples 'test', focus: true do; end
39
+ #
40
+ # # good
41
+ # shared_examples 'test' do; end
42
+ #
43
+ # # bad
44
+ # shared_context 'test', focus: true do; end
45
+ #
46
+ # # good
47
+ # shared_context 'test' do; end
48
+ #
37
49
  # # bad (does not support autocorrection)
38
50
  # focus 'test' do; end
39
51
  #
@@ -51,6 +63,7 @@ module RuboCop
51
63
  #Examples.regular
52
64
  #Examples.skipped
53
65
  #Examples.pending
66
+ #SharedGroups.all
54
67
  }
55
68
  PATTERN
56
69
 
@@ -8,6 +8,9 @@ module RuboCop
8
8
  # It makes reading the test harder because it's not clear what exactly
9
9
  # is tested by this particular example.
10
10
  #
11
+ # The configurable options `AllowedIdentifiers` and `AllowedPatterns`
12
+ # will also read those set in `Naming/VariableNumber`.
13
+ #
11
14
  # @example `Max: 1 (default)`
12
15
  # # bad
13
16
  # let(:item_1) { create(:item) }
@@ -31,7 +34,20 @@ module RuboCop
31
34
  # let(:item_1) { create(:item) }
32
35
  # let(:item_2) { create(:item) }
33
36
  #
37
+ # @example `AllowedIdentifiers: ['item_1', 'item_2']`
38
+ # # good
39
+ # let(:item_1) { create(:item) }
40
+ # let(:item_2) { create(:item) }
41
+ #
42
+ # @example `AllowedPatterns: ['item']`
43
+ # # good
44
+ # let(:item_1) { create(:item) }
45
+ # let(:item_2) { create(:item) }
46
+ #
34
47
  class IndexedLet < Base
48
+ include AllowedIdentifiers
49
+ include AllowedPattern
50
+
35
51
  MSG = 'This `let` statement uses index in its name. Please give it ' \
36
52
  'a meaningful name.'
37
53
 
@@ -69,12 +85,27 @@ module RuboCop
69
85
  end
70
86
 
71
87
  def indexed_let?(node)
72
- let?(node) && SUFFIX_INDEX_REGEX.match?(let_name(node))
88
+ let?(node) &&
89
+ SUFFIX_INDEX_REGEX.match?(let_name(node)) &&
90
+ !allowed_identifier?(let_name(node).to_s) &&
91
+ !matches_allowed_pattern?(let_name(node).to_s)
73
92
  end
74
93
 
75
94
  def let_name_stripped_index(node)
76
95
  let_name(node).to_s.gsub(INDEX_REGEX, '')
77
96
  end
97
+
98
+ def cop_config_patterns_values
99
+ Array(config.for_cop('Naming/VariableNumber')
100
+ .fetch('AllowedPatterns', [])) +
101
+ Array(cop_config.fetch('AllowedPatterns', []))
102
+ end
103
+
104
+ def allowed_identifiers
105
+ Array(config.for_cop('Naming/VariableNumber')
106
+ .fetch('AllowedIdentifiers', [])) +
107
+ Array(cop_config.fetch('AllowedIdentifiers', []))
108
+ end
78
109
  end
79
110
  end
80
111
  end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  class InstanceVariable < Base
49
49
  include TopLevelGroup
50
50
 
51
- MSG = 'Avoid instance variables use let, ' \
51
+ MSG = 'Avoid instance variables - use let, ' \
52
52
  'a method call, or a local variable (if possible).'
53
53
 
54
54
  # @!method dynamic_class?(node)
@@ -17,7 +17,7 @@ module RuboCop
17
17
  # Anonymous classes are fine, since they don't result in global
18
18
  # namespace name clashes.
19
19
  #
20
- # @see https://relishapp.com/rspec/rspec-mocks/docs/mutating-constants
20
+ # @see https://rspec.info/features/3-12/rspec-mocks/mutating-constants
21
21
  #
22
22
  # @example Constants leak between examples
23
23
  # # bad
@@ -51,6 +51,10 @@ module RuboCop
51
51
  }
52
52
  PATTERN
53
53
 
54
+ def self.autocorrect_incompatible_with
55
+ [RSpec::ScatteredLet]
56
+ end
57
+
54
58
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
55
59
  return unless example_group_with_body?(node)
56
60
 
@@ -145,7 +145,7 @@ module RuboCop
145
145
  end
146
146
 
147
147
  def find_subject(block_node)
148
- block_node.body.child_nodes.find { |send_node| subject?(send_node) }
148
+ block_node.body&.child_nodes&.find { |send_node| subject?(send_node) }
149
149
  end
150
150
  end
151
151
  end
@@ -41,10 +41,15 @@ module RuboCop
41
41
  def_node_matcher :skippable?, <<~PATTERN
42
42
  {
43
43
  (send #rspec? #ExampleGroups.regular ...)
44
- (send nil? #Examples.regular ...)
44
+ #skippable_example?
45
45
  }
46
46
  PATTERN
47
47
 
48
+ # @!method skippable_example?(node)
49
+ def_node_matcher :skippable_example?, <<~PATTERN
50
+ (send nil? #Examples.regular ...)
51
+ PATTERN
52
+
48
53
  # @!method pending_block?(node)
49
54
  def_node_matcher :pending_block?, <<~PATTERN
50
55
  {
@@ -62,7 +67,12 @@ module RuboCop
62
67
  private
63
68
 
64
69
  def skipped?(node)
65
- skippable?(node) && skipped_in_metadata?(node)
70
+ skippable?(node) && skipped_in_metadata?(node) ||
71
+ skipped_regular_example_without_body?(node)
72
+ end
73
+
74
+ def skipped_regular_example_without_body?(node)
75
+ skippable_example?(node) && !node.block_node
66
76
  end
67
77
  end
68
78
  end
@@ -74,7 +74,7 @@ module RuboCop
74
74
  name[0..-2]
75
75
  when 'exist?', 'exists?'
76
76
  'exist'
77
- when /^has_/
77
+ when /\Ahas_/
78
78
  name.sub('has_', 'have_')[0..-2]
79
79
  else
80
80
  "be_#{name[0..-2]}"
@@ -240,10 +240,10 @@ module RuboCop
240
240
  'include?'
241
241
  when 'respond_to'
242
242
  'respond_to?'
243
- when /^have_(.+)/
243
+ when /\Ahave_(.+)/
244
244
  "has_#{Regexp.last_match(1)}?"
245
245
  else
246
- "#{matcher[/^be_(.+)/, 1]}?"
246
+ "#{matcher[/\Abe_(.+)/, 1]}?"
247
247
  end
248
248
  end
249
249
  # rubocop:enable Metrics/MethodLength
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ module Rails
7
+ # Enforces use of `be_invalid` or `not_to` for negated be_valid.
8
+ #
9
+ # @example EnforcedStyle: not_to (default)
10
+ # # bad
11
+ # expect(foo).to be_invalid
12
+ #
13
+ # # good
14
+ # expect(foo).not_to be_valid
15
+ #
16
+ # @example EnforcedStyle: be_invalid
17
+ # # bad
18
+ # expect(foo).not_to be_valid
19
+ #
20
+ # # good
21
+ # expect(foo).to be_invalid
22
+ #
23
+ class NegationBeValid < Base
24
+ extend AutoCorrector
25
+ include ConfigurableEnforcedStyle
26
+
27
+ MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
28
+ RESTRICT_ON_SEND = %i[be_valid be_invalid].freeze
29
+
30
+ # @!method not_to?(node)
31
+ def_node_matcher :not_to?, <<~PATTERN
32
+ (send ... :not_to (send nil? :be_valid ...))
33
+ PATTERN
34
+
35
+ # @!method be_invalid?(node)
36
+ def_node_matcher :be_invalid?, <<~PATTERN
37
+ (send ... :to (send nil? :be_invalid ...))
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ return unless offense?(node.parent)
42
+
43
+ add_offense(offense_range(node),
44
+ message: message(node.method_name)) do |corrector|
45
+ corrector.replace(node.parent.loc.selector, replaced_runner)
46
+ corrector.replace(node.loc.selector, replaced_matcher)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def offense?(node)
53
+ case style
54
+ when :not_to
55
+ be_invalid?(node)
56
+ when :be_invalid
57
+ not_to?(node)
58
+ end
59
+ end
60
+
61
+ def offense_range(node)
62
+ node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
63
+ end
64
+
65
+ def message(_matcher)
66
+ format(MSG,
67
+ runner: replaced_runner,
68
+ matcher: replaced_matcher)
69
+ end
70
+
71
+ def replaced_runner
72
+ case style
73
+ when :not_to
74
+ 'not_to'
75
+ when :be_invalid
76
+ 'to'
77
+ end
78
+ end
79
+
80
+ def replaced_matcher
81
+ case style
82
+ when :not_to
83
+ 'be_valid'
84
+ when :be_invalid
85
+ 'be_invalid'
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for multiple messages stubbed on the same object.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # before do
11
+ # allow(Service).to receive(:foo).and_return(bar)
12
+ # allow(Service).to receive(:baz).and_return(qux)
13
+ # end
14
+ #
15
+ # # good
16
+ # before do
17
+ # allow(Service).to receive_messages(foo: bar, baz: qux)
18
+ # end
19
+ #
20
+ # # good - ignore same message
21
+ # before do
22
+ # allow(Service).to receive(:foo).and_return(bar)
23
+ # allow(Service).to receive(:foo).and_return(qux)
24
+ # end
25
+ #
26
+ class ReceiveMessages < Base
27
+ extend AutoCorrector
28
+ include RangeHelp
29
+
30
+ MSG = 'Use `receive_messages` instead of multiple stubs on lines ' \
31
+ '%<loc>s.'
32
+
33
+ # @!method allow_receive_message?(node)
34
+ def_node_matcher :allow_receive_message?, <<~PATTERN
35
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym _)) :and_return !#heredoc?))
36
+ PATTERN
37
+
38
+ # @!method allow_argument(node)
39
+ def_node_matcher :allow_argument, <<~PATTERN
40
+ (send (send nil? :allow $_ ...) ...)
41
+ PATTERN
42
+
43
+ # @!method receive_node(node)
44
+ def_node_search :receive_node, <<~PATTERN
45
+ $(send (send nil? :receive ...) ...)
46
+ PATTERN
47
+
48
+ # @!method receive_arg(node)
49
+ def_node_search :receive_arg, <<~PATTERN
50
+ (send (send nil? :receive $_) ...)
51
+ PATTERN
52
+
53
+ # @!method receive_and_return_argument(node)
54
+ def_node_matcher :receive_and_return_argument, <<~PATTERN
55
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym $_)) :and_return $_))
56
+ PATTERN
57
+
58
+ def on_begin(node)
59
+ repeated_receive_message(node).each do |item, repeated_lines, args|
60
+ next if repeated_lines.empty?
61
+
62
+ register_offense(item, repeated_lines, args)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def repeated_receive_message(node)
69
+ node
70
+ .children
71
+ .select { |child| allow_receive_message?(child) }
72
+ .group_by { |child| allow_argument(child) }
73
+ .values
74
+ .reject(&:one?)
75
+ .flat_map { |items| add_repeated_lines_and_arguments(items) }
76
+ end
77
+
78
+ def add_repeated_lines_and_arguments(items)
79
+ uniq_items = uniq_items(items)
80
+ repeated_lines = uniq_items.map(&:first_line)
81
+ uniq_items.map do |item|
82
+ [item, repeated_lines - [item.first_line], arguments(uniq_items)]
83
+ end
84
+ end
85
+
86
+ def uniq_items(items)
87
+ items.select do |item|
88
+ items.none? do |i|
89
+ receive_arg(item).first == receive_arg(i).first &&
90
+ !same_line?(item, i)
91
+ end
92
+ end
93
+ end
94
+
95
+ def arguments(items)
96
+ items.map do |item|
97
+ receive_and_return_argument(item) do |receive_arg, return_arg|
98
+ "#{normalize_receive_arg(receive_arg)}: " \
99
+ "#{normalize_return_arg(return_arg)}"
100
+ end
101
+ end
102
+ end
103
+
104
+ def normalize_receive_arg(receive_arg)
105
+ if requires_quotes?(receive_arg)
106
+ "'#{receive_arg}'"
107
+ else
108
+ receive_arg
109
+ end
110
+ end
111
+
112
+ def normalize_return_arg(return_arg)
113
+ if return_arg.hash_type? && !return_arg.braces?
114
+ "{ #{return_arg.source} }"
115
+ else
116
+ return_arg.source
117
+ end
118
+ end
119
+
120
+ def register_offense(item, repeated_lines, args)
121
+ add_offense(item, message: message(repeated_lines)) do |corrector|
122
+ if item.loc.line < repeated_lines.min
123
+ replace_to_receive_messages(corrector, item, args)
124
+ else
125
+ corrector.remove(item_range_by_whole_lines(item))
126
+ end
127
+ end
128
+ end
129
+
130
+ def message(repeated_lines)
131
+ format(MSG, loc: repeated_lines)
132
+ end
133
+
134
+ def replace_to_receive_messages(corrector, item, args)
135
+ receive_node(item) do |node|
136
+ corrector.replace(node,
137
+ "receive_messages(#{args.join(', ')})")
138
+ end
139
+ end
140
+
141
+ def item_range_by_whole_lines(item)
142
+ range_by_whole_lines(item.source_range, include_final_newline: true)
143
+ end
144
+
145
+ def heredoc?(node)
146
+ (node.str_type? || node.dstr_type?) && node.heredoc?
147
+ end
148
+
149
+ def requires_quotes?(value)
150
+ value.match?(/^:".*?"|=$/)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -7,7 +7,7 @@ module RuboCop
7
7
  #
8
8
  # Only investigates references that are one of the supported styles.
9
9
  #
10
- # @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
10
+ # @see https://rspec.info/features/3-12/rspec-mocks/verifying-doubles
11
11
  #
12
12
  # This cop can be configured in your configuration using the
13
13
  # `EnforcedStyle` option and supports `--auto-gen-config`.
@@ -5,7 +5,7 @@ module RuboCop
5
5
  module RSpec
6
6
  # Prefer using verifying doubles over normal doubles.
7
7
  #
8
- # @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
8
+ # @see https://rspec.info/features/3-12/rspec-mocks/verifying-doubles
9
9
  #
10
10
  # @example
11
11
  # # bad
@@ -18,6 +18,7 @@ require_relative 'rspec/factory_bot/syntax_methods'
18
18
 
19
19
  require_relative 'rspec/rails/avoid_setup_hook'
20
20
  require_relative 'rspec/rails/have_http_status'
21
+ require_relative 'rspec/rails/negation_be_valid'
21
22
  begin
22
23
  require_relative 'rspec/rails/http_status'
23
24
  rescue LoadError
@@ -99,6 +100,7 @@ require_relative 'rspec/pending'
99
100
  require_relative 'rspec/pending_without_reason'
100
101
  require_relative 'rspec/predicate_matcher'
101
102
  require_relative 'rspec/receive_counts'
103
+ require_relative 'rspec/receive_messages'
102
104
  require_relative 'rspec/receive_never'
103
105
  require_relative 'rspec/redundant_around'
104
106
  require_relative 'rspec/repeated_description'
@@ -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.22.0'
7
+ STRING = '2.23.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.22.0
4
+ version: 2.23.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: 2023-05-06 00:00:00.000000000 Z
13
+ date: 2023-07-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rubocop
@@ -175,8 +175,10 @@ files:
175
175
  - lib/rubocop/cop/rspec/rails/http_status.rb
176
176
  - lib/rubocop/cop/rspec/rails/inferred_spec_type.rb
177
177
  - lib/rubocop/cop/rspec/rails/minitest_assertions.rb
178
+ - lib/rubocop/cop/rspec/rails/negation_be_valid.rb
178
179
  - lib/rubocop/cop/rspec/rails/travel_around.rb
179
180
  - lib/rubocop/cop/rspec/receive_counts.rb
181
+ - lib/rubocop/cop/rspec/receive_messages.rb
180
182
  - lib/rubocop/cop/rspec/receive_never.rb
181
183
  - lib/rubocop/cop/rspec/redundant_around.rb
182
184
  - lib/rubocop/cop/rspec/repeated_description.rb