Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions lib/solargraph/rspec/convention.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative 'correctors/let_methods_corrector'
require_relative 'correctors/subject_method_corrector'
require_relative 'correctors/dsl_methods_corrector'
require_relative 'correctors/shared_examples_corrector'
require_relative 'test_helpers'
require_relative 'pin_factory'

Expand All @@ -33,6 +34,15 @@ module Rspec
pending
].freeze

SHARED_EXAMPLE_INCLUSION_METHODS = %w[
include_examples
it_behaves_like
it_should_behave_like
include_context
].freeze

SHARED_EXAMPLE_DEFINITION_METHODS = %w[shared_examples shared_examples_for shared_context].freeze

CONTEXT_METHODS = %w[
example_group
describe
Expand All @@ -42,12 +52,8 @@ module Rspec
fdescribe
fcontext
shared_examples
include_examples
it_behaves_like
it_should_behave_like
shared_context
include_context
].freeze
].freeze + SHARED_EXAMPLE_INCLUSION_METHODS

# @type [Array<Class<Correctors::Base>>]
CORRECTOR_CLASSES = [
Expand All @@ -56,7 +62,8 @@ module Rspec
Correctors::DslMethodsCorrector,
Correctors::ExampleAndHookBlocksBindingCorrector,
Correctors::LetMethodsCorrector,
Correctors::SubjectMethodCorrector
Correctors::SubjectMethodCorrector,
Correctors::SharedExamplesCorrector
].freeze

# Provides completion for RSpec DSL and helper methods.
Expand Down
32 changes: 32 additions & 0 deletions lib/solargraph/rspec/correctors/shared_examples_corrector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

require_relative 'base'

module Solargraph
module Rspec
module Correctors
class SharedExamplesCorrector < Base
# @param source_map [Solargraph::SourceMap]
def correct(source_map)
@shared_examples = {}

rspec_walker.on_shared_example_definition do |shared_examples_name, location_range|
SHARED_EXAMPLE_INCLUSION_METHODS.each do |method_name|
pin = Solargraph::Pin::FactoryParameter.new(
method_name: method_name,
method_namespace: 'RSpec::Core::ExampleGroup',
method_scope: :class,
param_name: 'name',
value: shared_examples_name,
return_type: nil,
decl: :arg,
location: PinFactory.build_location(location_range, source_map.source.filename)
)
add_pin(pin)
end
end
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/solargraph/rspec/spec_walker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def initialize(source_map:, config:)
on_example_block: [],
on_hook_block: [],
on_blocks_in_examples: [],
on_shared_example_definition: [],
after_walk: []
}
end
Expand Down Expand Up @@ -89,6 +90,14 @@ def on_blocks_in_examples(&block)
@handlers[:on_blocks_in_examples] << block
end

# @param block [Proc]
# @yieldparam name [String]
# @yieldparam location_range [Solargraph::Range]
# @return [void]
def on_shared_example_definition(&block)
@handlers[:on_shared_example_definition] << block
end

# @param block [Proc]
# @return [void]
def after_walk(&block)
Expand Down Expand Up @@ -167,6 +176,17 @@ def walk!
end
end

walker.on :block do |block_ast|
next unless NodeTypes.a_shared_example_definition?(block_ast)

name = NodeTypes.shared_example_name(block_ast)
next unless name

@handlers[:on_shared_example_definition].each do |handler|
handler.call(name, PinFactory.build_location_range(block_ast))
end
end

walker.walk

@handlers[:after_walk].each(&:call)
Expand Down
22 changes: 22 additions & 0 deletions lib/solargraph/rspec/spec_walker/node_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ def self.let_method_name(block_ast)

block_ast.children[0].children[2]&.children&.[](0)&.to_s # rubocop:disable Style/SafeNavigationChainLength
end

# @param block_ast [::Parser::AST::Node]
# @return [Boolean]
def self.a_shared_example_definition?(block_ast)
SHARED_EXAMPLE_DEFINITION_METHODS.include?(method_with_block_name(block_ast))
end

# @param block_ast [::Parser::AST::Node]
# @return [String, Symbol, nil] The name of the shared example being defined or included
def self.shared_example_name(block_ast)
return nil unless a_shared_example_definition?(block_ast)

name_node = block_ast.children[0].children[2]
return nil unless name_node

case name_node.type
when :str, :dstr
name_node.children[0]&.to_s
when :sym
name_node.children[0]
end
end
end
end
end
Expand Down
65 changes: 61 additions & 4 deletions spec/solargraph/rspec/convention_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@
end
RUBY

expect(completion_at(filename, [2, 6])).not_to include('subject')
expect(api_map.pins.any? { |pin| pin.name == 'subject' }).to be false
expect(api_map.pins.any? { |pin| pin.path == 'RSpec::ExampleGroups::TestSomeTextDescription#subject' }).to be false
end

# Regression test: prevents errors when described_class is manually overridden with a let
Expand All @@ -217,8 +216,7 @@
end
RUBY

expect(completion_at(filename, [2, 6])).not_to include('subject')
expect(api_map.pins.any? { |pin| pin.name == 'subject' }).to be false
expect(api_map.pins.any? { |pin| pin.path == 'RSpec::ExampleGroups::TestSomeTextDescription#subject' }).to be false
end

