Skip to content

Commit

Permalink
Merge pull request #925 from andrykonchin/speedup-subject-stub-cop
Browse files Browse the repository at this point in the history
RSpec/SubjectStub. Refactor and decrease complexity
  • Loading branch information
pirj committed Jun 11, 2020
2 parents 72f5b65 + 956efea commit 7f56f41
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 47 deletions.
72 changes: 25 additions & 47 deletions lib/rubocop/cop/rspec/subject_stub.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'set'

module RuboCop
module Cop
module RSpec
Expand Down Expand Up @@ -75,70 +77,46 @@ class SubjectStub < Cop

def on_block(node)
return unless example_group?(node)
return unless (processed_example_groups & node.ancestors).empty?

processed_example_groups << node
@explicit_subjects = find_all_explicit_subjects(node)

find_subject_stub(node) do |stub|
find_subject_expectations(node) do |stub|
add_offense(stub)
end
end

private

# Find subjects within tree and then find (send) nodes for that subject
#
# @param node [RuboCop::Node] example group
#
# @yield [RuboCop::Node] message expectations for subject
def find_subject_stub(node, &block)
find_subject(node) do |subject_name, context|
find_subject_expectation(context, subject_name, &block)
end
def processed_example_groups
@processed_example_groups ||= Set.new
end

# Find a subject message expectation
#
# @param node [RuboCop::Node]
# @param subject_name [Symbol] name of subject
#
# @yield [RuboCop::Node] message expectation
def find_subject_expectation(node, subject_name, &block)
# Do not search node if it is an example group with its own subject.
return if example_group?(node) && redefines_subject?(node)

# Yield the current node if it is a message expectation.
yield(node) if message_expectation?(node, subject_name)
def find_all_explicit_subjects(node)
node.each_descendant(:block).each_with_object({}) do |child, h|
name = subject(child)
next unless name

# Recurse through node's children looking for a message expectation.
node.each_child_node do |child|
find_subject_expectation(child, subject_name, &block)
end
end
outer_example_group = child.each_ancestor.find do |a|
example_group?(a)
end

# Check if node's children contain a subject definition
#
# @param node [RuboCop::Node]
#
# @return [Boolean]
def redefines_subject?(node)
node.each_child_node.any? do |child|
subject(child) || redefines_subject?(child)
h[outer_example_group] ||= []
h[outer_example_group] << name
end
end

# Find a subject definition
#
# @param node [RuboCop::Node]
# @param parent [RuboCop::Node,nil]
#
# @yieldparam subject_name [Symbol] name of subject being defined
# @yieldparam parent [RuboCop::Node] parent of subject definition
def find_subject(node, parent: nil, &block)
# An implicit subject is defined by RSpec when no subject is declared
subject_name = subject(node) || :subject
def find_subject_expectations(node, subject_names = [], &block)
subject_names = @explicit_subjects[node] if @explicit_subjects[node]

yield(subject_name, parent) if parent
expectation_detected = (subject_names + [:subject]).any? do |name|
message_expectation?(node, name)
end
return yield(node) if expectation_detected

node.each_child_node do |child|
find_subject(child, parent: node, &block)
find_subject_expectations(child, subject_names, &block)
end
end
end
Expand Down
20 changes: 20 additions & 0 deletions spec/rubocop/cop/rspec/subject_stub_spec.rb
Expand Up @@ -20,6 +20,26 @@
RUBY
end

it 'flags when subject is stubbed and there are several named subjects ' \
'in the same example group' do
expect_offense(<<-RUBY)
describe Foo do
subject(:foo) { described_class.new }
subject(:bar) { described_class.new }
subject(:baz) { described_class.new }
before do
allow(bar).to receive(:bar).and_return(baz)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not stub methods of the object under test.
end
it 'uses expect twice' do
expect(foo.bar).to eq(baz)
end
end
RUBY
end

it 'flags when subject is mocked' do
expect_offense(<<-RUBY)
describe Foo do
Expand Down

0 comments on commit 7f56f41

Please sign in to comment.