Skip to content

Commit 7f56f41

Browse files
authored
Merge pull request #925 from andrykonchin/speedup-subject-stub-cop
RSpec/SubjectStub. Refactor and decrease complexity
2 parents 72f5b65 + 956efea commit 7f56f41

File tree

2 files changed

+45
-47
lines changed

2 files changed

+45
-47
lines changed

lib/rubocop/cop/rspec/subject_stub.rb

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'set'
4+
35
module RuboCop
46
module Cop
57
module RSpec
@@ -75,70 +77,46 @@ class SubjectStub < Cop
7577

7678
def on_block(node)
7779
return unless example_group?(node)
80+
return unless (processed_example_groups & node.ancestors).empty?
81+
82+
processed_example_groups << node
83+
@explicit_subjects = find_all_explicit_subjects(node)
7884

79-
find_subject_stub(node) do |stub|
85+
find_subject_expectations(node) do |stub|
8086
add_offense(stub)
8187
end
8288
end
8389

8490
private
8591

86-
# Find subjects within tree and then find (send) nodes for that subject
87-
#
88-
# @param node [RuboCop::Node] example group
89-
#
90-
# @yield [RuboCop::Node] message expectations for subject
91-
def find_subject_stub(node, &block)
92-
find_subject(node) do |subject_name, context|
93-
find_subject_expectation(context, subject_name, &block)
94-
end
92+
def processed_example_groups
93+
@processed_example_groups ||= Set.new
9594
end
9695

97-
# Find a subject message expectation
98-
#
99-
# @param node [RuboCop::Node]
100-
# @param subject_name [Symbol] name of subject
101-
#
102-
# @yield [RuboCop::Node] message expectation
103-
def find_subject_expectation(node, subject_name, &block)
104-
# Do not search node if it is an example group with its own subject.
105-
return if example_group?(node) && redefines_subject?(node)
106-
107-
# Yield the current node if it is a message expectation.
108-
yield(node) if message_expectation?(node, subject_name)
96+
def find_all_explicit_subjects(node)
97+
node.each_descendant(:block).each_with_object({}) do |child, h|
98+
name = subject(child)
99+
next unless name
109100

110-
# Recurse through node's children looking for a message expectation.
111-
node.each_child_node do |child|
112-
find_subject_expectation(child, subject_name, &block)
113-
end
114-
end
101+
outer_example_group = child.each_ancestor.find do |a|
102+
example_group?(a)
103+
end
115104

116-
# Check if node's children contain a subject definition
117-
#
118-
# @param node [RuboCop::Node]
119-
#
120-
# @return [Boolean]
121-
def redefines_subject?(node)
122-
node.each_child_node.any? do |child|
123-
subject(child) || redefines_subject?(child)
105+
h[outer_example_group] ||= []
106+
h[outer_example_group] << name
124107
end
125108
end
126109

127-
# Find a subject definition
128-
#
129-
# @param node [RuboCop::Node]
130-
# @param parent [RuboCop::Node,nil]
131-
#
132-
# @yieldparam subject_name [Symbol] name of subject being defined
133-
# @yieldparam parent [RuboCop::Node] parent of subject definition
134-
def find_subject(node, parent: nil, &block)
135-
# An implicit subject is defined by RSpec when no subject is declared
136-
subject_name = subject(node) || :subject
110+
def find_subject_expectations(node, subject_names = [], &block)
111+
subject_names = @explicit_subjects[node] if @explicit_subjects[node]
137112

138-
yield(subject_name, parent) if parent
113+
expectation_detected = (subject_names + [:subject]).any? do |name|
114+
message_expectation?(node, name)
115+
end
116+
return yield(node) if expectation_detected
139117

140118
node.each_child_node do |child|
141-
find_subject(child, parent: node, &block)
119+
find_subject_expectations(child, subject_names, &block)
142120
end
143121
end
144122
end

spec/rubocop/cop/rspec/subject_stub_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@
2020
RUBY
2121
end
2222

23+
it 'flags when subject is stubbed and there are several named subjects ' \
24+
'in the same example group' do
25+
expect_offense(<<-RUBY)
26+
describe Foo do
27+
subject(:foo) { described_class.new }
28+
subject(:bar) { described_class.new }
29+
subject(:baz) { described_class.new }
30+
31+
before do
32+
allow(bar).to receive(:bar).and_return(baz)
33+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not stub methods of the object under test.
34+
end
35+
36+
it 'uses expect twice' do
37+
expect(foo.bar).to eq(baz)
38+
end
39+
end
40+
RUBY
41+
end
42+
2343
it 'flags when subject is mocked' do
2444
expect_offense(<<-RUBY)
2545
describe Foo do

0 commit comments

Comments
 (0)