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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9159cd716027712a72b3a3e675f83a92c55070200ae0f628a8b00c7333bc99f0
4
- data.tar.gz: 5e84a7c7108c0f745cfa3e32ab75fd9fe50a1b0f2dae4ba6e0433d6cf5cd30dc
3
+ metadata.gz: 61b047dc2dcdafc0499a7df00f8689fa68090a92b3a9897d37fee92a88655523
4
+ data.tar.gz: 52f2bd8775f31951731ca043c75297722f40d6fe943f5b8d9fadc9f54b6dfc66
5
5
  SHA512:
6
- metadata.gz: bc41fe5a7eccbb4452fd4bc6459d03ceb5dcd8a13316245b223db3088ea25e38bfa029e834f527dbb511b80ce5b553c83673a6e6f957c974f599d7140d4f7d40
7
- data.tar.gz: 0c6a077dd4b221d68524ed4b2734727000efb97c5fb553c31016aab51f57e0049f67c701727f6d7eeb04c42ad2866b96592793bc768bac7361ca4e7ca0d81248
6
+ metadata.gz: 5f68a310bb36188a5bc1d0114c8c31606d3e7f280607f26a032177dd42c7fdf88b283166b8f41d3968559527985b9c3ea10f0da036b09945c3696c8632bab3a5
7
+ data.tar.gz: 37d2040cb0b7d48fed88ba954a4ec0601d9ba11c6a1b3547ef553e3b35f509b9dc89b23820635ed9e7048da6a7fe4611dc5167b679a3e26336b2f940c6858e73
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 3.10.2 (2026-06-06)
6
+
7
+ - Fix false positives for `RSpec/SpecFilePathFormat` when `CustomTransform` maps a namespace to an empty string. ([@sakuro])
8
+ - Fix `RSpec/MatchWithSimpleRegex` to ignore regular expressions with options. ([@bquorning])
9
+
10
+ ## 3.10.1 (2026-06-05)
11
+
12
+ - Add `Strict` option to `RSpec/SharedContext` to flag `shared_context` whenever it contains examples, even alongside setup code. ([@Darhazer])
13
+ - Add `NegatedMatcher` configuration option `RSpec/ExpectChange`. ([@Darhazer])
14
+ - Fix `RSpec/MatchWithSimpleRegex` to ignore regular expressions with interpolations. ([@bquorning])
15
+
16
+ ## 3.10.0 (2026-06-05)
17
+
18
+ - Add new cop `RSpec/MatchWithSimpleRegex` to suggest `include` matcher when `match` is used with simple string literals without regex-specific features. ([@bquorning])
19
+ - Add new cop `RSpec/DiscardedMatcher` to detect matchers in void context (e.g. missing `.and` between compound matchers). ([@ydakuka])
20
+ - Add support for `itblock` nodes. ([@Darhazer])
21
+ - `RSpec/ScatteredLet` now preserves the order of `let`s during auto-correction. ([@Darhazer])
22
+ - Fix a false negative for `RSpec/EmptyLineAfterFinalLet` inside `shared_examples` / `include_examples` / `it_behaves_like` blocks. ([@Darhazer])
23
+ - Fix a false positive for `RSpec/ContainExactly` when `contain_exactly` has multiple splat arguments. ([@ydah])
24
+ - Add autocorrect support for `RSpec/SubjectDeclaration`. ([@eugeneius])
25
+ - Fix false negatives for `RSpec/SpecFilePathFormat` when the expected class path only partially matches a path segment. ([@ydah])
26
+ - Fix a false negative for `RSpec/ExpectActual` when the matcher takes no arguments (e.g. `expect("foo").to be_present`, `expect(1).to be`). ([@cvx])
27
+
5
28
  ## 3.9.0 (2026-01-07)
6
29
 
7
30
  - Fix a false positive for `RSpec/LeakyLocalVariable` when variables are used only in example metadata (e.g., skip messages). ([@ydah])
@@ -988,6 +1011,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
988
1011
  [@composerinteralia]: https://github.com/composerinteralia
989
1012
  [@corsonknowles]: https://github.com/corsonknowles
990
1013
  [@corydiamand]: https://github.com/corydiamand
1014
+ [@cvx]: https://github.com/cvx
991
1015
  [@d4rky-pl]: https://github.com/d4rky-pl
992
1016
  [@darhazer]: https://github.com/Darhazer
993
1017
  [@daveworth]: https://github.com/daveworth
