rubocop-rspec 2.22.0 → 2.23.1

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: 2d8e118064ce45e0670b72b711b2e680f511f645997f86c9d22dcf9f0eb564d7
4
+ data.tar.gz: 8a0a510f390617e4e83e99bdbec1bc0146eb06b1b61c51aada05721154a6be43
5
5
  SHA512:
6
- metadata.gz: 554345811f404b27a39fd522e0085022c39147e0487f04ea77ed7887002451af112e2ddc45ad94841c0a4201a76f37fbfb7a2e64aed3c7efa91cddf1a30843bc
7
- data.tar.gz: 22882a56ddf03c55d6eaf299fb05197c835049054231e6d7798e2ca3eac5cbd9840eeada823a12431b00efd37289a8353792dfe3c8f653b2606f9ccda480615a
6
+ metadata.gz: 4a1f937488b43fe96496ec87d1a6fcc1d53385248937de31d1fb4a7923dd98a2769d225e23b7e8d5bd66c7c6e2c38fb5b3652c2792087f00999464a7161561fb
7
+ data.tar.gz: 9b2fed4ef2b93e9ac867133aaa11efdfd66cea642416cd82cc2ca322875154c6a3f174991f1a2c49cf762cfcf9c0aa17635348ce5e8ff8c300103b8c727d33c1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.23.1 (2023-08-07)
6
+
7
+ - Mark to `Safe: false` for `RSpec/Rails/NegationBeValid` cop. ([@ydah])
8
+ - Declare autocorrect as unsafe for `RSpec/ReceiveMessages`. ([@bquorning])
9
+
10
+ ## 2.23.0 (2023-07-30)
11
+
12
+ - Add new `RSpec/Rails/NegationBeValid` cop. ([@ydah])
13
+ - Fix a false negative for `RSpec/ExcessiveDocstringSpacing` when finds description with em space. ([@ydah])
14
+ - Fix a false positive for `RSpec/EmptyExampleGroup` when example group with examples defined in `if` branch inside iterator. ([@ydah])
15
+ - Update the message output of `RSpec/ExpectActual` to include the word 'value'. ([@corydiamand])
16
+ - Fix a false negative for `RSpec/Pending` when `it` without body. ([@ydah])
17
+ - Add new `RSpec/ReceiveMessages` cop. ([@ydah])
18
+ - Change default.yml path to use `**/spec/*` instead of `spec/*`. ([@ydah])
19
+ - Add `AllowedIdentifiers` and `AllowedPatterns` configuration option to `RSpec/IndexedLet`. ([@ydah])
20
+ - Fix `RSpec/NamedSubject` when block has no body. ([@splattael])
21
+ - Fix `RSpec/LetBeforeExamples` autocorrect incompatible with `RSpec/ScatteredLet` autocorrect. ([@ydah])
22
+ - Update `RSpec/Focus` to support `shared_context` and `shared_examples` ([@tmaier])
23
+
5
24
  ## 2.22.0 (2023-05-06)
6
25
 
7
26
  - 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 +800,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
781
800
  [@cfabianski]: https://github.com/cfabianski
782
801
  [@clupprich]: https://github.com/clupprich
783
802
  [@composerinteralia]: https://github.com/composerinteralia
803
+ [@corydiamand]: https://github.com/corydiamand
784
804
  [@darhazer]: https://github.com/Darhazer
785
805
  [@daveworth]: https://github.com/daveworth
786
806
  [@dduugg]: https://github.com/dduugg
@@ -859,6 +879,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
859
879
  [@seanpdoyle]: https://github.com/seanpdoyle
860
880
  [@sl4vr]: https://github.com/sl4vr
861
881
  [@smcgivern]: https://github.com/smcgivern
882
+ [@splattael]: https://github.com/splattael
862
883
  [@stephannv]: https://github.com/stephannv
863
884
  [@t3h2mas]: https://github.com/t3h2mas
864
885
  [@tdeo]: https://github.com/tdeo
@@ -866,6 +887,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
866
887
  [@telmofcosta]: https://github.com/telmofcosta
867
888
  [@tietew]: https://github.com/Tietew
868
889
  [@timrogers]: https://github.com/timrogers
890
+ [@tmaier]: https://github.com/tmaier
869
891
  [@topalovic]: https://github.com/topalovic
870
892
  [@twalpole]: https://github.com/twalpole
