rubocop-rspec 3.6.0 → 3.8.0

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: 4e4727f136d1a399d3108fd010c76ac61f7fce57e3c89cb2d97ad121d558af44
4
- data.tar.gz: dcc8c7f61c420d5938807dae3a610f53c6be8c657687e38542f701db2aec4ce2
3
+ metadata.gz: abe85fc893e4ec59cb9b5f1e999d35bb41692feacc4dba579d135b3eefd6e4c7
4
+ data.tar.gz: 953ba29bf3fce28b79943e69039b638574d74432dac8b896f3dc8e589a20581c
5
5
  SHA512:
6
- metadata.gz: 651f8b53a0b70738f9dc084df5bdf471ce65ae4d7f0c2db8cc2eb557178fd541f317bee36ae062b8109aed72b452f69eb1c965db923d5d9875c86de9ab84f23e
7
- data.tar.gz: f189ec83b4b663df4e3aca303f2f273fded6d5529e689d7f9421f379dd46203da95236a4f649414466b0f6050b0d9e7d952df003b4b63ed45722c1764fd1a094
6
+ metadata.gz: acdf813781e2081ec34540e83a9a48fc289dab5cc8bff69d9610beb4a3f21c30955794eb4226d40b9f0460a04d38665c2aa4ce0857f35d8bd3a7325d91a861be
7
+ data.tar.gz: 752074b0fdce204596efc71e2358982c73c2fccff07da5da364c88b8ad5e99d5a8bfdbc855a9dd422169adeddf278266352f732092b5aa86d14502016ca77837
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 3.8.0 (2025-11-12)
6
+
7
+ - Add new cop `RSpec/LeakyLocalVariable`. ([@lovro-bikic])
8
+ - Bump RuboCop requirement to +1.81. ([@ydah])
9
+ - Fix a false positive for `RSpec/LetSetup` when `let!` used in outer scope. ([@ydah])
10
+ - Fix a false positive for `RSpec/ReceiveNever` cop when `allow(...).to receive(...).never`. ([@ydah])
11
+ - Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
12
+ - Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
13
+ - Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles], [@bquorning])
14
+
15
+ ## 3.7.0 (2025-09-01)
16
+
17
+ - Mark `RSpec/IncludeExamples` as `SafeAutoCorrect: false`. ([@yujideveloper])
18
+ - Fix a false positive for `RSpec/LeakyConstantDeclaration` when defining constants in explicit namespaces. ([@naveg])
19
+ - Add support for error matchers (`raise_exception` and `raise_error`) to `RSpec/Dialect`. ([@lovro-bikic])
20
+ - Don't register offenses for `RSpec/DescribedClass` within `Data.define` blocks. ([@lovro-bikic])
21
+ - Add autocorrection support for `RSpec/IteratedExpectation` for single expectations. ([@lovro-bikic])
22
+ - Exclude all cops from inspecting factorybot files, except if explicitly included. ([@Mth0158])
23
+ - Fix a false positive for `RSpec/ExcessiveDocstringSpacing` when receivers are not RSpec methods. ([@ydah])
24
+
5
25
  ## 3.6.0 (2025-04-18)
6
26
 
7
27
  - Fix false positive in `RSpec/Pending`, where it would mark the default block `it` as an offense. ([@bquorning])
@@ -1021,6 +1041,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
1021
1041
  [@mlarraz]: https://github.com/mlarraz
1022
1042
  [@mockdeep]: https://github.com/mockdeep
1023
1043
  [@mothonmars]: https://github.com/MothOnMars
1044
+ [@mth0158]: https://github.com/Mth0158
1024
1045
  [@mvz]: https://github.com/mvz
1025
1046
  [@naveg]: https://github.com/naveg
1026
1047
  [@nc-holodakg]: https://github.com/nc-holodakg
@@ -1066,6 +1087,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
1066
1087
  [@tmaier]: https://github.com/tmaier
1067
1088
  [@topalovic]: https://github.com/topalovic
1068
1089
  [@twalpole]: https://github.com/twalpole
1090
+ [@ushi-as]: https://github.com/ushi-as
1069
1091
  [@vzvu3k6k]: https://github.com/vzvu3k6k
1070
1092
  [@walf443]: https://github.com/walf443
1071
1093
  [@yasu551]: https://github.com/yasu551
@@ -1073,5 +1095,6 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
1073
1095
  [@ydah]: https://github.com/ydah
1074
1096
  [@yevhene]: https://github.com/yevhene
1075
1097
  [@ypresto]: https://github.com/ypresto
1098
+ [@yujideveloper]: https://github.com/yujideveloper
1076
1099
  [@zdennis]: https://github.com/zdennis
1077
1100
  [@zverok]: https://github.com/zverok
data/config/default.yml CHANGED
@@ -6,6 +6,10 @@ RSpec:
6
6
  Include:
7
7
  - "**/*_spec.rb"
8
8
  - "**/spec/**/*"
9
+ Exclude:
10
+ - "**/spec/factories.rb"
11
+ - "**/spec/factories/**/*.rb"
12
+ - "**/features/support/factories/**/*.rb"
9
13
  Language:
