Skip to content
Merged
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Add Rspec included modules support by @ShadiestGoat in #32

### Changed
- Chore: Test solargraph prereleases (#31)
- Chore: More reliable closure and location updates by @apiology in #30
- Chore: Less limiting of gem pin processing in specs by @apiology in #29


## [0.5.5] - 2025-10-05

### Fixed
- Go-to-defintion/Hover for RSpec DSL methods (eg. context or it)

### Other Changes
- Chore: Add rubocop-yard by @lekemula in #20
- Chore: Add CI solargraph typecheck step by @lekemula in #21
- Chore: Adjust specs/CI for next solargraph release after 0.56.2 by @lekemula in #22
- Chore: Improve CI speed by @lekemula in #23
- Chore: Improve tests speed by @lekemula in #24
- Chore: Update testing steps in developer documentation by @apiology in #25
- Chore: Move 3rd party gems to Appraisals by @lekemula in #27

### Changed
- Update testing steps in developer documentation

## [0.5.4] - 2025-09-06

### Fixed
- Fix breaking of solargraph rubocop diagnostics by @lekemula in #19

## [0.5.3] - 2025-09-02

### Fixed
Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,25 @@ Add `solargraph-rspec` to your `.solargraph.yml` as a plugin.
```
### Configuration

If you have your own custom `let`-like memoized methods, you can add them to your `.solargraph.yml` file like this:
You can customize the plugin behavior in your `.solargraph.yml`:

```yaml
# .solargraph.yml
# ...
rspec:
# Custom let-like memoized methods (e.g., from rspec-given, test-prof)
let_methods:
- let_it_be
```

If you have your own custom `example`-like methods like `it`, you can add them to your `.solargraph.yml` file like this:

```yaml
# .solargraph.yml
# ...
rspec:
# Custom example-like methods (e.g., from rspec-given)
example_methods:
- my_it
```

This is useful if you use gems like [rspec-given](https://github.com/rspec-given/rspec-given) which introduces its own `let` and `example` methods.
# RSpec helper files to analyze for included modules (shared contexts, custom matchers)
config_helper_files:
- spec/spec_helper.rb
- spec/rails_helper.rb
```


### Gem completions
Expand Down
20 changes: 20 additions & 0 deletions lib/solargraph/rspec/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Rspec
# rspec:
# let_methods:
# - let_it_be
# config_helper_files:
# - spec/spec_helper.rb
# - spec/rails_helper.rb
class Config
def initialize(solargraph_config = Solargraph::Workspace::Config.new('./'))
@solargraph_config = solargraph_config
Expand All @@ -25,8 +28,18 @@ def example_methods
(Rspec::EXAMPLE_METHODS + additional_example_methods).map(&:to_sym)
end

# @return [Array<String>]
def config_helper_files
DEFAULT_CONFIG_HELPER_FILES + additional_config_helper_files
end

private

DEFAULT_CONFIG_HELPER_FILES = [
'spec/spec_helper.rb',
'spec/rails_helper.rb'
].freeze

# @return [Hash]
def rspec_raw_data
@rspec_raw_data ||= raw_data['rspec'] || {}
Expand All @@ -46,6 +59,13 @@ def additional_example_methods
(example_methods || []).map(&:to_sym)
end

# @return [Array<String>]
def additional_config_helper_files
# @type [Array<String>, nil]
config_files = rspec_raw_data['config_helper_files']
config_files || []
end

# @return [Hash]
def raw_data
@solargraph_config.raw_data
Expand Down
11 changes: 10 additions & 1 deletion lib/solargraph/rspec/convention.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require_relative 'correctors/dsl_methods_corrector'
require_relative 'gems'
require_relative 'pin_factory'
require_relative 'rspec_configure'

module Solargraph
module Rspec
Expand Down Expand Up @@ -86,8 +87,16 @@ def global(_yard_map)
root_example_group_namespace_pin: root_example_group_namespace_pin
)
pins += annotation_pins

# Add pins from RSpec.configure blocks
rspec_configure = RSpecConfigure.new(
config: config,
root_namespace_pin: root_example_group_namespace_pin
)
pins += rspec_configure.pins