it 'generates modules for describe/context blocks' do
Expand Down Expand Up @@ -352,6 +350,65 @@ def self.my_class_method
expect(completion_at(filename, [7, 11])).to include('my_class_method') # inherit from parent context
end

it 'resolves globally and locally defined shared examples' do
load_string filename, <<~RUBY
RSpec.shared_examples 'a global shared example' do
it 'does something globally' do
expect(true).to eq(true)
end
end

RSpec.shared_examples :another_global_shared_example do
it 'does something globally with symbols' do
expect(true).to eq(true)
end
end

RSpec.describe SomeNamespace::Transaction do
shared_examples 'local shared example' do
it 'does something locally' do
expect(false).to eq(false)
end
end

shared_examples :local_symbol_example do
it 'does something locally with symbols' do
expect(42).to eq(42)
end
end

context 'when using shared examples' do
include_examples 'a global shared example'
include_examples :another_global_shared_example
include_examples 'local shared example'
it_behaves_like :local_symbol_example
end
end
RUBY

# Check global shared examples
pins = defintion_pins_at(filename, [26, 25])
factory_param = pins.first
expect(factory_param).not_to be_nil
expect(factory_param.value).to eq('a global shared example')

pins = defintion_pins_at(filename, [27, 25])
factory_param = pins.first
expect(factory_param).not_to be_nil
expect(factory_param.value).to eq(:another_global_shared_example)

# Check local shared examples
pins = defintion_pins_at(filename, [28, 25])
factory_param = pins.first
expect(factory_param).not_to be_nil
expect(factory_param.value).to eq('local shared example')

pins = defintion_pins_at(filename, [29, 25])
factory_param = pins.first
expect(factory_param).not_to be_nil
expect(factory_param.value).to eq(:local_symbol_example)
end

it 'completes RSpec DSL methods' do
pending('https://github.com/castwide/solargraph/pull/877')
load_string filename, <<~RUBY
Expand Down
158 changes: 158 additions & 0 deletions spec/solargraph/rspec/shared_example_definition_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

RSpec.describe Solargraph::Rspec::Convention do
let(:api_map) { Solargraph::ApiMap.new }
let(:library) { Solargraph::Library.new }
let(:filename) { File.expand_path('spec/models/shared_examples_spec.rb') }

describe 'shared examples support' do
it 'provides completion for shared example names in include_examples' do
load_string filename, <<~RUBY
RSpec.shared_examples 'a shared example' do
it 'does something' do
expect(true).to eq(true)
end
end

RSpec.shared_examples 'another shared example' do
it 'does something else' do
expect(false).to eq(false)
end
end

RSpec.describe SomeClass do
include_examples 'a shared example'
end
RUBY

# Check that factory parameters are created for shared examples
factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

shared_example_params = factory_params.select do |pin|
pin.method_name == 'include_examples' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(shared_example_params.map(&:value)).to include('a shared example', 'another shared example')
end

it 'provides completion for shared example names in it_behaves_like' do
load_string filename, <<~RUBY
RSpec.shared_examples 'behaves like something' do
it 'has behavior' do
expect(subject).to be_truthy
end
end

RSpec.describe SomeClass do
it_behaves_like 'behaves like something'
end
RUBY

factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

behaves_like_params = factory_params.select do |pin|
pin.method_name == 'it_behaves_like' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(behaves_like_params.map(&:value)).to include('behaves like something')
end

it 'provides completion for shared example names in it_should_behave_like' do
load_string filename, <<~RUBY
RSpec.shared_examples 'legacy behavior' do
it 'works with old syntax' do
expect(1).to eq(1)
end
end

RSpec.describe SomeClass do
it_should_behave_like 'legacy behavior'
end
RUBY

factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

should_behave_params = factory_params.select do |pin|
pin.method_name == 'it_should_behave_like' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(should_behave_params.map(&:value)).to include('legacy behavior')
end

it 'supports shared_context and include_context' do
load_string filename, <<~RUBY
RSpec.shared_context 'with setup' do
let(:value) { 42 }
end

RSpec.describe SomeClass do
include_context 'with setup'
end
RUBY

factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

context_params = factory_params.select do |pin|
pin.method_name == 'include_context' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(context_params.map(&:value)).to include('with setup')
end

it 'handles shared examples with symbols' do
load_string filename, <<~RUBY
RSpec.shared_examples :symbol_example do
it 'works with symbols' do
expect(true).to be true
end
end

RSpec.describe SomeClass do
include_examples :symbol_example
end
RUBY

factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

symbol_params = factory_params.select do |pin|
pin.method_name == 'include_examples' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(symbol_params.map(&:value)).to include(:symbol_example)
end

it 'works with shared_examples_for alias' do
load_string filename, <<~RUBY
RSpec.shared_examples_for 'aliased example' do
it 'uses the alias' do
expect(42).to eq(42)
end
end

RSpec.describe SomeClass do
include_examples 'aliased example'
end
RUBY

factory_params = api_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::FactoryParameter) }

alias_params = factory_params.select do |pin|
pin.method_name == 'include_examples' &&
pin.method_namespace == 'RSpec::Core::ExampleGroup' &&
pin.method_scope == :class
end

expect(alias_params.map(&:value)).to include('aliased example')
end
end
end
Loading
Loading