10
14
  inherit_mode:
11
15
  merge:
@@ -79,6 +83,9 @@ RSpec:
79
83
  - prepend_after
80
84
  - after
81
85
  - append_after
86
+ ErrorMatchers:
87
+ - raise_error
88
+ - raise_exception
82
89
  Includes:
83
90
  inherit_mode:
84
91
  merge:
@@ -535,7 +542,9 @@ RSpec/ImplicitSubject:
535
542
  RSpec/IncludeExamples:
536
543
  Description: Checks for usage of `include_examples`.
537
544
  Enabled: pending
545
+ SafeAutoCorrect: false
538
546
  VersionAdded: '3.6'
547
+ VersionChanged: '3.7'
539
548
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/IncludeExamples
540
549
 
541
550
  RSpec/IndexedLet:
@@ -601,6 +610,12 @@ RSpec/LeakyConstantDeclaration:
601
610
  StyleGuide: https://rspec.rubystyle.guide/#declare-constants
602
611
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyConstantDeclaration
603
612
 
613
+ RSpec/LeakyLocalVariable:
614
+ Description: Checks for local variables from outer scopes used inside examples.
615
+ Enabled: pending
616
+ VersionAdded: '3.8'
617
+ Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/LeakyLocalVariable
618
+
604
619
  RSpec/LetBeforeExamples:
605
620
  Description: Checks for `let` definitions that come after an example.
606
621
  Enabled: true
@@ -782,7 +797,7 @@ RSpec/ReceiveCounts:
782
797
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ReceiveCounts
783
798
 
784
799
  RSpec/ReceiveMessages:
785
- Description: Checks for multiple messages stubbed on the same object.
800
+ Description: Prefer `receive_messages` over multiple `receive`s on the same object.
786
801
  Enabled: true
787
802
  SafeAutoCorrect: false
788
803
  VersionAdded: '2.23'
@@ -924,7 +939,13 @@ RSpec/SpecFilePathFormat:
924
939
  IgnoreMethods: false
925
940
  IgnoreMetadata:
926
941
  type: routing
942
+ InflectorPath: "./config/initializers/inflections.rb"
943
+ SupportedInflectors:
944
+ - default
945
+ - active_support
946
+ EnforcedInflector: default
927
947
  VersionAdded: '2.24'
948
+ VersionChanged: '3.8'
928
949
  Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SpecFilePathFormat
929
950
 
930
951
  RSpec/SpecFilePathSuffix:
@@ -67,7 +67,7 @@ module RuboCop
67
67
 
68
68
  # @!method context_wording(node)
69
69
  def_node_matcher :context_wording, <<~PATTERN
