rubocop-rspec 3.9.0 → 3.10.2

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/config/default.yml +18 -1
  4. data/lib/rubocop/cop/rspec/around_block.rb +22 -0
  5. data/lib/rubocop/cop/rspec/contain_exactly.rb +8 -22
  6. data/lib/rubocop/cop/rspec/context_method.rb +1 -1
  7. data/lib/rubocop/cop/rspec/context_wording.rb +1 -1
  8. data/lib/rubocop/cop/rspec/described_class.rb +1 -1
  9. data/lib/rubocop/cop/rspec/discarded_matcher.rb +113 -0
  10. data/lib/rubocop/cop/rspec/empty_example_group.rb +3 -2
  11. data/lib/rubocop/cop/rspec/empty_hook.rb +1 -1
  12. data/lib/rubocop/cop/rspec/empty_line_after_example.rb +1 -1
  13. data/lib/rubocop/cop/rspec/empty_line_after_example_group.rb +1 -1
  14. data/lib/rubocop/cop/rspec/empty_line_after_final_let.rb +7 -2
  15. data/lib/rubocop/cop/rspec/empty_line_after_hook.rb +1 -0
  16. data/lib/rubocop/cop/rspec/empty_line_after_subject.rb +1 -1
  17. data/lib/rubocop/cop/rspec/example_length.rb +1 -1
  18. data/lib/rubocop/cop/rspec/example_without_description.rb +1 -1
  19. data/lib/rubocop/cop/rspec/example_wording.rb +1 -1
  20. data/lib/rubocop/cop/rspec/expect_actual.rb +33 -13
  21. data/lib/rubocop/cop/rspec/expect_change.rb +46 -16
  22. data/lib/rubocop/cop/rspec/expect_in_hook.rb +1 -0
  23. data/lib/rubocop/cop/rspec/expect_in_let.rb +1 -1
  24. data/lib/rubocop/cop/rspec/hook_argument.rb +1 -0
  25. data/lib/rubocop/cop/rspec/hooks_before_examples.rb +1 -0
  26. data/lib/rubocop/cop/rspec/indexed_let.rb +1 -1
  27. data/lib/rubocop/cop/rspec/instance_spy.rb +1 -1
  28. data/lib/rubocop/cop/rspec/iterated_expectation.rb +13 -0
  29. data/lib/rubocop/cop/rspec/leading_subject.rb +1 -1
  30. data/lib/rubocop/cop/rspec/let_before_examples.rb +1 -1
  31. data/lib/rubocop/cop/rspec/let_setup.rb +1 -1
  32. data/lib/rubocop/cop/rspec/match_with_simple_regex.rb +92 -0
  33. data/lib/rubocop/cop/rspec/missing_example_group_argument.rb +1 -1
  34. data/lib/rubocop/cop/rspec/mixin/inside_example.rb +16 -0
  35. data/lib/rubocop/cop/rspec/mixin/metadata.rb +1 -0
  36. data/lib/rubocop/cop/rspec/mixin/repeated_items.rb +36 -0
  37. data/lib/rubocop/cop/rspec/multiple_expectations.rb +1 -1
  38. data/lib/rubocop/cop/rspec/multiple_memoized_helpers.rb +1 -1
  39. data/lib/rubocop/cop/rspec/multiple_subjects.rb +1 -1
  40. data/lib/rubocop/cop/rspec/named_subject.rb +1 -1
  41. data/lib/rubocop/cop/rspec/no_expectation_example.rb +1 -0
  42. data/lib/rubocop/cop/rspec/overwriting_setup.rb +1 -1
  43. data/lib/rubocop/cop/rspec/predicate_matcher.rb +1 -1
  44. data/lib/rubocop/cop/rspec/redundant_around.rb +1 -0
  45. data/lib/rubocop/cop/rspec/repeated_description.rb +1 -1
  46. data/lib/rubocop/cop/rspec/repeated_example.rb +7 -5
  47. data/lib/rubocop/cop/rspec/repeated_example_group_body.rb +6 -10
  48. data/lib/rubocop/cop/rspec/repeated_example_group_description.rb +6 -10
  49. data/lib/rubocop/cop/rspec/repeated_include_example.rb +8 -11
  50. data/lib/rubocop/cop/rspec/return_from_stub.rb +1 -1
  51. data/lib/rubocop/cop/rspec/scattered_let.rb +11 -5
  52. data/lib/rubocop/cop/rspec/scattered_setup.rb +7 -9
  53. data/lib/rubocop/cop/rspec/shared_context.rb +47 -7
  54. data/lib/rubocop/cop/rspec/skip_block_inside_example.rb +3 -6
  55. data/lib/rubocop/cop/rspec/spec_file_path_format.rb +20 -12
  56. data/lib/rubocop/cop/rspec/subject_declaration.rb +17 -1
  57. data/lib/rubocop/cop/rspec/undescriptive_literals_description.rb +1 -1
  58. data/lib/rubocop/cop/rspec/void_expect.rb +3 -5
  59. data/lib/rubocop/cop/rspec/yield.rb +1 -1
  60. data/lib/rubocop/cop/rspec_cops.rb +2 -0
  61. data/lib/rubocop/rspec/description_extractor.rb +1 -1
  62. data/lib/rubocop/rspec/version.rb +1 -1
  63. data/lib/rubocop-rspec.rb +2 -0
  64. metadata +28 -4