871
893
  [@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,13 @@ 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
+ SafeAutoCorrect: false
737
+ VersionAdded: '2.23'
738
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveMessages
739
+
728
740
  RSpec/ReceiveNever:
729
741
  Description: Prefer `not_to receive(...)` over `receive(...).never`.
730
742
  Enabled: true
@@ -974,11 +986,11 @@ RSpec/FactoryBot/AttributeDefinedStatically:
974
986
  Description: Always declare attribute values as blocks.
975
987
  Enabled: true
976
988
  Include:
977
- - spec/factories.rb
978
- - spec/factories/**/*.rb
979
- - features/support/factories/**/*.rb
989
+ - "**/spec/factories.rb"
990
+ - "**/spec/factories/**/*.rb"
991
+ - "**/features/support/factories/**/*.rb"
980
992
  VersionAdded: '1.28'
981
- VersionChanged: '2.0'
993
+ VersionChanged: '2.23'
982
994
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically
983
995
 
984
996
  RSpec/FactoryBot/ConsistentParenthesesStyle:
@@ -997,26 +1009,26 @@ RSpec/FactoryBot/CreateList:
997
1009
  Include:
998
1010
  - "**/*_spec.rb"
999
1011
  - "**/spec/**/*"
1000
- - spec/factories.rb
1001
- - spec/factories/**/*.rb
1002
- - features/support/factories/**/*.rb
1012
+ - "**/spec/factories.rb"
1013
+ - "**/spec/factories/**/*.rb"
1014
+ - "**/features/support/factories/**/*.rb"
1003
1015
  EnforcedStyle: create_list
1004
1016
  SupportedStyles:
1005
1017
  - create_list
1006
1018
  - n_times
1007
1019
  VersionAdded: '1.25'
1008
- VersionChanged: '2.0'
1020
+ VersionChanged: '2.23'
1009
1021
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/CreateList
1010
1022
 
1011
1023
  RSpec/FactoryBot/FactoryClassName:
1012
1024
  Description: Use string value when setting the class attribute explicitly.
1013
1025
  Enabled: true
1014
1026
  Include:
1015
- - spec/factories.rb
1016
- - spec/factories/**/*.rb
1017
- - features/support/factories/**/*.rb
1027
+ - "**/spec/factories.rb"
1028
+ - "**/spec/factories/**/*.rb"
1029
+ - "**/features/support/factories/**/*.rb"
1018
1030
  VersionAdded: '1.37'
1019
- VersionChanged: '2.0'
1031
+ VersionChanged: '2.23'
1020
1032
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/FactoryClassName
1021
1033
 
1022
1034
  RSpec/FactoryBot/FactoryNameStyle:
@@ -1095,6 +1107,17 @@ RSpec/Rails/MinitestAssertions:
1095
1107
  VersionAdded: '2.17'
1096
1108
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/MinitestAssertions
1097
1109
 
1110
+ RSpec/Rails/NegationBeValid:
1111
+ Description: Enforces use of `be_invalid` or `not_to` for negated be_valid.
1112
+ Safe: false
1113
+ EnforcedStyle: not_to
1114
+ SupportedStyles:
1115
+ - not_to
1116
+ - be_invalid
1117
+ Enabled: pending
1118
+ VersionAdded: '2.23'
1119
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/NegationBeValid
1120
+
1098
1121
  RSpec/Rails/TravelAround:
1099
1122
  Description: Prefer to travel in `before` rather than `around`.
1100
1123
  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,96 @@
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
+ # @safety
10
+ # This cop is unsafe because it cannot guarantee that
11
+ # the test target is an instance of `ActiveModel::Validations``.
12
+ #
13
+ # @example EnforcedStyle: not_to (default)
14
+ # # bad
15
+ # expect(foo).to be_invalid
16
+ #
17
+ # # good
18
+ # expect(foo).not_to be_valid
19
+ #
20
+ # @example EnforcedStyle: be_invalid
21
+ # # bad
22
+ # expect(foo).not_to be_valid
23
+ #
24
+ # # good
25
+ # expect(foo).to be_invalid
26
+ #
27
+ class NegationBeValid < Base
28
+ extend AutoCorrector
29
+ include ConfigurableEnforcedStyle
30
+
31
+ MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
32
+ RESTRICT_ON_SEND = %i[be_valid be_invalid].freeze
33
+
34
+ # @!method not_to?(node)
35
+ def_node_matcher :not_to?, <<~PATTERN
36
+ (send ... :not_to (send nil? :be_valid ...))
37
+ PATTERN
38
+
39
+ # @!method be_invalid?(node)
40
+ def_node_matcher :be_invalid?, <<~PATTERN
41
+ (send ... :to (send nil? :be_invalid ...))
42
+ PATTERN
43
+
44
+ def on_send(node)
45
+ return unless offense?(node.parent)
46
+
47
+ add_offense(offense_range(node),
48
+ message: message(node.method_name)) do |corrector|
49
+ corrector.replace(node.parent.loc.selector, replaced_runner)
50
+ corrector.replace(node.loc.selector, replaced_matcher)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def offense?(node)
57
+ case style
58
+ when :not_to
59
+ be_invalid?(node)
60
+ when :be_invalid
61
+ not_to?(node)
62
+ end
63
+ end
64
+
65
+ def offense_range(node)
66
+ node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
67
+ end
68
+
69
+ def message(_matcher)
70
+ format(MSG,
71
+ runner: replaced_runner,
72
+ matcher: replaced_matcher)
73
+ end
74
+
75
+ def replaced_runner
76
+ case style
77
+ when :not_to
78
+ 'not_to'
79
+ when :be_invalid
80
+ 'to'
81
+ end
82
+ end
83
+
84
+ def replaced_matcher
85
+ case style
86
+ when :not_to
87
+ 'be_valid'
88
+ when :be_invalid
89
+ 'be_invalid'
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,160 @@
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
+ # @safety
9
+ # The autocorrection is marked as unsafe, because it may change the
10
+ # order of stubs. This in turn may cause e.g. variables to be called
11
+ # before they are defined.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # before do
16
+ # allow(Service).to receive(:foo).and_return(bar)
17
+ # allow(Service).to receive(:baz).and_return(qux)
18
+ # end
19
+ #
20
+ # # good
21
+ # before do
22
+ # allow(Service).to receive_messages(foo: bar, baz: qux)
23
+ # end
24
+ #
25
+ # # good - ignore same message
26
+ # before do
27
+ # allow(Service).to receive(:foo).and_return(bar)
28
+ # allow(Service).to receive(:foo).and_return(qux)
29
+ # end
30
+ #
31
+ class ReceiveMessages < Base
32
+ extend AutoCorrector
33
+ include RangeHelp
34
+
35
+ MSG = 'Use `receive_messages` instead of multiple stubs on lines ' \
36
+ '%<loc>s.'
37
+
38
+ # @!method allow_receive_message?(node)
39
+ def_node_matcher :allow_receive_message?, <<~PATTERN
40
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym _)) :and_return !#heredoc?))
41
+ PATTERN
42
+
43
+ # @!method allow_argument(node)
44
+ def_node_matcher :allow_argument, <<~PATTERN
45
+ (send (send nil? :allow $_ ...) ...)
46
+ PATTERN
47
+
48
+ # @!method receive_node(node)
49
+ def_node_search :receive_node, <<~PATTERN
50
+ $(send (send nil? :receive ...) ...)
51
+ PATTERN
52
+
53
+ # @!method receive_arg(node)
54
+ def_node_search :receive_arg, <<~PATTERN
55
+ (send (send nil? :receive $_) ...)
56
+ PATTERN
57
+
58
+ # @!method receive_and_return_argument(node)
59
+ def_node_matcher :receive_and_return_argument, <<~PATTERN
60
+ (send (send nil? :allow ...) :to (send (send nil? :receive (sym $_)) :and_return $_))
61
+ PATTERN
62
+
63
+ def on_begin(node)
64
+ repeated_receive_message(node).each do |item, repeated_lines, args|
65
+ next if repeated_lines.empty?
66
+
67
+ register_offense(item, repeated_lines, args)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def repeated_receive_message(node)
74
+ node
75
+ .children
76
+ .select { |child| allow_receive_message?(child) }
77
+ .group_by { |child| allow_argument(child) }
78
+ .values
79
+ .reject(&:one?)
80
+ .flat_map { |items| add_repeated_lines_and_arguments(items) }
81
+ end
82
+
83
+ def add_repeated_lines_and_arguments(items)
84
+ uniq_items = uniq_items(items)
85
+ repeated_lines = uniq_items.map(&:first_line)
86
+ uniq_items.map do |item|
87
+ [item, repeated_lines - [item.first_line], arguments(uniq_items)]
88
+ end
89
+ end
90
+
91
+ def uniq_items(items)
92
+ items.select do |item|
93
+ items.none? do |i|
94
+ receive_arg(item).first == receive_arg(i).first &&
95
+ !same_line?(item, i)
96
+ end
97
+ end
98
+ end
99
+
100
+ def arguments(items)
101
+ items.map do |item|
102
+ receive_and_return_argument(item) do |receive_arg, return_arg|
103
+ "#{normalize_receive_arg(receive_arg)}: " \
104
+ "#{normalize_return_arg(return_arg)}"
105
+ end
106
+ end
107
+ end
108
+
109
+ def normalize_receive_arg(receive_arg)
110
+ if requires_quotes?(receive_arg)
111
+ "'#{receive_arg}'"
112
+ else
113
+ receive_arg
114
+ end
115
+ end
116
+
117
+ def normalize_return_arg(return_arg)
118
+ if return_arg.hash_type? && !return_arg.braces?
119
+ "{ #{return_arg.source} }"
120
+ else
121
+ return_arg.source
122
+ end
123
+ end
124
+
125
+ def register_offense(item, repeated_lines, args)
126
+ add_offense(item, message: message(repeated_lines)) do |corrector|
127
+ if item.loc.line < repeated_lines.min
128
+ replace_to_receive_messages(corrector, item, args)
129
+ else
130
+ corrector.remove(item_range_by_whole_lines(item))
131
+ end
132
+ end
133
+ end
134
+
135
+ def message(repeated_lines)
136
+ format(MSG, loc: repeated_lines)
137
+ end
138
+
139
+ def replace_to_receive_messages(corrector, item, args)
140
+ receive_node(item) do |node|
141
+ corrector.replace(node,
142
+ "receive_messages(#{args.join(', ')})")
143
+ end
144
+ end
145
+
146
+ def item_range_by_whole_lines(item)
147
+ range_by_whole_lines(item.source_range, include_final_newline: true)
148
+ end
149
+
150
+ def heredoc?(node)
151
+ (node.str_type? || node.dstr_type?) && node.heredoc?
152
+ end
153
+
154
+ def requires_quotes?(value)
155
+ value.match?(/^:".*?"|=$/)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ 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.1'
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.1
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-08-07 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