# TODO: Include gem requires conditionally based on Gemfile definition
requires = Solargraph::Rspec::Gems.gem_names
requires = Solargraph::Rspec::Gems.gem_names + rspec_configure.extra_requires

if pins.any?
Solargraph.logger.debug(
Expand Down
67 changes: 67 additions & 0 deletions lib/solargraph/rspec/rspec_configure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require_relative 'spec_configuration_walker'
require_relative 'pin_factory'

module Solargraph
module Rspec
# Handles RSpec.configure ... config.include parsing
# Provides pins for modules included globally via RSpec.configure
class RSpecConfigure
# @param config [Config]
# @param root_namespace_pin [Solargraph::Pin::Namespace]
def initialize(config:, root_namespace_pin:)
@config = config
@root_namespace_pin = root_namespace_pin
@included_modules = nil
end

# @return [Array<Solargraph::Pin::Reference::Include>]
def pins
included_modules.map do |mod|
Solargraph::Pin::Reference::Include.new(
closure: @root_namespace_pin,
name: mod.module_name,
location: Solargraph::Location.new(mod.file, Solargraph::Parser.node_range(mod.node))
)
end
end

# @return [Array<String>]
def extra_requires
included_modules.map(&:file).uniq + support_files
end

# @return [Array<SpecConfigurationWalker::IncludedModule>]
def included_modules
@included_modules ||= parse_included_modules
end

private

# @return [Array<String>]
def support_files
Dir['spec/support/**/*.rb']
end

# @return [Array<SpecConfigurationWalker::IncludedModule>]
def parse_included_modules
modules = []

@config.config_helper_files.each do |file_path|
ast = Solargraph::Parser.parse(File.read(file_path), file_path)
walker = SpecConfigurationWalker.new(ast, file_path)
modules += walker.extract_included_modules
rescue Errno::ENOENT
# Ignore missing files - they may not exist in all projects
rescue StandardError => e
Solargraph.logger.error(
"[RSpec] [RSpecConfigure] Error reading helper file '#{file_path}': #{e.message}"
)
end

modules
end
end
end
end
74 changes: 74 additions & 0 deletions lib/solargraph/rspec/spec_configuration_walker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require_relative 'walker'
require_relative 'spec_walker/full_constant_name'

module Solargraph
module Rspec
# Walks AST to find RSpec.configure blocks and extract included modules
class SpecConfigurationWalker
# @param ast [::Parser::AST::Node]
# @param filename [String] The file being parsed
def initialize(ast, filename)
@ast = ast
@filename = filename
@walker = Walker.new(ast)
@included_modules = []
end

# Represents a module included via config.include in RSpec.configure
# @param node [::Parser::AST::Node] The AST node for the include call
# @param file [String] The file where this module is included
# @param module_name [String] The fully qualified name of the included module
class IncludedModule < Struct.new(:node, :file, :module_name)
end

# Walk the AST and extract included modules
# @return [Array<IncludedModule>]
def extract_included_modules
@walker.on :block, [:send] do |node|
send_node = node.children[0]
send_receiver = send_node.children[0]

# Check if this is RSpec.configure
next unless send_receiver&.type == :const
next unless send_receiver.children[1] == :RSpec
next unless send_node.children[1] == :configure

# Ensure block has arguments (the config parameter)
next if node.children[1].children.empty?

process_configure_block(node)
end

@walker.walk
@included_modules
end

private

# Process the RSpec.configure block to find config.include calls
# @param configure_block [::Parser::AST::Node]
# @return [void]
def process_configure_block(configure_block)
# Get the config variable name (e.g., |config| or |c|)
config_name = configure_block.children[1].children[0].children[0]

config_walker = Walker.new(configure_block)
config_walker.on :send, [:lvar, config_name] do |include_node|
# Look for config.include calls
next unless include_node.children[1] == :include

mod_node = include_node.children[2]
next unless mod_node.is_a?(::Parser::AST::Node)
next unless mod_node.type == :const

module_name = SpecWalker::FullConstantName.from_ast(mod_node)
@included_modules << IncludedModule.new(include_node, @filename, module_name)
end

config_walker.walk
end
end
end
end
Loading
Loading