@@ -89,6 +89,7 @@ module RuboCop
89
89
  end
90
90
 
91
91
  alias on_numblock on_block
92
+ alias on_itblock on_block
92
93
 
93
94
  private
94
95
 
@@ -45,6 +45,7 @@ module RuboCop
45
45
  end
46
46
 
47
47
  alias on_numblock on_block
48
+ alias on_itblock on_block
48
49
 
49
50
  private
50
51
 
@@ -59,7 +59,7 @@ module RuboCop
59
59
  }
60
60
  PATTERN
61
61
 
62
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
62
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
63
63
  return unless spec_group?(node)
64
64
 
65
65
  children = node.body&.child_nodes
@@ -42,7 +42,7 @@ module RuboCop
42
42
  ...)
43
43
  PATTERN
44
44
 
45
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
45
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
46
46
  return unless example?(node)
47
47
 
48
48
  null_double(node) do |var, receiver|
@@ -38,6 +38,13 @@ module RuboCop
38
38
  )
39
39
  PATTERN
40
40
 
41
+ # @!method each_itblock?(node)
42
+ def_node_matcher :each_itblock?, <<~PATTERN
43
+ (itblock
44
+ (send ... :each) _ (...)
45
+ )
46
+ PATTERN
47
+
41
48
  # @!method expectation?(node)
42
49
  def_node_matcher :expectation?, <<~PATTERN
43
50
  (send (send nil? :expect (lvar %)) :to ...)
@@ -55,6 +62,12 @@ module RuboCop
55
62
  end
56
63
  end
57
64
 
65
+ def on_itblock(node)
66
+ each_itblock?(node) do
67
+ check_offense(node, :it)
68
+ end
69
+ end
70
+
58
71
  private
59
72
 
60
73
  def check_offense(node, argument)
@@ -37,7 +37,7 @@ module RuboCop
37
37
 
38
38
  MSG = 'Declare `subject` above any other `%<offending>s` declarations.'
39
39
 
40
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
40
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
41
41
  return unless subject?(node)
42
42
  return unless inside_example_group?(node)
43
43
 
@@ -55,7 +55,7 @@ module RuboCop
55
55
  [RSpec::ScatteredLet]
56
56
  end
57
57
 
58
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
58
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
59
59
  return unless example_group_with_body?(node)
60
60
 
61
61
  check_let_declarations(node.body) if multiline_block?(node.body)
@@ -59,7 +59,7 @@ module RuboCop
59
59
  # @!method method_called?(node)
60
60
  def_node_search :method_called?, '(send nil? %)'
61
61
 
62
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
62
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
63
63
  return unless example_or_shared_group_or_including?(node)
