rubocop-rspec 3.10.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5b6ac2158196f3722c9b53e233a5f96becfced4ae47abe76ad577f3e258ff1c
4
- data.tar.gz: be3134943d1be32c95f193ef984e27ba7ce3091b862e0e338afb8167c3e3b106
3
+ metadata.gz: 4dd90ecda80edb7aeadd84bf5866b0d6b7d44dca8d56601782b5686fa75c970e
4
+ data.tar.gz: 5205b01345d70457d949142a464639589df5f87f3cd1ddfe7c8a64439bb519a1
5
5
  SHA512:
6
- metadata.gz: 51df438548b237d380ada23cfc3e38d399e86951588acda134bf23abe6ad7b36d206e4eddd52457695ac74aba24ec84535074828827b953ad46ac6932315f0c4
7
- data.tar.gz: 93eb7b7f6c8cfad2a5e816488ef9a60df934814947086b4b702d634ac9f6643dbafdd8acec8ab1eb8449c8e76e710ff794990b22213d7335e916b65054b87981
6
+ metadata.gz: cca8c27b7c61499264c8dd30009dd00a1c3d13cc5dda8efa4ff930706845d8c7ce7630f1402520dd421d36a1ed2971dfa62f0f182fc032ebae7920618ba3aaa9
7
+ data.tar.gz: b605960f8d588ccab7ebfa78874fe317e0fdf9c7db3b5cd0bdf682d226ac5326475f0cb05a123b491c21ccde6c60f82cc2cee18c30e593b6dc0ac204520fdebc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 3.10.1 (2026-06-05)
6
+
7
+ - Add `Strict` option to `RSpec/SharedContext` to flag `shared_context` whenever it contains examples, even alongside setup code. ([@Darhazer])
8
+ - Add `NegatedMatcher` configuration option `RSpec/ExpectChange`. ([@Darhazer])
9
+ - Fix `RSpec/MatchWithSimpleRegex` to ignore regular expressions with interpolations. ([@bquorning])
10
+
5
11
  ## 3.10.0 (2026-06-05)
6
12
 
7
13
  - Add new cop `RSpec/MatchWithSimpleRegex` to suggest `include` matcher when `match` is used with simple string literals without regex-specific features. ([@bquorning])
data/config/default.yml CHANGED
@@ -460,7 +460,8 @@ RSpec/ExpectChange:
460
460
  - block
461
461
  SafeAutoCorrect: false
462
462
  VersionAdded: '1.22'
463
- VersionChanged: '2.5'
463
+ VersionChanged: '3.10'
464
+ NegatedMatcher: ~
464
465
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectChange
465
466
 
466
467
  RSpec/ExpectInHook:
@@ -915,7 +916,9 @@ RSpec/ScatteredSetup:
915
916
  RSpec/SharedContext:
916
917
  Description: Checks for proper shared_context and shared_examples usage.
917
918
  Enabled: true
919
+ Strict: false
918
920
  VersionAdded: '1.13'
921
+ VersionChanged: '3.10'
919
922
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedContext
920
923
 
921
924
  RSpec/SharedExamples:
@@ -10,6 +10,10 @@ module RuboCop
10
10
  #
11
11
  # This cop can be configured using the `EnforcedStyle` option.
12
12
  #
13
+ # When using compound expectations with `change` and a negated matcher
14
+ # (e.g., `not_change`), you can configure the `NegatedMatcher` option
15
+ # to ensure consistent style enforcement across both matchers.
16
+ #
13
17
  # @safety
14
18
  # Autocorrection is unsafe because `method_call` style calls the
15
19
  # receiver *once* and sends the message to it before and after
@@ -48,23 +52,29 @@ module RuboCop
48
52
  # # good
49
53
  # expect { run }.to change { Foo.bar }
50
54
  #
55
+ # @example `NegatedMatcher: not_change` (with compound expectations)
56
+ # # bad
57
+ # expect { run }.to change(Foo, :bar).and not_change { Foo.baz }
58
+ #
59
+ # # good
60
+ # expect { run }.to change(Foo, :bar).and not_change(Foo, :baz)
61
+ #
51
62
  class ExpectChange < Base
52
63
  extend AutoCorrector
53
64
  include ConfigurableEnforcedStyle
54
65
 
