diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index c46c106..1fe00e7 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,6 +15,10 @@ on: permissions: contents: read +env: + SOLARGRAPH_CACHE: "${{ github.workspace }}/solargraph-rspec/gem-cache" + BUNDLE_GEMFILE: "${{ github.workspace }}/solargraph-rspec/gemfiles/default.gemfile" + jobs: test: name: Tests (ruby v${{ matrix.ruby-version }}) @@ -25,7 +29,6 @@ jobs: # FIXME: Why '3.0' is not working with Appraisal? # FIXME: Add 'head' https://github.com/lekemula/solargraph-rspec/pull/8/commits/3b52752b96e7f2ec01831406f8e5a51c91523187 ruby-version: ['3.1', '3.2', '3.3'] - steps: - name: Checkout solargraph-rspec uses: actions/checkout@v4 @@ -42,32 +45,27 @@ jobs: - name: Cache Ruby gems uses: actions/cache@v3 with: - path: solargraph-rspec/vendor/bundle - key: bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby-version }}-${{ hashFiles('solargraph-rspec/solargraph-rspec.gemspec') }} - restore-keys: | - bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby-version }}-${{ hashFiles('solargraph-rspec/solargraph-rspec.gemspec') }} + path: solargraph-rspec/gemfiles/vendor/bundle + key: bundle-use-ruby-${{ matrix.ruby-version }}-${{ hashFiles('solargraph-rspec/Gemfile', 'solargraph-rspec/gemfiles/default.gemfile.lock') }} - name: Install dependencies run: | cd solargraph-rspec bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - bundle exec appraisal install - - - name: Run Rubocop - run: cd solargraph-rspec && bundle exec rubocop - - name: Set up yardocs - # yard gems caches the yardocs into /doc/.yardoc path, hence they should be cached by ruby gems cache - run: cd solargraph-rspec && bundle exec appraisal yard gems --verbose + # - name: Run Rubocop + # run: cd solargraph-rspec && bundle exec rubocop - - name: List all Yardoc constants and methods - run: | - cd solargraph-rspec - bundle exec yard list + - name: Cache Docs + run: |- + # Vendor files: don't do them + echo 'exclude: ["**/vendor/**/*"]' > solargraph-rspec/.solargraph.yml + cd solargraph-rspec + bundle exec solargraph gems --rebuild - name: Run tests - run: cd solargraph-rspec && bundle exec appraisal rspec --format progress + run: cd solargraph-rspec && bundle exec rspec --format progress - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 diff --git a/.gitignore b/.gitignore index bcc8d5f..79e5a26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ # rspec failure tracking .rspec_status + +.solargraph.yml +vendor +gemfiles/vendor diff --git a/gemfiles/.bundle/config b/gemfiles/.bundle/config index c127f80..b8c1796 100644 --- a/gemfiles/.bundle/config +++ b/gemfiles/.bundle/config @@ -1,2 +1,3 @@ --- BUNDLE_RETRY: "1" +BUNDLE_PATH: "vendor/bundle" diff --git a/gemfiles/default.gemfile.lock b/gemfiles/default.gemfile.lock index dc35419..a4a89b5 100644 --- a/gemfiles/default.gemfile.lock +++ b/gemfiles/default.gemfile.lock @@ -1,8 +1,8 @@ PATH remote: .. specs: - solargraph-rspec (0.4.1) - solargraph (~> 0.52, >= 0.52.0) + solargraph-rspec (0.5.2) + solargraph (~> 0.56, >= 0.56.0) GEM remote: https://rubygems.org/ @@ -79,7 +79,7 @@ GEM debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) - diff-lcs (1.6.0) + diff-lcs (1.6.2) docile (1.4.1) domain_name (0.6.20240107) drb (2.2.1) @@ -97,13 +97,13 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - jaro_winkler (1.6.0) - json (2.10.2) + jaro_winkler (1.6.1) + json (2.12.2) kramdown (2.5.1) rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - language_server-protocol (3.17.0.4) + language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.6.6) loofah (2.24.0) @@ -134,16 +134,19 @@ GEM netrc (0.11.0) nokogiri (1.17.2-arm64-darwin) racc (~> 1.4) + nokogiri (1.17.2-x86_64-linux) + racc (~> 1.4) observer (0.1.2) optparse (0.6.0) - ostruct (0.6.1) - parallel (1.26.3) - parser (3.3.7.2) + ostruct (0.6.2) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc pp (0.6.2) prettyprint prettyprint (0.2.0) + prism (1.4.0) profile-viewer (0.0.4) optparse webrick @@ -183,7 +186,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rbs (3.6.1) + rbs (3.9.4) logger rdoc (6.12.0) psych (>= 4.0.0) @@ -200,16 +203,16 @@ GEM reverse_markdown (3.0.0) nokogiri rexml (3.4.1) - rspec (3.13.0) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) + rspec-core (3.13.5) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.2) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (7.1.1) @@ -225,8 +228,8 @@ GEM rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) - rspec-support (3.13.2) - rubocop (1.74.0) + rspec-support (3.13.4) + rubocop (1.78.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -234,11 +237,12 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.41.0) + rubocop-ast (1.45.1) parser (>= 3.3.7.2) + prism (~> 1.4) ruby-progressbar (1.13.0) securerandom (0.3.2) shoulda-matchers (6.4.0) @@ -258,20 +262,21 @@ GEM simplecov (~> 0.19) simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - solargraph (0.52.0) + solargraph (0.56.0) backport (~> 1.2) - benchmark + benchmark (~> 0.4) bundler (~> 2.0) diff-lcs (~> 1.4) - jaro_winkler (~> 1.6) + jaro_winkler (~> 1.6, >= 1.6.1) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) logger (~> 1.6) observer (~> 0.1) ostruct (~> 0.6) parser (~> 3.0) - rbs (~> 3.0) - reverse_markdown (>= 2.0, < 4) + prism (~> 1.4) + rbs (~> 3.3) + reverse_markdown (~> 3.0) rubocop (~> 1.38) thor (~> 1.0) tilt (~> 2.0) @@ -279,7 +284,7 @@ GEM yard-solargraph (~> 0.1) stringio (3.1.5) thor (1.3.2) - tilt (2.6.0) + tilt (2.6.1) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -298,6 +303,7 @@ GEM PLATFORMS arm64-darwin-24 + x86_64-linux DEPENDENCIES actionmailer diff --git a/lib/solargraph/rspec/convention.rb b/lib/solargraph/rspec/convention.rb index 7fb8c38..49020a2 100644 --- a/lib/solargraph/rspec/convention.rb +++ b/lib/solargraph/rspec/convention.rb @@ -10,6 +10,7 @@ require_relative 'correctors/subject_method_corrector' require_relative 'correctors/context_block_methods_corrector' require_relative 'correctors/dsl_methods_corrector' +require_relative 'rspec_helper' require_relative 'test_helpers' require_relative 'pin_factory' @@ -120,6 +121,8 @@ def local(source_map) pins = [] # @type [Array] namespace_pins = [] + # @type [Array] + extra_requires = ['rspec'] rspec_walker = SpecWalker.new(source_map: source_map, config: config) @@ -133,14 +136,26 @@ def local(source_map) rspec_walker.walk! pins += namespace_pins + begin + pins += RSpecConfigure.instance.pins + extra_requires += RSpecConfigure.instance.extra_requires + rescue StandardError => e + Solargraph.logger.error("[RSpec] [RSpecConfigure] Can't add pins: #{e}") + [] + end if pins.any? Solargraph.logger.debug( "[RSpec] added #{pins.map(&:inspect)} to #{source_map.filename}" ) end + if extra_requires.any? + Solargraph.logger.debug( + "[RSpec] added requires #{extra_requires} to #{source_map.filename}" + ) + end - Environ.new(requires: [], pins: pins) + Environ.new(requires: extra_requires, pins: pins) rescue StandardError, SyntaxError => e raise e if ENV['SOLARGRAPH_DEBUG'] diff --git a/lib/solargraph/rspec/rspec_configure.rb b/lib/solargraph/rspec/rspec_configure.rb new file mode 100644 index 0000000..6664bf3 --- /dev/null +++ b/lib/solargraph/rspec/rspec_configure.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Solargraph + module Rspec + # RSpec.configure ... config.include handler, essentially + class RSpecConfigure + COMMON_HELPER_FILES = [ + 'spec/spec_helper.rb', + 'spec/rails_helper.rb' + ].freeze + + # @param node [::Parser::AST::Node] + # @param file [String] The name of the file this is module is defined in + # @param module_name [String] The name of the module to be included + class IncludedModule < Struct.new(:node, :file, :module_name) do + end + + def self.instance + @instance ||= new + end + + def self.reset + @instance = nil + end + + # @return [Array] + def pins + ns = Solargraph::Pin::Namespace.new(name: 'RSpec::ExampleGroups') + + included_modules.map do |m| + Solargraph::Pin::Reference::Include.new( + closure: ns, + name: m.module_name, + location: Solargraph::Location.new(m.file, Solargraph::Parser.node_range(m.node)) + ) + end + end + + def extra_requires + included_modules.map(&:file).uniq + Dir['spec/support/**/*.rb'] + end + + # @return [Array] + def included_modules + @included_modules ||= parse_included_modules + end + + private + + # @return [Array] + def parse_included_modules + modules = [] + + COMMON_HELPER_FILES.each do |f| + ast = Solargraph::Parser.parse(File.read(f), f) + modules += extract_included_modules(ast, f) + rescue Errno::ENOENT + # Ignore this error - no file means we can chill + rescue StandardError => e + Solargraph.logger.error("[RSpec] [RSpecConfigure] Can't read helper file '#{f}': #{e}") + end + + modules + end + + # Parses the modules that were included int he Rspec.configure (in common helper files) + # @param ast [Parser::AST::Node] + # @param file [String] + # + # @return [Array] + def extract_included_modules(ast, file) + walker = Walker.new(ast) + + # @type [Array] + included_modules = [] + + walker.on :block, [:send] do |node| + send_node = node.children[0] + send_receiver = send_node.children[0] + + next if send_receiver.type != :const || send_receiver.children[2] == :Rspec + next unless send_node.children[1] == :configure + # No args + next if node.children[1].children.empty? + + config_name = node.children[1].children[0].children[0] + config_walker = Walker.new(node) + config_walker.on :send, [:lvar, config_name] do |include_node| + 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 + + included_modules << IncludedModule.new( + include_node, file, SpecWalker::FullConstantName.from_ast(mod_node) + ) + end + + config_walker.walk + end + + walker.walk + + included_modules + end + end + end +end diff --git a/solargraph-rspec.gemspec b/solargraph-rspec.gemspec index ebf6109..23301a0 100644 --- a/solargraph-rspec.gemspec +++ b/solargraph-rspec.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'solargraph', '~> 0.52', '>= 0.52.0' + spec.add_dependency 'solargraph', '~> 0.56', '>= 0.56.0' # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/solargraph/rspec/convention_spec.rb b/spec/solargraph/rspec/convention_spec.rb index c81090c..f27c11c 100644 --- a/spec/solargraph/rspec/convention_spec.rb +++ b/spec/solargraph/rspec/convention_spec.rb @@ -252,7 +252,6 @@ # NOTE: This spec depends on RSpec's YARDoc comments, if it fails try running: yard gems it 'completes RSpec::Matchers methods' do - pending('https://github.com/castwide/solargraph/pull/877') load_string filename, <<~RUBY RSpec.describe SomeNamespace::Transaction, type: :model do context 'some context' do @@ -324,7 +323,6 @@ def self.my_class_method end it 'completes RSpec DSL methods' do - pending('https://github.com/castwide/solargraph/pull/877') load_string filename, <<~RUBY RSpec.describe SomeNamespace::Transaction, type: :model do desc @@ -732,6 +730,29 @@ class MyClass; end end end + # TODO: Move back to helpers context method + describe 'rspec-mocks' do + it 'completes methods from rspec-mocks' do + load_string filename, <<~RUBY + RSpec.describe SomeNamespace::Transaction, type: :model do + let(:something) { double } + + it 'should do something' do + allow(something).to rec + allow(double).to receive_me + my_double = doub + my_double = inst + end + end + RUBY + + expect(completion_at(filename, [4, 26])).to include('receive') + expect(completion_at(filename, [5, 30])).to include('receive_message_chain') + expect(completion_at(filename, [6, 18])).to include('double') + expect(completion_at(filename, [7, 18])).to include('instance_double') + end + end + describe 'helpers' do before { pending('https://github.com/castwide/solargraph/pull/877') } @@ -849,28 +870,6 @@ class MyClass; end end end - describe 'rspec-mocks' do - it 'completes methods from rspec-mocks' do - load_string filename, <<~RUBY - RSpec.describe SomeNamespace::Transaction, type: :model do - let(:something) { double } - - it 'should do something' do - allow(something).to rec - allow(double).to receive_me - my_double = doub - my_double = inst - end - end - RUBY - - expect(completion_at(filename, [4, 26])).to include('receive') - expect(completion_at(filename, [5, 30])).to include('receive_message_chain') - expect(completion_at(filename, [6, 18])).to include('double') - expect(completion_at(filename, [7, 18])).to include('instance_double') - end - end - describe 'rspec-rails' do # A model spec is a thin wrapper for an ActiveSupport::TestCase # See: https://api.rubyonrails.org/v5.2.8.1/classes/ActiveSupport/Testing/Assertions.html @@ -1149,4 +1148,81 @@ class MyClass; end end end end + + describe 'included modules' do + require 'parser' + + before do + Solargraph::Rspec::RSpecConfigure.reset + + allow_any_instance_of(Solargraph::Rspec::RSpecConfigure).to receive(:parse_included_modules).and_return( + [ + Solargraph::Rspec::RSpecConfigure::IncludedModule.new( + # What the fuck + Parser::AST::Node.new( + :send, [], { + location: Parser::Source::Map.new( + Parser::Source::Range.new( + Parser::Source::Buffer.new('name.rb', source: '1'), + 0, 1 + ) + ) + } + ), 'spec_helper.rb', 'HelperModule' + ) + ] + ) + + source_helper = parse_string File.expand_path('spec/spec_helper.rb'), <<~RUBY + module HelperModule + def module_method + end + end + RUBY + + source_main = parse_string filename, <<~RUBY + RSpec.describe SomeNamespace::Transaction, type: :model do + it 'example test' do + mo + end + + describe 'fake example group' do + let(:var) { mo } + + mo + + before do + mo + end + + it 'example test' do + mo + end + end + end + RUBY + + load_sources(source_helper, source_main) + end + + it 'should complete inside a top level example' do + expect(completion_at(filename, [2, 6])).to include('module_method') + end + + it 'should complete inside a let block' do + expect(completion_at(filename, [6, 18])).to include('module_method') + end + + it 'should complete inside a context block' do + expect(completion_at(filename, [8, 6])).to include('module_method') + end + + it 'should complete inside a hook' do + expect(completion_at(filename, [11, 8])).to include('module_method') + end + + it 'should complete a nested example' do + expect(completion_at(filename, [15, 8])).to include('module_method') + end + end end diff --git a/spec/solargraph/rspec/rspec_configure_spec.rb b/spec/solargraph/rspec/rspec_configure_spec.rb new file mode 100644 index 0000000..a8c8c7f --- /dev/null +++ b/spec/solargraph/rspec/rspec_configure_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.describe Solargraph::Rspec::RSpecConfigure do + describe '#extract_included_modules' do + it 'should pull included modules' do + ast = Solargraph::Parser.parse(%( + Rspec.configure do |config| + config.include ModuleName + config.example OtherVal + config.include(SubMod::Module) + end + )) + + # @type [Array] + modules = Solargraph::Rspec::RSpecConfigure.instance.send(:extract_included_modules, ast, 'spec_helper.rb') + + expect(modules.map(&:module_name)).to eql(%w[ModuleName SubMod::Module]) + end + end +end diff --git a/spec/support/solargraph_helpers.rb b/spec/support/solargraph_helpers.rb index 097d485..52a09d4 100644 --- a/spec/support/solargraph_helpers.rb +++ b/spec/support/solargraph_helpers.rb @@ -2,11 +2,22 @@ module SolargraphHelpers def load_string(filename, str) - source = Solargraph::Source.load_string(str, filename) + source = parse_string(filename, str) api_map.map(source) # api_map should be defined in the spec source end + # Util method to parse (but NOT load) a string + # This method is mostly here for heredocs + # + # @param filename [String] + # @param str [String] The source code + # + # @return [Solargraph::Source] + def parse_string(filename, str) + Solargraph::Source.load_string(str, filename) + end + def load_sources(*sources) source_maps = sources.map { |s| Solargraph::SourceMap.map(s) } bench = Solargraph::Bench.new(source_maps: source_maps)