64
64
 
65
65
  unused_let_bang(node) do |let|
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'regexp_parser'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ # Enforces the use of `include` matcher instead of `match` when the
9
+ # matcher is a simple string literal without regex-specific features.
10
+ #
11
+ # When `match` is used with a regex that contains only literal characters
12
+ # (no anchors, character classes, quantifiers, alternations, or
13
+ # metacharacters), it's clearer to use the `include` matcher instead.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # expect('foobar').to match(/foo/)
18
+ # expect(response.body).to match(/http:\/\/example\.com/)
19
+ #
20
+ # # good
21
+ # expect('foobar').to include('foo')
22
+ # expect(response.body).to include('http://example.com')
23
+ #
24
+ # # good - regex features needed
25
+ # expect('foobar').to match(/^foo/) # anchor
26
+ # expect('foobar').to match(/foo\d+/) # quantifier
27
+ # expect('foobar').to match(/foo[ob]/) # character class
28
+ #
29
+ class MatchWithSimpleRegex < Base
30
+ extend AutoCorrector
31
+
32
+ MSG = 'Prefer using `include(%<string>s)` when the regex is a simple ' \
33
+ 'string literal.'
34
+ RESTRICT_ON_SEND = %i[match].freeze
35
+
36
+ # @!method match_with_regexp?(node)
37
+ def_node_matcher :match_with_regexp?, <<~PATTERN
38
+ (send nil? :match $regexp)
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ match_with_regexp?(node) do |regexp|
43
+ next unless simple_regexp?(regexp)
44
+
45
+ string_literal = regexp_to_string(regexp)
46
+ message = format(MSG, string: string_literal)
47
+
48
+ add_offense(node, message: message) do |corrector|
49
+ corrector.replace(node, "include(#{string_literal})")
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def simple_regexp?(node)
57
+ return false if node.interpolation?
58
+ return false if node.regopt.children.any?
59
+
60
+ parsed = Regexp::Parser.parse(node.content)
61
+ parsed.expressions.all? { |expr| simple_expression?(expr) }
62
+ end
63
+
64
+ def simple_expression?(expr)
65
+ return false if expr.quantified?
66
+ return true if expr.is_a?(Regexp::Expression::Literal)
67
+ return true if expr.is_a?(Regexp::Expression::EscapeSequence::Literal)
68
+
69
+ false
70
+ end
71
+
72
+ # Reconstruct the literal string that the regex matches
73
+ def regexp_to_string(node)
74
+ parsed = Regexp::Parser.parse(node.content)
75
+ string_content = parsed.expressions.map do |expr|
76
+ expr.respond_to?(:char) ? expr.char : expr.text
77
+ end.join
78
+
79
+ to_string_literal(string_content)
80
+ end
81
+
82
+ def to_string_literal(string)
83
+ if string.include?("'")
84
+ %("#{string.gsub('"', '\"')}")
85
+ else
86
+ "'#{string}'"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -23,7 +23,7 @@ module RuboCop
23
23
  class MissingExampleGroupArgument < Base
24
24
  MSG = 'The first argument to `%<method>s` should not be empty.'
25
25
 
26
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
26
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
27
27
  return unless example_group?(node)
28
28
  return if node.send_node.arguments?
29
29
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Helps check if a given node is within an example block.
7
+ module InsideExample
8
+ private
9
+
10
+ def inside_example?(node)
11
+ node.each_ancestor(:block).any? { |ancestor| example?(ancestor) }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -39,6 +39,7 @@ module RuboCop
39
39
  end
40
40
  end
41
41
  alias on_numblock on_block
42
+ alias on_itblock on_block
42
43
 
43
44
  def on_metadata(_symbols, _hash)
