Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSpec/SubjectStub. Refactor and decrease complexity #925

Merged
merged 6 commits into from Jun 11, 2020
72 changes: 21 additions & 51 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 All @@ -20,6 +22,8 @@ module RSpec
# end
#
class SubjectStub < Cop
include RuboCop::RSpec::TopLevelDescribe

MSG = 'Do not stub methods of the object under test.'

# @!method subject(node)
Expand Down Expand Up @@ -73,72 +77,38 @@ class SubjectStub < Cop
} ...)
PATTERN

def on_block(node)
return unless example_group?(node)
def on_top_level_describe(node, _args)
@explicit_subjects = find_all_explicit_subjects(node.parent)
Darhazer marked this conversation as resolved.
Show resolved Hide resolved

find_subject_stub(node) do |stub|
find_subject_expectations(node.parent) 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)
def find_all_explicit_subjects(node)
node.each_descendant(:block).each_with_object({}) do |child, h|
name = subject(child)
if name
h[child.parent.parent] ||= []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the meaning of parent.parent? isn't it node.parent? while you are attaching to the parent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent should be a block node for outer describe/context/...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it work properly for contexts defined in iterators? (yes, another funny concept from my current project)

RSpec.describe do
  %i[one two].each do |count|
    context "for #{count}" do
      specify do
        expect(subject).to receive(count).and_return(0)

There's a parent block, but not an example group block.
I'm not sure this is the example to fail, hope you can figure out a better example to fail .parent.parent.

h[child.parent.parent] << name
end
end
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)

# Recurse through node's children looking for a message expectation.
node.each_child_node do |child|
find_subject_expectation(child, subject_name, &block)
def find_subject_expectations(node, subject_names = [], &block)
if example_group?(node) && @explicit_subjects[node]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well I guess you can just check if there are explicit_subjects for this node, as if it is not an example group, there won't be any anyway. That could simplify the code

subject_names = @explicit_subjects[node]
end
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)
expectation_detected = (subject_names + [:subject]).any? do |name|
message_expectation?(node, 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

yield(subject_name, parent) if parent
return yield(node) if expectation_detected
pirj marked this conversation as resolved.
Show resolved Hide resolved

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', :wip 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