@@ -1006,6 +1030,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
1006
1030
  [@elebow]: https://github.com/elebow
1007
1031
  [@elisefitz15]: https://github.com/EliseFitz15
1008
1032
  [@elliterate]: https://github.com/elliterate
1033
+ [@eugeneius]: https://github.com/eugeneius
1009
1034
  [@faucct]: https://github.com/faucct
1010
1035
  [@foton]: https://github.com/foton
1011
1036
  [@francois-ferrandis]: https://github.com/francois-ferrandis
@@ -1103,6 +1128,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
1103
1128
  [@yasu551]: https://github.com/yasu551
1104
1129
  [@ybiquitous]: https://github.com/ybiquitous
1105
1130
  [@ydah]: https://github.com/ydah
1131
+ [@ydakuka]: https://github.com/ydakuka
1106
1132
  [@yevhene]: https://github.com/yevhene
1107
1133
  [@ypresto]: https://github.com/ypresto
1108
1134
  [@yujideveloper]: https://github.com/yujideveloper
data/config/default.yml CHANGED
@@ -311,6 +311,13 @@ RSpec/Dialect:
311
311
  VersionAdded: '1.33'
312
312
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Dialect
313
313
 
314
+ RSpec/DiscardedMatcher:
315
+ Description: Checks for matchers that are used in void context.
316
+ Enabled: pending
317
+ VersionAdded: '3.10'
318
+ CustomMatcherMethods: []
319
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/DiscardedMatcher
320
+
314
321
  RSpec/DuplicatedMetadata:
315
322
  Description: Avoid duplicated metadata.
316
323
  Enabled: true
@@ -453,7 +460,8 @@ RSpec/ExpectChange:
453
460
  - block
454
461
  SafeAutoCorrect: false
455
462
  VersionAdded: '1.22'
456
- VersionChanged: '2.5'
463
+ VersionChanged: '3.10'
464
+ NegatedMatcher: ~
457
465
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExpectChange
458
466
 
459
467
  RSpec/ExpectInHook:
@@ -636,6 +644,13 @@ RSpec/MatchArray:
636
644
  VersionAdded: '2.19'
637
645
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MatchArray
638
646
 
647
+ RSpec/MatchWithSimpleRegex:
648
+ Description: Enforces the use of `include` matcher instead of `match` when the matcher
649
+ is a simple string literal without regex-specific features.
650
+ Enabled: pending
651
+ VersionAdded: '3.10'
652
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MatchWithSimpleRegex
653
+
639
654
  RSpec/MessageChain:
640
655
  Description: Check that chains of messages are not being stubbed.
641
656
  Enabled: true
@@ -901,7 +916,9 @@ RSpec/ScatteredSetup:
901
916
  RSpec/SharedContext:
902
917
  Description: Checks for proper shared_context and shared_examples usage.
903
918
  Enabled: true
919
+ Strict: false
904
920
  VersionAdded: '1.13'
921
+ VersionChanged: '3.10'
905
922
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedContext
906
923
 
907
924
  RSpec/SharedExamples:
@@ -41,6 +41,11 @@ module RuboCop
41
41
  (numblock (send nil? :around sym ?) ...)
42
42
  PATTERN
43
43
 
44
+ # @!method hook_itblock(node)
45
+ def_node_matcher :hook_itblock, <<~PATTERN
46
+ (itblock (send nil? :around sym ?) ...)
47
+ PATTERN
48
+
44
49
  # @!method find_arg_usage(node)
45
50
  def_node_search :find_arg_usage, <<~PATTERN
46
51
  {(send $... {:call :run}) (send _ _ $...) (yield $...) (block-pass $...)}
@@ -62,6 +67,12 @@ module RuboCop
62
67
  end
63
68
  end
64
69
 
70
+ def on_itblock(node)
71
+ hook_itblock(node) do
72
+ check_for_itblock(node)
73
+ end
74
+ end
75
+
65
76
  private
66
77
 
67
78
  def add_no_arg_offense(node)
@@ -89,6 +100,17 @@ module RuboCop
89
100
  message: format(MSG_UNUSED_ARG, arg: :_1)
90
101
  )
91
102
  end
103
+
104
+ def check_for_itblock(block)
105
+ find_arg_usage(block) do |usage|
106
+ return if usage.include?(s(:lvar, :it))
107
+ end
108
+
109
+ add_offense(
110
+ block.children.last,
111
+ message: format(MSG_UNUSED_ARG, arg: :it)
112
+ )
113
+ end
92
114
  end