44
45
  raise ::NotImplementedError
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Helps find repeated items in a collection
7
+ #
8
+ # Provides a generic method to find repeated items by grouping them
9
+ # by a key and returning pairs of [item, repeated_lines] for items
10
+ # that appear more than once.
11
+ module RepeatedItems
12
+ # Groups items by key and returns only groups with more than one item
13
+ #
14
+ # @param items [Enumerable] the filtered collection to group
15
+ # @param key_proc [Proc] block returning the grouping key for each item
16
+ # @return [Array<Array>] array of groups containing more than one item
17
+ # that share the same key and there are multiple items in the group
18
+ def find_repeated_groups(items, key_proc:)
19
+ items
20
+ .group_by(&key_proc)
21
+ .values
22
+ .reject(&:one?)
23
+ end
24
+
25
+ # Maps a group of items to pairs of [item, repeated_lines]
26
+ #
27
+ # @param items [Array] array of items that share the same key
28
+ # @return [Array<Array>] array of [item, repeated_lines] pairs
29
+ def add_repeated_lines(items)
30
+ repeated_lines = items.map(&:first_line)
31
+ items.map { |item| [item, repeated_lines - [item.first_line]] }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -90,7 +90,7 @@ module RuboCop
90
90
  (block (send nil? :aggregate_failures ...) ...)
91
91
  PATTERN
92
92
 
93
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
93
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
94
94
  return unless example?(node)
95
95
 
96
96
  return if example_with_aggregate_failures?(node)
@@ -88,7 +88,7 @@ module RuboCop
88
88
 
89
89
  exclude_limit 'Max'
90
90
 
91
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
91
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
92
92
  return unless spec_group?(node)
93
93
 
94
94
  count = all_helpers(node).uniq.count
@@ -54,7 +54,7 @@ module RuboCop
54
54
 
55
55
  MSG = 'Do not set more than one subject per example group'
56
56
 
57
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
57
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
58
58
  return unless example_group?(node)
59
59
 
60
60
  subjects = RuboCop::RSpec::ExampleGroup.new(node).subjects
@@ -94,7 +94,7 @@ module RuboCop
94
94
  # @!method subject_usage(node)
95
95
  def_node_search :subject_usage, '$(send nil? :subject)'
96
96
 
97
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
97
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
98
98
  if !example_or_hook_block?(node) || ignored_shared_example?(node)
99
99
  return
100
100
  end
@@ -96,6 +96,7 @@ module RuboCop
96
96
  end
97
97
 
98
98
  alias on_numblock on_block
99
+ alias on_itblock on_block
99
100
  end
100
101
  end
101
102
  end
@@ -33,7 +33,7 @@ module RuboCop
33
33
  # @!method first_argument_name(node)
34
34
  def_node_matcher :first_argument_name, '(send _ _ ({str sym} $_))'
35
35
 
36
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
36
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
37
37
  return unless example_group_with_body?(node)
38
38
 
39
39
  find_duplicates(node.body) do |duplicate, name|
@@ -340,7 +340,7 @@ module RuboCop
340
340
  end
341
341
  end
342
342
 
343
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
343
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
344
344
  check_explicit(node) if style == :explicit
345
345
  end
346
346
  end
@@ -28,6 +28,7 @@ module RuboCop
28
28
  end
29
29
  end
30
30
  alias on_numblock on_block
31
+ alias on_itblock on_block
31
32
 
32
33
  def on_send(node)
33
34
  return unless match_redundant_around_hook_send?(node)
@@ -42,7 +42,7 @@ module RuboCop
42
42
  class RepeatedDescription < Base
43
43
  MSG = "Don't repeat descriptions within an example group."
44
44
 
45
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
45
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
46
46
  return unless example_group?(node)
47
47
 
48
48
  repeated_descriptions(node).each do |description|
@@ -16,10 +16,12 @@ module RuboCop
16
16
  # end
17
17
  #
18
18
  class RepeatedExample < Base
19
+ include RepeatedItems
20
+
19
21
  MSG = "Don't repeat examples within an example group. " \
20
22
  'Repeated on line(s) %<lines>s.'
21
23
 
22
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
24
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
23
25
  return unless example_group?(node)
24
26
 
25
27
  find_repeated_examples(node).each do |repeated_examples|
@@ -32,10 +34,10 @@ module RuboCop
32
34
  def find_repeated_examples(node)