70
- (block (send #rspec? { :context :shared_context } $({str dstr xstr} ...) ...) ...)
70
+ (block (send #rspec? { :context :shared_context } $(any_str ...) ...) ...)
71
71
  PATTERN
72
72
 
73
73
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
@@ -13,6 +13,20 @@ module RuboCop
13
13
  # `OnlyStaticConstants` is only relevant when `EnforcedStyle` is
14
14
  # `described_class`.
15
15
  #
16
+ # There's a known caveat with rspec-rails's `controller` helper that
17
+ # runs its block in a different context, and `described_class` is not
18
+ # available to it. `SkipBlocks` option excludes detection in all
19
+ # non-RSpec related blocks.
20
+ #
21
+ # To narrow down this setting to only a specific directory, it is
22
+ # possible to use an overriding configuration file local to that
23
+ # directory.
24
+ #
25
+ # @safety
26
+ # Autocorrection is unsafe when `SkipBlocks: false` because
27
+ # `described_class` might not be available within the block (for
28
+ # example, in rspec-rails's `controller` helper).
29
+ #
16
30
  # @example `EnforcedStyle: described_class` (default)
17
31
  # # bad
18
32
  # describe MyClass do
@@ -47,15 +61,6 @@ module RuboCop
47
61
  # subject { MyClass.do_something }
48
62
  # end
49
63
  #
50
- # There's a known caveat with rspec-rails's `controller` helper that
51
- # runs its block in a different context, and `described_class` is not
52
- # available to it. `SkipBlocks` option excludes detection in all
53
- # non-RSpec related blocks.
54
- #
55
- # To narrow down this setting to only a specific directory, it is
56
- # possible to use an overriding configuration file local to that
57
- # directory.
58
- #
59
64
  # @example `SkipBlocks: true`
60
65
  # # spec/controllers/.rubocop.yml
61
66
  # # RSpec/DescribedClass:
@@ -78,7 +83,13 @@ module RuboCop
78
83
 
79
84
  # @!method common_instance_exec_closure?(node)
80
85
  def_node_matcher :common_instance_exec_closure?, <<~PATTERN
81
- (block (send (const nil? {:Class :Module :Struct}) :new ...) ...)
86
+ (block
87
+ {
88
+ (send (const nil? {:Class :Module :Struct}) :new ...)
89
+ (send (const nil? :Data) :define ...)
90
+ }
91
+ ...
92
+ )
82
93
  PATTERN
83
94
 
84
95
  # @!method rspec_block?(node)
@@ -21,6 +21,7 @@ module RuboCop
21
21
  # - let, let!
22
22
  # - subject, subject!
23
23
  # - expect, is_expected, expect_any_instance_of
24
+ # - raise_error, raise_exception
24
25
  #
25
26
  # By default all of the RSpec methods and aliases are allowed. By setting
26
27
  # a config like:
@@ -10,8 +10,19 @@ module RuboCop
10
10
  #
11
11
  # @see http://betterspecs.org/#should
12
12
  #
13
- # The autocorrect is experimental - use with care! It can be configured
14
- # with CustomTransform (e.g. have => has) and IgnoredWords (e.g. only).
13
+ # @safety
14
+ # The autocorrect is experimental - use with care! It can be configured
15
+ # with CustomTransform (e.g. have => has) and IgnoredWords (e.g. only).
16
+ #
17
+ # While the autocorrect will not break your code (it only modifies test
18
+ # description strings, not the actual test logic), it may produce
19
+ # grammatically incorrect English in some cases. Always review the diff
20
+ # when using autocorrect to ensure the descriptions remain natural and
21
+ # accurate.
22
+ #
23
+ # This is not classified as an unsafe autocorrect because it does not
24
+ # affect code behavior, but manual review of changes is strongly
25
+ # recommended.
15
26
  #
16
27
  # Use the DisallowedExamples setting to prevent unclear or insufficient
17
28
  # descriptions. Please note that this config will not be treated as
@@ -30,7 +30,7 @@ module RuboCop
30
30
 
31
31
  # @!method example_description(node)
32
32
  def_node_matcher :example_description, <<~PATTERN
33
- (send _ {#Examples.all #ExampleGroups.all} ${
33
+ (send #rspec? {#Examples.all #ExampleGroups.all} ${
34
34
  $str
35
35
  $(dstr ({str dstr `sym} ...) ...)
36
36
  } ...)
@@ -12,6 +12,57 @@ module RuboCop
12
12
  #
13
13
  # Prefer using `it_behaves_like` instead.
14
14
  #
15
+ # @safety
16
+ # `include_examples` and `it_behaves_like` have different scoping
17
+ # behaviors.
18
+ # Changing `include_examples` to `it_behaves_like` creates a new
19
+ # context, altering setup dependencies, which can lead to unexpected
20
+ # test failures.
21
+ # Specifically, the scope of hooks (`before`, `after`, `around`)
22
+ # changes, which may prevent expected setup from being inherited
23
+ # correctly.
24
+ #
25
+ # Additionally, `let` and `subject` are affected by scoping rules.
26
+ # When `include_examples` is used, `let` and `subject` defined within
27
+ # `shared_examples` are evaluated in the caller's context, allowing
28
+ # access to their values.
29
+ # In contrast, `it_behaves_like` creates a new context, preventing
30
+ # access to `let` or `subject` values from the caller's context.
31
+ #
32
+ # [source,ruby]
33
+ # ----
34
+ # shared_examples "mock behavior" do
35
+ # before do
36
+ # allow(service).to receive(:call).and_return("mocked response")
37
+ # end
38
+ #
39
+ # it "returns mocked response" do
40
+ # expect(service.call).to eq "mocked response"
41
+ # end
42
+ # end
43
+ #
44
+ # context "working example with include_examples" do
45
+ # let(:service) { double(:service) }
46
+ #
47
+ # include_examples "mock behavior"
48
+ #
49
+ # it "uses the mocked service" do
50
+ # expect(service.call).to eq "mocked response" # Passes
51
+ # end
52
+ # end
53
+ #
54
+ # context "broken example with it_behaves_like" do
55
+ # let(:service) { double(:service) }
56
+ #
57
+ # it_behaves_like "mock behavior"
58
+ #
59
+ # it "unexpectedly does not use the mocked service" do
60
+ # # Fails because `it_behaves_like` does not apply the mock setup
61
+ # expect(service.call).to eq "mocked response"
62
+ # end
63
+ # end
64
+ # ----
65
+ #
15
66
  # @example
16
67
  # # bad
17
68
  # include_examples 'examples'
@@ -17,6 +17,8 @@ module RuboCop
17
17
  # end
18
18
  #
19
19
  class IteratedExpectation < Base
20
+ extend AutoCorrector
21
+
20
22
  MSG = 'Prefer using the `all` matcher instead ' \
21
23
  'of iterating over an array.'
22
24
 
@@ -25,14 +27,14 @@ module RuboCop
25
27
  (block
26
28
  (send ... :each)
27
29
  (args (arg $_))
28
- $(...)
30
+ (...)
29
31
  )
30
32
  PATTERN
31
33
 
32
34
  # @!method each_numblock?(node)
33
35
  def_node_matcher :each_numblock?, <<~PATTERN
34
36
  (numblock
35
- (send ... :each) _ $(...)
37
+ (send ... :each) _ (...)
36
38
  )
37
39
  PATTERN
38
40
 
@@ -42,23 +44,43 @@ module RuboCop
42
44
  PATTERN
43
45
 
44
46
  def on_block(node)
45
- each?(node) do |arg, body|
46
- if single_expectation?(body, arg) || only_expectations?(body, arg)
47
- add_offense(node.send_node)
48
- end
47
+ each?(node) do |arg|
48
+ check_offense(node, arg)
49
49
  end
50
50
  end
51
51
 
52
52
  def on_numblock(node)
53
- each_numblock?(node) do |body|
54
- if single_expectation?(body, :_1) || only_expectations?(body, :_1)
55
- add_offense(node.send_node)
56
- end
53
+ each_numblock?(node) do
54
+ check_offense(node, :_1)
57
55
  end
58
56
  end
59
57
 
60
58
  private
61
59
 
60
+ def check_offense(node, argument)
61
+ if single_expectation?(node.body, argument)
62
+ add_offense(node.send_node) do |corrector|
63
+ next unless node.body.arguments.one?
64
+ next if uses_argument_in_matcher?(node, argument)
65
+
66
+ corrector.replace(node, single_expectation_replacement(node))
67
+ end
68
+ elsif only_expectations?(node.body, argument)
69
+ add_offense(node.send_node)
70
+ end
71
+ end
72
+
73
+ def single_expectation_replacement(node)
74
+ collection = node.receiver.source
75
+ matcher = node.body.first_argument.source
76
+
77
+ "expect(#{collection}).to all(#{matcher})"
78
+ end
79
+
80
+ def uses_argument_in_matcher?(node, argument)
81
+ node.body.first_argument.each_descendant.any?(s(:lvar, argument))
82
+ end
83
+
62
84
  def single_expectation?(body, arg)
63
85
  expectation?(body, arg)
64
86
  end
@@ -100,18 +100,21 @@ module RuboCop
100
100
 
101
101
  def on_casgn(node)
102
102
  return unless inside_describe_block?(node)
103
+ return if explicit_namespace?(node.namespace)
103
104
 
104
105
  add_offense(node, message: MSG_CONST)
105
106
  end
106
107
 
107
108
  def on_class(node)
108
109
  return unless inside_describe_block?(node)
110
+ return if explicit_namespace?(node.identifier.namespace)
109
111
 
110
112
  add_offense(node, message: MSG_CLASS)
111
113
  end
112
114
 
113
115
  def on_module(node)
114
116
  return unless inside_describe_block?(node)
117
+ return if explicit_namespace?(node.identifier.namespace)
115
118
 
116
119
  add_offense(node, message: MSG_MODULE)
117
120
  end
@@ -121,6 +124,10 @@ module RuboCop
121
124
  def inside_describe_block?(node)
122
125
  node.each_ancestor(:block).any? { |ancestor| spec_group?(ancestor) }
123
126
  end
127
+
128
+ def explicit_namespace?(namespace)
129
+ !namespace.nil?
130
+ end
124
131
  end
125
132
  end
126
133
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ # Checks for local variables from outer scopes used inside examples.
7
+ #
8
+ # Local variables assigned outside an example but used within it act
9
+ # as shared state, which can make tests non-deterministic.
10
+ #
11
+ # @example
12
+ # # bad - outside variable used in a hook
13
+ # user = create(:user)
14
+ #
15
+ # before { user.update(admin: true) }
16
+ #
17
+ # # good
18
+ # let(:user) { create(:user) }
19
+ #
20
+ # before { user.update(admin: true) }
21
+ #
22
+ # # bad - outside variable used in an example
23
+ # user = create(:user)
24
+ #
25
+ # it 'is persisted' do
26
+ # expect(user).to be_persisted
27
+ # end
28
+ #
29
+ # # good
30
+ # let(:user) { create(:user) }
31
+ #
32
+ # it 'is persisted' do
33
+ # expect(user).to be_persisted
34
+ # end
35
+ #
36
+ # # also good - assigning the variable within the example
37
+ # it 'is persisted' do
38
+ # user = create(:user)
39
+ #
40
+ # expect(user).to be_persisted
41
+ # end
42
+ #
43
+ # # bad - outside variable passed to included examples
44
+ # attrs = ['foo', 'bar']
45
+ #
46
+ # it_behaves_like 'some examples', attrs
47
+ #
48
+ # # good
49
+ # it_behaves_like 'some examples' do
50
+ # let(:attrs) { ['foo', 'bar'] }
51
+ # end
52
+ #
53
+ # # good - when variable is used only as example description
54
+ # attribute = 'foo'
55
+ #
56
+ # it "#{attribute} is persisted" do
57
+ # expectations
58
+ # end
59
+ #
60
+ # # good - when variable is used only to include other examples
61
+ # examples = foo ? 'some examples' : 'other examples'
62
+ #
63
+ # it_behaves_like examples, another_argument
64
+ #
65
+ class LeakyLocalVariable < Base
66
+ MSG = 'Do not use local variables defined outside of ' \
67
+ 'examples inside of them.'
68
+
69
+ # @!method example_method?(node)
70
+ def_node_matcher :example_method?, <<~PATTERN
71
+ (send nil? #Examples.all _)
72
+ PATTERN
73
+
74
+ # @!method includes_method?(node)
75
+ def_node_matcher :includes_method?, <<~PATTERN
76
+ (send nil? #Includes.all ...)
77
+ PATTERN
78
+
79
+ def self.joining_forces
80
+ VariableForce
81
+ end
82
+
83
+ def after_leaving_scope(scope, _variable_table)
84
+ scope.variables.each_value { |variable| check_references(variable) }
85
+ end
86
+
87
+ private
88
+
89
+ def check_references(variable)
90
+ variable.assignments.each do |assignment|
91
+ next if part_of_example_scope?(assignment.node)
92
+
93
+ assignment.references.each do |reference|
94
+ next unless inside_describe_block?(reference)
95
+ next unless part_of_example_scope?(reference)
96
+ next if allowed_reference?(reference)
97
+
98
+ add_offense(assignment.node)
99
+ end
100
+ end
101
+ end
102
+
103
+ def allowed_reference?(node)
104
+ node.each_ancestor.any? do |ancestor|
105
+ next true if example_method?(ancestor)
106
+ if includes_method?(ancestor)
107
+ next allowed_includes_arguments?(ancestor, node)
108
+ end
109
+
110
+ false
111
+ end
112
+ end
113
+
114
+ def allowed_includes_arguments?(node, argument)
115
+ node.arguments[1..].all? do |argument_node|
116
+ next true if argument_node.type?(:dstr, :dsym)
117
+
118
+ argument_node != argument &&
119
+ argument_node.each_descendant.none?(argument)
120
+ end
121
+ end
122
+
123
+ def part_of_example_scope?(node)
124
+ node.each_ancestor.any? { |ancestor| example_scope?(ancestor) }
125
+ end
126
+
127
+ def example_scope?(node)
128
+ subject?(node) || let?(node) || hook?(node) || example?(node) ||
129
+ include?(node)
130
+ end
131
+
132
+ def inside_describe_block?(node)
133
+ node.each_ancestor(:block).any? { |ancestor| spec_group?(ancestor) }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -25,6 +25,18 @@ module RuboCop
25
25
  # it 'counts widgets' do
26
26
  # expect(Widget.count).to eq(1)
27
27
  # end
28
+ #
29
+ # # good
30
+ # describe 'a widget' do
31
+ # let!(:my_widget) { create(:widget) }
32
+ # context 'when visiting its page' do
33
+ # let!(:my_widget) { create(:widget, name: 'Special') }
34
+ # it 'counts widgets' do
35
+ # expect(Widget.count).to eq(1)
36
+ # end
37
+ # end
38
+ # end
39
+ #
28
40
  class LetSetup < Base
29
41
  MSG = 'Do not use `let!` to setup objects not referenced in tests.'
30
42
 
@@ -59,6 +71,8 @@ module RuboCop
59
71
 
60
72
  def unused_let_bang(node)
61
73
  child_let_bang(node) do |method_send, method_name|
74
+ next if overrides_outer_let_bang?(node, method_name)
75
+
62
76
  yield(method_send) unless method_called?(node, method_name.to_sym)
63
77
  end
64
78
  end
@@ -68,6 +82,22 @@ module RuboCop
68
82
  let_bang(let, &block)
69
83
  end
70
84
  end
85
+
86
+ def overrides_outer_let_bang?(node, method_name)
87
+ node.each_ancestor(:block).any? do |ancestor|
88
+ next unless example_or_shared_group_or_including?(ancestor)
89
+
90
+ outer_let_bang?(ancestor, method_name)
91
+ end
92
+ end
93
+
94
+ def outer_let_bang?(ancestor_node, method_name)
95
+ RuboCop::RSpec::ExampleGroup.new(ancestor_node).lets.any? do |let|
96
+ let_bang(let) do |_send, name|
97
+ name == method_name
98
+ end
99
+ end
100
+ end
71
101
  end
72
102
  end
73
103
  end
@@ -7,7 +7,7 @@ module RuboCop
7
7
  module FinalEndLocation
8
8
  def final_end_location(start_node)
9
9
  heredoc_endings =
10
- start_node.each_node(:str, :dstr, :xstr)
10
+ start_node.each_node(:any_str)
11
11
  .select(&:heredoc?)
12
12
  .map { |node| node.loc.heredoc_end }
13
13
 
@@ -13,7 +13,7 @@ module RuboCop
13
13
  # @!method variable_definition?(node)
14
14
  def_node_matcher :variable_definition?, <<~PATTERN
15
15
  (send nil? {#Subjects.all #Helpers.all}
16
- $({sym str dsym dstr} ...) ...)
16
+ $({any_sym str dstr} ...) ...)
17
17
  PATTERN
18
18
  end
19
19
  end
@@ -181,9 +181,7 @@ module RuboCop
181
181
  end
182
182
 
183
183
  def heredoc_argument?(matcher)
184
- matcher.arguments.select do |arg|
185
- arg.type?(:str, :dstr, :xstr)
186
- end.any?(&:heredoc?)
184
+ matcher.arguments.select(&:any_str_type?).any?(&:heredoc?)
187
185
  end
188
186
 
189
187
  # @!method predicate_matcher?(node)
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- # Checks for multiple messages stubbed on the same object.
6
+ # Prefer `receive_messages` over multiple `receive`s on the same object.
7
7
  #
8
8
  # @safety
9
9
  # The autocorrection is marked as unsafe, because it may change the
@@ -5,6 +5,11 @@ module RuboCop
5
5
  module RSpec
6
6
  # Prefer `not_to receive(...)` over `receive(...).never`.
7
7
  #
8
+ # This cop only flags usage with `expect`. It ignores `allow` because
9
+ # `allow(...).to receive(...).never` is a valid way to ensure a method
10
+ # is not called, while `allow(...).not_to receive(...)` would have
11
+ # different semantics.
12
+ #
8
13
  # @example
9
14
  # # bad
10
15
  # expect(foo).to receive(:bar).never
@@ -12,6 +17,9 @@ module RuboCop
12
17
  # # good
13
18
  # expect(foo).not_to receive(:bar)
14
19
  #
20
+ # # not flagged by this cop
21
+ # allow(foo).to receive(:bar).never
22
+ #
15
23
  class ReceiveNever < Base
16
24
  extend AutoCorrector
17
25
  MSG = 'Use `not_to receive` instead of `never`.'
@@ -20,8 +28,20 @@ module RuboCop
20
28
  # @!method method_on_stub?(node)
21
29
  def_node_search :method_on_stub?, '(send nil? :receive ...)'
22
30
 
31
+ # @!method expect_to_receive?(node)
32
+ def_node_matcher :expect_to_receive?, <<~PATTERN
33
+ (send
34
+ {
35
+ (send #rspec? {:expect :expect_any_instance_of} ...)
36
+ (block (send #rspec? :expect) ...)
37
+ (send nil? :is_expected)
38
+ }
39
+ :to ...)
40
+ PATTERN
41
+
23
42
  def on_send(node)
24
43
  return unless node.method?(:never) && method_on_stub?(node)
44
+ return unless used_with_expect?(node)
25
45
 
26
46
  add_offense(node.loc.selector) do |corrector|
27
47
  autocorrect(corrector, node)
@@ -30,6 +50,12 @@ module RuboCop
30
50
 
31
51
  private
32
52
 
53
+ def used_with_expect?(node)
54
+ node.each_ancestor(:send).any? do |ancestor|
55
+ expect_to_receive?(ancestor)
56
+ end
57
+ end
58
+
33
59
  def autocorrect(corrector, node)
34
60
  corrector.replace(node.parent.loc.selector, 'not_to')
35
61
  range = node.loc.dot.with(end_pos: node.loc.selector.end_pos)
@@ -16,36 +16,55 @@ module RuboCop
16
16
  # end
17
17
  #
18
18
  class RepeatedExample < Base
19
- MSG = "Don't repeat examples within an example group."
19
+ MSG = "Don't repeat examples within an example group. " \
20
+ 'Repeated on line(s) %<lines>s.'
20
21
 
21
22
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
22
23
  return unless example_group?(node)
23
24
 
24
- repeated_examples(node).each do |repeated_example|
25
- add_offense(repeated_example)
25
+ find_repeated_examples(node).each do |repeated_examples|
26
+ add_offenses_for_repeated_group(repeated_examples)
26
27
  end
27
28
  end
28
29
 
29
30
  private
30
31
 
31
- def repeated_examples(node)
32
- RuboCop::RSpec::ExampleGroup.new(node)
33
- .examples
34
- .group_by { |example| example_signature(example) }
32
+ def find_repeated_examples(node)
33
+ examples = RuboCop::RSpec::ExampleGroup.new(node).examples
34
+
35
+ examples
36
+ .group_by { |example| build_example_signature(example) }
35
37
  .values
36
- .reject(&:one?)
37
- .flatten
38
- .map(&:to_node)
38
+ .select { |group| group.size > 1 }
39
39
  end
40
40
 
41
- def example_signature(example)
42
- key_parts = [example.metadata, example.implementation]
43
-
41
+ def build_example_signature(example)
42
+ signature = [example.metadata, example.implementation]
44
43
  if example.definition.method?(:its)
45
- key_parts << example.definition.arguments
44
+ signature << example.definition.arguments
45
+ end
46
+ signature
47
+ end
48
+
49
+ def add_offenses_for_repeated_group(repeated_examples)
50
+ repeated_examples.each do |example|
51
+ other_lines = extract_other_lines(repeated_examples, example)
52
+ add_offense(example.to_node, message: message(other_lines))
46
53
  end
54
+ end
55
+
56
+ def extract_other_lines(examples_group, current_example)
57
+ current_node = current_example.to_node
58
+
59
+ examples_group
60
+ .reject { |ex| ex.to_node.equal?(current_node) }
61
+ .map { |ex| ex.to_node.first_line }
62
+ .uniq
63
+ .sort
64
+ end
47
65
 
48
- key_parts
66
+ def message(other_lines)
67
+ format(MSG, lines: other_lines.join(', '))
49
68
  end
50
69
  end
51
70
  end
@@ -30,7 +30,7 @@ module RuboCop
30
30
 
31
31
  # @!method match_ambiguous_trailing_metadata?(node)
32
32
  def_node_matcher :match_ambiguous_trailing_metadata?, <<~PATTERN
33
- (send _ _ _ ... !{hash sym str dstr xstr})
33
+ (send _ _ _ ... !{hash sym any_str})
34
34
  PATTERN
35
35
 
36
36
  def on_metadata(args, hash)
@@ -32,6 +32,12 @@ module RuboCop
32
32
  # # good
33
33
  # whatever_spec.rb # describe MyClass, type: :routing do; end
34
34
  #
35
+ # @example `EnforcedInflector: active_support`
36
+ # # Enable to use ActiveSupport's inflector for custom acronyms
37
+ # # like HTTP, etc. Set to "default" by default.
38
+ # # Configure `InflectorPath` with the path to the inflector file.
39
+ # # The default is ./config/initializers/inflections.rb.
40
+ #
35
41
  class SpecFilePathFormat < Base
36
42
  include TopLevelGroup
37
43
  include Namespace
@@ -59,6 +65,53 @@ module RuboCop
59
65
 
60
66
  private
61
67
 
68
+ # Inflector module that uses ActiveSupport for advanced inflection rules
69
+ module ActiveSupportInflector
70
+ def self.call(string)
71
+ ActiveSupport::Inflector.underscore(string)
72
+ end
73
+
74
+ def self.prepare_availability(config)
75
+ return if @prepared
76
+
77
+ @prepared = true
78
+
79
+ inflector_path = config.fetch('InflectorPath')
80
+
81
+ unless File.exist?(inflector_path)
82
+ raise "The configured `InflectorPath` #{inflector_path} does " \
83
+ 'not exist.'
84
+ end
85
+
86
+ require 'active_support/inflector'
87
+ require inflector_path
88
+ end
89
+ end
90
+
91
+ # Inflector module that uses basic regex-based conversion
92
+ module DefaultInflector
93
+ def self.call(string)
94
+ string
95
+ .gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
96
+ .gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
97
+ .downcase
98
+ end
99
+ end
100
+
101
+ def inflector
102
+ case cop_config.fetch('EnforcedInflector')
103
+ when 'active_support'
104
+ ActiveSupportInflector.prepare_availability(cop_config)
105
+ ActiveSupportInflector
106
+ when 'default'
107
+ DefaultInflector
108
+ else
109
+ # :nocov:
110
+ :noop
111
+ # :nocov:
112
+ end
113
+ end
114
+
62
115
  def ensure_correct_file_path(send_node, class_name, arguments)
63
116
  pattern = correct_path_pattern(class_name, arguments)
64
117
  return if filename_ends_with?(pattern)
@@ -106,10 +159,7 @@ module RuboCop
106
159
  end
107
160
 
108
161
  def camel_to_snake_case(string)
109
- string
110
- .gsub(/([^A-Z])([A-Z]+)/, '\1_\2')
111
- .gsub(/([A-Z])([A-Z][^A-Z\d]+)/, '\1_\2')
112
- .downcase
162
+ inflector.call(string)
113
163
  end
114
164
 
115
165
  def custom_transform
@@ -60,16 +60,8 @@ module RuboCop
60
60
  end
61
61
 
62
62
  def style_offense?(variable)
63
- (style == :symbols && string?(variable)) ||
64
- (style == :strings && symbol?(variable))
65
- end
66
-
67
- def string?(node)
68
- node.str_type?
69
- end
70
-
71
- def symbol?(node)
72
- node.type?(:sym, :dsym)
63
+ (style == :symbols && variable.str_type?) ||
64
+ (style == :strings && variable.any_sym_type?)
73
65
  end
74
66
  end
75
67
  end
@@ -10,12 +10,12 @@ module RuboCop
10
10
  # @example
11
11
  # # bad
12
12
  # let(:foo) do
13
- # double(method_name: 'returned value')
13
+ # double("ClassName", method_name: 'returned value')
14
14
  # end
15
15
  #
16
16
  # # bad
17
17
  # let(:foo) do
18
- # double("ClassName", method_name: 'returned value')
18
+ # spy("ClassName", method_name: 'returned value')
19
19
  # end
20
20
  #
21
21
  # # good
@@ -23,6 +23,50 @@ module RuboCop
23
23
  # instance_double("ClassName", method_name: 'returned value')
24
24
  # end
25
25
  #
26
+ # # good
27
+ # let(:foo) do
28
+ # class_double("ClassName", method_name: 'returned value')
29
+ # end
30
+ #
31
+ # # good
32
+ # let(:foo) do
33
+ # object_double("some object", method_name: 'returned value')
34
+ # end
35
+ #
36
+ # @example `IgnoreNameless: true (default)`
37
+ # # good
38
+ # let(:foo) do
39
+ # double(method_name: 'returned value')
40
+ # end
41
+ #
42
+ # # good
43
+ # let(:foo) do
44
+ # double
45
+ # end
46
+ #
47
+ # @example `IgnoreNameless: false`
48
+ # # bad
49
+ # let(:foo) do
50
+ # double(method_name: 'returned value')
51
+ # end
52
+ #
53
+ # # bad
54
+ # let(:foo) do
55
+ # double
56
+ # end
57
+ #
58
+ # @example `IgnoreSymbolicNames: false (default)`
59
+ # # bad
60
+ # let(:foo) do
61
+ # double(:foo)
62
+ # end
63
+ #
64
+ # @example `IgnoreSymbolicNames: true`
65
+ # # good
66
+ # let(:foo) do
67
+ # double(:foo)
68
+ # end
69
+ #
26
70
  class VerifiedDoubles < Base
27
71
  MSG = 'Prefer using verifying doubles over normal doubles.'
28
72
  RESTRICT_ON_SEND = %i[double spy].freeze
@@ -34,7 +78,7 @@ module RuboCop
34
78
 
35
79
  def on_send(node)
36
80
  unverified_double(node) do |name, *_args|
37
- return if name.nil? && cop_config['IgnoreNameless']
81
+ return if (name.nil? || hash?(name)) && cop_config['IgnoreNameless']
38
82
  return if symbol?(name) && cop_config['IgnoreSymbolicNames']
39
83
 
40
84
  add_offense(node)
@@ -46,6 +90,10 @@ module RuboCop
46
90
  def symbol?(name)
47
91
  name&.sym_type?
48
92
  end
93
+
94
+ def hash?(arg)
95
+ arg.hash_type?
96
+ end
49
97
  end
50
98
  end
51
99
  end
@@ -57,6 +57,7 @@ require_relative 'rspec/it_behaves_like'
57
57
  require_relative 'rspec/iterated_expectation'
58
58
  require_relative 'rspec/leading_subject'
59
59
  require_relative 'rspec/leaky_constant_declaration'
60
+ require_relative 'rspec/leaky_local_variable'
60
61
  require_relative 'rspec/let_before_examples'
61
62
  require_relative 'rspec/let_setup'
62
63
  require_relative 'rspec/match_array'
@@ -72,6 +72,12 @@ module RuboCop
72
72
  # @!method subject?(node)
73
73
  def_node_matcher :subject?, '(block (send nil? #Subjects.all ...) ...)'
74
74
 
75
+ module ErrorMatchers # :nodoc:
76
+ def self.all(element)
77
+ Language.config['ErrorMatchers'].include?(element.to_s)
78
+ end
79
+ end
80
+
75
81
  module ExampleGroups # :nodoc:
76
82
  class << self
77
83
  def all(element)
@@ -200,14 +206,14 @@ module RuboCop
200
206
  # This is used in Dialect and DescribeClass cops to detect RSpec blocks.
201
207
  module ALL # :nodoc:
202
208
  def self.all(element)
203
- [ExampleGroups, Examples, Expectations, Helpers, Hooks, Includes,
204
- Runners, SharedGroups, Subjects]
209
+ [ErrorMatchers, ExampleGroups, Examples, Expectations, Helpers, Hooks,
210
+ Includes, Runners, SharedGroups, Subjects]
205
211
  .find { |concept| concept.all(element) }
206
212
  end
207
213
  end
208
214
 
209
- private_constant :ExampleGroups, :Examples, :Expectations, :Hooks,
210
- :Includes, :Runners, :SharedGroups, :ALL
215
+ private_constant :ErrorMatchers, :ExampleGroups, :Examples, :Expectations,
216
+ :Hooks, :Includes, :Runners, :SharedGroups, :ALL
211
217
  end
212
218
  end
213
219
  end
@@ -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.6.0'
7
+ STRING = '3.8.0'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.0
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Backus
@@ -31,20 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '1.72'
35
- - - ">="
36
- - !ruby/object:Gem::Version
37
- version: 1.72.1
34
+ version: '1.81'
38
35
  type: :runtime
39
36
  prerelease: false
40
37
  version_requirements: !ruby/object:Gem::Requirement
41
38
  requirements:
42
39
  - - "~>"
43
40
  - !ruby/object:Gem::Version
44
- version: '1.72'
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: 1.72.1
41
+ version: '1.81'
48
42
  description: |
49
43
  Code style checking for RSpec files.
50
44
  A plugin for the RuboCop code style enforcing & linting tool.
@@ -123,6 +117,7 @@ files:
123
117
  - lib/rubocop/cop/rspec/iterated_expectation.rb
124
118
  - lib/rubocop/cop/rspec/leading_subject.rb
125
119
  - lib/rubocop/cop/rspec/leaky_constant_declaration.rb
120
+ - lib/rubocop/cop/rspec/leaky_local_variable.rb
126
121
  - lib/rubocop/cop/rspec/let_before_examples.rb
127
122
  - lib/rubocop/cop/rspec/let_setup.rb
128
123
  - lib/rubocop/cop/rspec/match_array.rb
@@ -227,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
222
  - !ruby/object:Gem::Version
228
223
  version: '0'
229
224
  requirements: []
230
- rubygems_version: 3.6.7
225
+ rubygems_version: 3.6.9
231
226
  specification_version: 4
232
227
  summary: Code style checking for RSpec files
233
228
  test_files: []