93
115
  end
94
116
  end
@@ -12,10 +12,13 @@ module RuboCop
12
12
  #
13
13
  # @example
14
14
  # # bad
15
- # it { is_expected.to contain_exactly(*array1, *array2) }
15
+ # it { is_expected.to contain_exactly(*array) }
16
+ #
17
+ # # good
18
+ # it { is_expected.to match_array(array) }
16
19
  #
17
20
  # # good
18
- # it { is_expected.to match_array(array1 + array2) }
21
+ # it { is_expected.to contain_exactly(*array1, *array2) }
19
22
  #
20
23
  # # good
21
24
  # it { is_expected.to contain_exactly(content, *array) }
@@ -27,29 +30,12 @@ module RuboCop
27
30
  RESTRICT_ON_SEND = %i[contain_exactly].freeze
28
31
 
29
32
  def on_send(node)
30
- return if node.arguments.empty?
31
-
32
- check_populated_collection(node)
33
- end
34
-
35
- private
36
-
37
- def check_populated_collection(node)
38
- return unless node.each_child_node.all?(&:splat_type?)
33
+ return unless node.arguments.one? && node.first_argument.splat_type?
39
34
 
40
35
  add_offense(node) do |corrector|
41
- autocorrect_for_populated_array(node, corrector)
42
- end
43
- end
44
-
45
- def autocorrect_for_populated_array(node, corrector)
46
- arrays = node.arguments.map do |splat_node|
47
- splat_node.children.first
36
+ array = node.first_argument.children.first
37
+ corrector.replace(node, "match_array(#{array.source})")
48
38
  end
49
- corrector.replace(
50
- node,
51
- "match_array(#{arrays.map(&:source).join(' + ')})"
52
- )
53
39
  end
54
40
  end
55
41
  end
@@ -38,7 +38,7 @@ module RuboCop
38
38
  ...)
39
39
  PATTERN
40
40
 
41
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
41
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
42
42
  context_method(node) do |context|
43
43
  add_offense(context) do |corrector|
44
44
  corrector.replace(node.send_node.loc.selector, 'describe')