33
35
  examples = RuboCop::RSpec::ExampleGroup.new(node).examples
34
36
 
35
- examples
36
- .group_by { |example| build_example_signature(example) }
37
- .values
38
- .select { |group| group.size > 1 }
37
+ find_repeated_groups(
38
+ examples,
39
+ key_proc: ->(example) { build_example_signature(example) }
40
+ )
39
41
  end
40
42
 
41
43
  def build_example_signature(example)
@@ -44,6 +44,7 @@ module RuboCop
44
44
  #
45
45
  class RepeatedExampleGroupBody < Base
46
46
  include SkipOrPending
47
+ include RepeatedItems
47
48
 
48
49
  MSG = 'Repeated %<group>s block body on line(s) %<loc>s'
49
50
 
@@ -72,19 +73,14 @@ module RuboCop
72
73
  private
73
74
 
74
75
  def repeated_group_bodies(node)
75
- node
76
- .children
76
+ items = node.children
77
77
  .select { |child| example_group_with_body?(child) }
78
78
  .reject { |child| skip_or_pending_inside_block?(child) }
79
- .group_by { |group| signature_keys(group) }
80
- .values
81
- .reject(&:one?)
82
- .flat_map { |groups| add_repeated_lines(groups) }
83
- end
84
79
 
85
- def add_repeated_lines(groups)
86
- repeated_lines = groups.map(&:first_line)
87
- groups.map { |group| [group, repeated_lines - [group.first_line]] }
80
+ find_repeated_groups(
81
+ items,
82
+ key_proc: ->(group) { signature_keys(group) }
83
+ ).flat_map { |group| add_repeated_lines(group) }
88
84
  end
89
85
 
90
86
  def signature_keys(group)
@@ -44,6 +44,7 @@ module RuboCop
44
44
  #
45
45
  class RepeatedExampleGroupDescription < Base
46
46
  include SkipOrPending
47
+ include RepeatedItems
47
48
 
48
49
  MSG = 'Repeated %<group>s block description on line(s) %<loc>s'
49
50
 
@@ -71,20 +72,15 @@ module RuboCop
71
72
  private
72
73
 
73
74
  def repeated_group_descriptions(node)
74
- node
75
- .children
75
+ items = node.children
76
76
  .select { |child| example_group?(child) }
77
77
  .reject { |child| skip_or_pending_inside_block?(child) }
78
78
  .reject { |child| empty_description?(child) }
79
- .group_by { |group| doc_string_and_metadata(group) }
80
- .values
81
- .reject(&:one?)
82
- .flat_map { |groups| add_repeated_lines(groups) }
83
- end
84
79
 
85
- def add_repeated_lines(groups)
86
- repeated_lines = groups.map(&:first_line)
87
- groups.map { |group| [group, repeated_lines - [group.first_line]] }
80
+ find_repeated_groups(
81
+ items,
82
+ key_proc: ->(group) { doc_string_and_metadata(group) }
83
+ ).flat_map { |group| add_repeated_lines(group) }
88
84
  end
89
85
 
90
86
  def message(group, repeats)
@@ -46,6 +46,8 @@ module RuboCop
46
46
  # end
47
47
  #
48
48
  class RepeatedIncludeExample < Base
49
+ include RepeatedItems
50
+
49
51
  MSG = 'Repeated include of shared_examples %<name>s ' \
50
52
  'on line(s) %<repeat>s'
51
53
 
@@ -73,13 +75,13 @@ module RuboCop
73
75
  private
74
76
 
75
77
  def repeated_include_examples(node)
76
- node
77
- .children
78
+ items = node.children
78
79
  .select { |child| literal_include_examples?(child) }
79
- .group_by { |child| signature_keys(child) }
80
- .values
81
- .reject(&:one?)
82
- .flat_map { |items| add_repeated_lines(items) }
80
+
81
+ find_repeated_groups(
82
+ items,
83
+ key_proc: ->(child) { signature_keys(child) }
84
+ ).flat_map { |group| add_repeated_lines(group) }
83
85
  end