55
- MSG_BLOCK = 'Prefer `change(%<obj>s, :%<attr>s)`.'
56
- MSG_CALL = 'Prefer `change { %<obj>s.%<attr>s }`.'
57
- RESTRICT_ON_SEND = %i[change].freeze
66
+ MSG_BLOCK = 'Prefer `%<matcher>s(%<obj>s, :%<attr>s)`.'
67
+ MSG_CALL = 'Prefer `%<matcher>s { %<obj>s.%<attr>s }`.'
58
68
 
59
- # @!method expect_change_with_arguments(node)
60
- def_node_matcher :expect_change_with_arguments, <<~PATTERN
61
- (send nil? :change $_ ({sym str} $_))
69
+ # @!method expect_matcher_with_arguments(node)
70
+ def_node_matcher :expect_matcher_with_arguments, <<~PATTERN
71
+ (send nil? _ $_ ({sym str} $_))
62
72
  PATTERN
63
73
 
64
- # @!method expect_change_with_block(node)
65
- def_node_matcher :expect_change_with_block, <<~PATTERN
74
+ # @!method expect_matcher_with_block(node)
75
+ def_node_matcher :expect_matcher_with_block, <<~PATTERN
66
76
  (block
67
- (send nil? :change)
77
+ (send nil? _)
68
78
  (args)
69
79
  (send
70
80
  ${
@@ -78,11 +88,14 @@ module RuboCop
78
88
 
79
89
  def on_send(node)
80
90
  return unless style == :block
91
+ return unless matcher_method?(node.method_name)
81
92
 
82
- expect_change_with_arguments(node) do |receiver, message|
83
- msg = format(MSG_CALL, obj: receiver.source, attr: message)
93
+ expect_matcher_with_arguments(node) do |receiver, message|
94
+ matcher_name = node.method_name.to_s
95
+ msg = format(MSG_CALL, matcher: matcher_name,
96
+ obj: receiver.source, attr: message)
84
97
  add_offense(node, message: msg) do |corrector|
85
- replacement = "change { #{receiver.source}.#{message} }"
98
+ replacement = "#{matcher_name} { #{receiver.source}.#{message} }"
86
99
  corrector.replace(node, replacement)
87
100
  end
88
101
  end
@@ -90,15 +103,32 @@ module RuboCop
90
103
 
91
104
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
92
105
  return unless style == :method_call
106
+ return unless matcher_method?(node.method_name)
93
107
 
94
- expect_change_with_block(node) do |receiver, message|
95
- msg = format(MSG_BLOCK, obj: receiver.source, attr: message)
108
+ expect_matcher_with_block(node) do |receiver, message|
109
+ matcher_name = node.method_name.to_s
110
+ msg = format(MSG_BLOCK, matcher: matcher_name,
111
+ obj: receiver.source, attr: message)
96
112
  add_offense(node, message: msg) do |corrector|
97
- replacement = "change(#{receiver.source}, :#{message})"
113
+ replacement = "#{matcher_name}(#{receiver.source}, :#{message})"
98
114
  corrector.replace(node, replacement)
99
115
  end
100
116
  end
101
117
  end
118
+
119
+ private
120
+
121
+ def matcher_method_names
122
+ [:change, negated_matcher&.to_sym].compact
123
+ end
124
+
125
+ def matcher_method?(method_name)
126
+ matcher_method_names.include?(method_name)
127
+ end
128
+
129
+ def negated_matcher
130
+ cop_config['NegatedMatcher']
131
+ end
102
132
  end
103
133
  end
104
134
  end
@@ -54,6 +54,8 @@ module RuboCop
54
54
  private
55
55
 
56
56
  def simple_regexp?(node)
57
+ return false if node.interpolation?
58
+
57
59
  parsed = Regexp::Parser.parse(node.content)
58
60
  parsed.expressions.all? { |expr| simple_expression?(expr) }
59
61
  end
@@ -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
@@ -16,6 +16,8 @@ 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
 
@@ -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
@@ -42,6 +42,7 @@ 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 ' \
@@ -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)
@@ -8,6 +8,9 @@ module RuboCop
8
8
  # If there are no examples defined, use shared_context.
9
9
  # If there is no setup defined, use shared_examples.
10
10
  #
11
+ # With `Strict: true`, `shared_context` is flagged whenever it contains
12
+ # any examples, even if it also contains setup code.
13
+ #
11
14
  # @example
12
15
  # # bad
13
16
  # RSpec.shared_context 'only examples here' do
@@ -50,11 +53,31 @@ module RuboCop
50
53
  # end
51
54
  # end
52
55
  #
56
+ # @example Strict: true
57
+ # # bad - shared_context with examples is flagged
58
+ # RSpec.shared_context 'setup and examples' do
59
+ # let(:foo) { :bar }
60
+ #
61
+ # it 'does x' do
62
+ # end
63
+ # end
64
+ #
65
+ # # good - split into separate shared_context and shared_examples
66
+ # RSpec.shared_context 'setup' do
67
+ # let(:foo) { :bar }
68
+ # end
69
+ #
70
+ # RSpec.shared_examples 'examples' do
71
+ # it 'does x' do
72
+ # end
73
+ # end
74
+ #
53
75
  class SharedContext < Base
54
76
  extend AutoCorrector
55
77
 
56
78
  MSG_EXAMPLES = "Use `shared_examples` when you don't define context."
57
- MSG_CONTEXT = "Use `shared_context` when you don't define examples."
79
+ MSG_EXAMPLES_STRICT = 'Use `shared_examples` when you define examples.'
80
+ MSG_CONTEXT = "Use `shared_context` when you don't define examples."
58
81
 
59
82
  # @!method examples?(node)
60
83
  def_node_search :examples?, <<~PATTERN
@@ -79,9 +102,12 @@ module RuboCop
79
102
  PATTERN
80
103
 
81
104
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
82
- context_with_only_examples(node) do
83
- add_offense(node.send_node, message: MSG_EXAMPLES) do |corrector|
84
- corrector.replace(node.send_node.loc.selector, 'shared_examples')
105
+ offending_node(node) do
106
+ add_offense(node.send_node, message: message) do |corrector|
107
+ if can_correct?(node)
108
+ corrector.replace(node.send_node.loc.selector,
109
+ 'shared_examples')
110
+ end
85
111
  end
86
112
  end
87
113
 
@@ -94,8 +120,22 @@ module RuboCop
94
120
 
95
121
  private
96
122
 
97
- def context_with_only_examples(node)
98
- shared_context(node) { yield if examples?(node) && !context?(node) }
123
+ def strict?
124
+ cop_config.fetch('Strict', false)
125
+ end
126
+
127
+ def message
128
+ strict? ? MSG_EXAMPLES_STRICT : MSG_EXAMPLES
129
+ end
130
+
131
+ def can_correct?(node)
132
+ !strict? || !context?(node)
133
+ end
134
+
135
+ def offending_node(node)
136
+ shared_context(node) do
137
+ yield if examples?(node) && (strict? || !context?(node))
138
+ end
99
139
  end
100
140
 
101
141
  def examples_with_only_context(node)
@@ -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 = '3.10.0'
7
+ STRING = '3.10.1'
8
8
  end
9
9
  end
10
10
  end
data/lib/rubocop-rspec.rb CHANGED
@@ -19,6 +19,7 @@ require_relative 'rubocop/cop/rspec/mixin/inside_example_group'
19
19
  require_relative 'rubocop/cop/rspec/mixin/location_help'
20
20
  require_relative 'rubocop/cop/rspec/mixin/metadata'
21
21
  require_relative 'rubocop/cop/rspec/mixin/namespace'
22
+ require_relative 'rubocop/cop/rspec/mixin/repeated_items'
22
23
  require_relative 'rubocop/cop/rspec/mixin/skip_or_pending'
23
24
  require_relative 'rubocop/cop/rspec/mixin/top_level_group'
24
25
  require_relative 'rubocop/cop/rspec/mixin/variable'
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: 3.10.0
4
+ version: 3.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Backus
@@ -158,6 +158,7 @@ files:
158
158
  - lib/rubocop/cop/rspec/mixin/location_help.rb
159
159
  - lib/rubocop/cop/rspec/mixin/metadata.rb
160
160
  - lib/rubocop/cop/rspec/mixin/namespace.rb
161
+ - lib/rubocop/cop/rspec/mixin/repeated_items.rb
161
162
  - lib/rubocop/cop/rspec/mixin/skip_or_pending.rb
162
163
  - lib/rubocop/cop/rspec/mixin/top_level_group.rb
163
164
  - lib/rubocop/cop/rspec/mixin/variable.rb