@@ -70,7 +70,7 @@ module RuboCop
70
70
  (block (send #rspec? { :context :shared_context } $(any_str ...) ...) ...)
71
71
  PATTERN
72
72
 
73
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
73
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
74
74
  context_wording(node) do |context|
75
75
  unless matches_allowed_pattern?(description(context))
76
76
  add_offense(context, message: message)
@@ -110,7 +110,7 @@ module RuboCop
110
110
  def_node_search :contains_described_class?,
111
111
  '(send nil? :described_class)'
112
112
 
113
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
113
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
114
114
  # In case the explicit style is used, we need to remember what's
115
115
  # being described.
116
116
  @described_class, body = described_constant(node)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for matchers that are used in void context.
7
+ #
8
+ # Matcher calls like `change`, `receive`, etc. that appear as
9
+ # standalone expressions have their result silently discarded.
10
+ # This usually means a missing `.and` to chain compound matchers.
11
+ #
12
+ # The list of matcher methods can be configured
13
+ # with `CustomMatcherMethods`.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # specify do
18
+ # expect { result }
19
+ # .to change { obj.foo }.from(1).to(2)
20
+ # change { obj.bar }.from(3).to(4)
21
+ # end
22
+ #
23
+ # # good
24
+ # specify do
25
+ # expect { result }
26
+ # .to change { obj.foo }.from(1).to(2)
27
+ # .and change { obj.bar }.from(3).to(4)
28
+ # end
29
+ #
30
+ # # good
31
+ # specify do
32
+ # expect { result }.to change { obj.foo }.from(1).to(2)
33
+ # end
34
+ #
35
+ class DiscardedMatcher < Base
36
+ include InsideExample
37
+
38
+ MSG = 'The result of `%<method>s` is not used. ' \
39
+ 'Did you mean to chain it with `.and`?'
40
+
41
+ MATCHER_METHODS = %i[
42
+ change have_received output
43
+ receive receive_messages receive_message_chain
44
+ ].to_set.freeze
45
+
46
+ def on_send(node)
47
+ check_discarded_matcher(node, node)
48
+ end
49
+
50
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
51
+ check_discarded_matcher(node.send_node, node)
52
+ end
53
+
54
+ private
55
+
56
+ def check_discarded_matcher(send_node, node)
57
+ return unless matcher_call?(send_node)
58
+ return unless inside_example?(node)
59
+ return unless example_with_matcher_expectation?(node)
60
+
61
+ target = find_outermost_chain(node)
62
+ return unless void_value?(target)
63
+
64
+ add_offense(target, message: format(MSG, method: node.method_name))
65
+ end
66
+
67
+ def example_with_matcher_expectation?(node)
68
+ example_node =
69
+ node.each_ancestor(:block).find { |ancestor| example?(ancestor) }
70
+
71
+ example_node.each_descendant(:send).any? do |send_node|
72
+ expectation_with_matcher?(send_node)
73
+ end
74
+ end
75
+
76
+ def expectation_with_matcher?(node)
77
+ %i[to to_not not_to].include?(node.method_name) &&
78
+ node.arguments.any? do |arg|
79
+ arg.each_node(:send).any? { |s| matcher_call?(s) }
80
+ end
81
+ end
82
+
83
+ def void_value?(node)
84
+ case node.parent.type
85
+ when :block
86
+ example?(node.parent)
87
+ when :begin, :case, :when
88
+ void_value?(node.parent)
89
+ end
90
+ end
91
+
92
+ def matcher_call?(node)
93
+ node.receiver.nil? && all_matcher_methods.include?(node.method_name)
94
+ end
95
+
96
+ def all_matcher_methods
97
+ @all_matcher_methods ||=
98
+ (MATCHER_METHODS + custom_matcher_methods).freeze
99
+ end
100
+
101
+ def custom_matcher_methods
102
+ cop_config.fetch('CustomMatcherMethods', []).map(&:to_sym)
103
+ end
104
+
105
+ def find_outermost_chain(node)
106
+ current = node
107
+ current = current.parent while current.parent.receiver == current
108
+ current
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -38,6 +38,7 @@ module RuboCop
38
38
  class EmptyExampleGroup < Base
39
39
  extend AutoCorrector
40
40
 
41
+ include InsideExample
41
42
  include RangeHelp
42
43
 
43
44
  MSG = 'Empty example group detected.'
@@ -136,9 +137,9 @@ module RuboCop
136
137
  }
137
138
  PATTERN
138
139
 
139
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
140
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
140
141
  return if node.each_ancestor(:any_def).any?
141
- return if node.each_ancestor(:block).any? { |block| example?(block) }
142
+ return if inside_example?(node)
142
143
 
143
144
  example_group_body(node) do |body|
144
145
  next unless offensive?(body)
@@ -34,7 +34,7 @@ module RuboCop
34
34
  (block $(send nil? #Hooks.all ...) _ nil?)
35
35
  PATTERN
36
36
 
37
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
37
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
38
38
  empty_hook?(node) do |hook|
39
39
  add_offense(hook) do |corrector|
40
40
  corrector.remove(
@@ -46,7 +46,7 @@ module RuboCop
46
46
 
47
47
  MSG = 'Add an empty line after `%<example>s`.'
48
48
 
49
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
49
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
50
50
  return unless example?(node)
51
51
  return if allowed_one_liner?(node)
52
52
 
@@ -29,7 +29,7 @@ module RuboCop
29
29
 
30
30
  MSG = 'Add an empty line after `%<example_group>s`.'
31
31
 
32
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
32
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
33
33
  return unless spec_group?(node)
34
34
 
35
35
  missing_separating_line_offense(node) do |method|
@@ -23,8 +23,13 @@ module RuboCop
23
23
 
24
24
  MSG = 'Add an empty line after the last `%<let>s`.'
25
25
 
26
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
27
- return unless example_group_with_body?(node)
26
+ # @!method example_group_or_include?(node)
27
+ def_node_matcher :example_group_or_include?, <<~PATTERN
28
+ (block (send #rspec? {#SharedGroups.all #ExampleGroups.all #Includes.all} ...) args $_)
29
+ PATTERN
30
+
31
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
32
+ return unless example_group_or_include?(node)
28
33
 
29
34
  final_let = node.body.child_nodes.reverse.find { |child| let?(child) }
30
35
 
@@ -68,6 +68,7 @@ module RuboCop
68
68
  end
69
69
 
70
70
  alias on_numblock on_block
71
+ alias on_itblock on_block
71
72
 
72
73
  private
73
74
 
@@ -22,7 +22,7 @@ module RuboCop
22
22
 
23
23
  MSG = 'Add an empty line after `%<subject>s`.'
24
24
 
25
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
25
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
26
26
  return unless subject?(node)
27
27
  return unless inside_example_group?(node)
28
28
 
@@ -59,7 +59,7 @@ module RuboCop
59
59
 
60
60
  LABEL = 'Example'
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?(node)
64
64
 
65
65
  check_code_length(node)
@@ -66,7 +66,7 @@ module RuboCop
66
66
  # @!method example_description(node)
67
67
  def_node_matcher :example_description, '(send nil? _ $(str $_))'
68
68
 
69
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
69
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
70
70
  return unless example?(node)
71
71
 
72
72
  check_example_without_description(node.send_node)
@@ -78,7 +78,7 @@ module RuboCop
78
78
  } ...) ...)
79
79
  PATTERN
80
80
 
81
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
81
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
82
82
  it_description(node) do |description_node, message|
83
83
  if message.match?(SHOULD_PREFIX)
84
84
  add_wording_offense(description_node, MSG_SHOULD)
@@ -19,12 +19,15 @@ module RuboCop
19
19
  # expect(name).to eq("John")
20
20
  #
21
21
  # # bad (not supported autocorrection)
22
+ # expect(42).to be_even
22
23
  # expect(false).to eq(true)
24
+ # expect("user").to be_present
23
25
  #
24
26
  class ExpectActual < Base
25
27
  extend AutoCorrector
26
28
 
27
29
  MSG = 'Provide the actual value you are testing to `expect(...)`.'
30
+ MSG_NO_ARG = 'Test a non-literal value with `expect(...)`.'
28
31
 
29
32
  RESTRICT_ON_SEND = Runners.all
30
33
 
@@ -65,26 +68,43 @@ module RuboCop
65
68
  )
66
69
  PATTERN
67
70
 
71
+ # @!method expect_literal_no_arg(node)
72
+ def_node_matcher :expect_literal_no_arg, <<~PATTERN
73
+ (send
74
+ (send nil? :expect $#literal?)
75
+ #Runners.all
76
+ $(send nil? $_)
77
+ )
78
+ PATTERN
79
+
68
80
  def on_send(node)
69
81
  expect_literal(node) do |actual, send_node, matcher, expected|
70
- next if SKIPPED_MATCHERS.include?(matcher)
71
-
72
- add_offense(actual) do |corrector|
73
- next unless CORRECTABLE_MATCHERS.include?(matcher)
74
- next if literal?(expected)
75
-
76
- corrector.replace(actual, expected.source)
77
- if matcher == :be
78
- corrector.replace(expected, actual.source)
79
- else
80
- corrector.replace(send_node, "#{matcher}(#{actual.source})")
81
- end
82
- end
82
+ register_offense(actual, send_node, matcher, expected)
83
+ end
84
+ expect_literal_no_arg(node) do |actual, send_node, matcher|
85
+ register_offense(actual, send_node, matcher, nil)
83
86
  end
84
87
  end
85
88
 
86
89
  private
87
90
 
91
+ def register_offense(actual, send_node, matcher, expected)
92
+ return if SKIPPED_MATCHERS.include?(matcher)
93
+
94
+ message = expected.nil? ? MSG_NO_ARG : MSG
95
+ add_offense(actual, message: message) do |corrector|
96
+ next unless CORRECTABLE_MATCHERS.include?(matcher)
97
+ next if expected.nil? || literal?(expected)
98
+
99
+ corrector.replace(actual, expected.source)
100
+ if matcher == :be
101
+ corrector.replace(expected, actual.source)
102
+ else
103
+ corrector.replace(send_node, "#{matcher}(#{actual.source})")
104
+ end
105
+ end
106
+ end
107
+
88
108
  # This is not implemented using a NodePattern because it seems
89
109
  # to not be able to match against an explicit (nil) sexp
90
110
  def literal?(node)
@@ -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,27 +88,47 @@ 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
89
102
  end
90
103
 
91
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
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
@@ -38,6 +38,7 @@ module RuboCop
38
38
  end
39
39
 
40
40
  alias on_numblock on_block
41
+ alias on_itblock on_block
41
42
 
42
43
  private
43
44
 
@@ -22,7 +22,7 @@ module RuboCop
22
22
  # @!method expectation(node)
23
23
  def_node_search :expectation, '(send nil? #Expectations.all ...)'
24
24
 
25
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
25
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
26
26
  return unless let?(node)
27
27
  return if node.body.nil?
28
28