84
86
 
85
87
  def literal_include_examples?(node)
@@ -87,11 +89,6 @@ module RuboCop
87
89
  node.arguments.all?(&:recursive_literal_or_const?)
88
90
  end
89
91
 
90
- def add_repeated_lines(items)
91
- repeated_lines = items.map(&:first_line)
92
- items.map { |item| [item, repeated_lines - [item.first_line]] }
93
- end
94
-
95
92
  def signature_keys(item)
96
93
  item.arguments
97
94
  end
@@ -59,7 +59,7 @@ module RuboCop
59
59
  check_and_return_call(node)
60
60
  end
61
61
 
62
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
62
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
63
63
  return unless style == :and_return
64
64
  return unless stub_with_block?(node)
65
65
 
@@ -19,11 +19,11 @@ module RuboCop
19
19
  #
20
20
  # # good
21
21
  # describe Foo do
22
- # subject { Foo }
23
- # before { prepare }
24
22
  # let(:foo) { 1 }
25
23
  # let(:bar) { 2 }
26
24
  # let!(:baz) { 3 }
25
+ # subject { Foo }
26
+ # before { prepare }
27
27
  # end
28
28
  #
29
29
  class ScatteredLet < Base
@@ -31,7 +31,7 @@ module RuboCop
31
31
 
32
32
  MSG = 'Group all let/let! blocks in the example group together.'
33
33
 
34
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
34
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
35
35
  return unless example_group_with_body?(node)
36
36
 
37
37
  check_let_declarations(node.body)
@@ -41,15 +41,21 @@ module RuboCop
41
41
 
42
42
  def check_let_declarations(body)
43
43
  lets = body.each_child_node.select { |node| let?(node) }
44
+ return if lets.empty?
44
45
 
45
46
  first_let = lets.first
47
+ reference_let = first_let
48
+
46
49
  lets.each_with_index do |node, idx|
47
- next if node.sibling_index == first_let.sibling_index + idx
50
+ if node.sibling_index == first_let.sibling_index + idx
51
+ reference_let = node
52
+ next
53
+ end
48
54
 
49
55
  add_offense(node) do |corrector|
50
56
  RuboCop::RSpec::Corrector::MoveNode.new(
51
57
  node, corrector, processed_source
52
- ).move_after(first_let)
58
+ ).move_after(reference_let)
53
59
  end
54
60
  end
55
61
  end
@@ -42,12 +42,13 @@ module RuboCop
42
42
  class ScatteredSetup < Base
43
43
  include FinalEndLocation
44
44
  include RangeHelp
45
+ include RepeatedItems
45
46
  extend AutoCorrector
46
47
 
47
48
  MSG = 'Do not define multiple `%<hook_name>s` hooks in the same ' \
48
49
  'example group (also defined on %<lines>s).'
49
50
 
50
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
51
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
51
52
  return unless example_group?(node)
52
53
 
53
54
  repeated_hooks(node).each do |occurrences|
@@ -63,17 +64,14 @@ module RuboCop
63
64
  private
64
65
 
65
66
  def repeated_hooks(node) # rubocop:disable Metrics/CyclomaticComplexity
66
- hooks = RuboCop::RSpec::ExampleGroup.new(node)
67
- .hooks
67
+ hooks = RuboCop::RSpec::ExampleGroup.new(node).hooks
68
68
  .reject(&:inside_class_method?)
69
69
  .select { |hook| hook.knowable_scope? && hook.name != :around }
70
- .group_by { |hook| [hook.name, hook.scope, hook.metadata] }
71
- .values
72
- .reject(&:one?)
73
70
 
74
- hooks.map do |hook|
75
- hook.map(&:to_node)
76
- end
71
+ find_repeated_groups(
72
+ hooks,
73
+ key_proc: ->(hook) { [hook.name, hook.scope, hook.metadata] }
74
+ ).map { |hook_group| hook_group.map(&:to_node) }
77
75
  end
78
76
 
79
77
  def lines_msg(numbers)