From cac08ade92c251f2d0fffd42bf1bba63ab63d739 Mon Sep 17 00:00:00 2001 From: ShadiestGoat <48590492+ShadiestGoat@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:34:53 +0100 Subject: [PATCH] [WIP] Allow setting a target ruby version --- .gitignore | 4 ++ .../parser/parser_gem/class_methods.rb | 67 +++++++++++++++++-- lib/solargraph/workspace.rb | 2 + lib/solargraph/workspace/config.rb | 5 ++ spec/parser/node_chainer_spec.rb | 19 +++--- spec/parser_spec.rb | 49 ++++++++++++++ spec/workspace_spec.rb | 6 +- 7 files changed, 135 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index fc09c2fea..44a704c35 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ doc coverage /tmp/ +pkg/* +.tool-versions +.ruby-version +.ruby_version \ No newline at end of file diff --git a/lib/solargraph/parser/parser_gem/class_methods.rb b/lib/solargraph/parser/parser_gem/class_methods.rb index 58ca8056b..e73c77969 100644 --- a/lib/solargraph/parser/parser_gem/class_methods.rb +++ b/lib/solargraph/parser/parser_gem/class_methods.rb @@ -14,6 +14,13 @@ module Solargraph module Parser module ParserGem + FORCED_LEGACY_PARSERS = { + 1 => (8..9), + 2 => (0..7), + 3 => (0..2) + } + MIN_MODERN_PARSER_VERSION = [3, 3] + module ClassMethods # @param code [String] # @param filename [String, nil] @@ -31,17 +38,67 @@ def parse_with_comments code, filename = nil def parse code, filename = nil, line = 0 buffer = ::Parser::Source::Buffer.new(filename, line) buffer.source = code - parser.parse(buffer) + res = parser.parse(buffer) + parser.reset + + res rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e + parser.reset + raise Parser::SyntaxError, e.message end + def parser_opts(parser) + parser.diagnostics.all_errors_are_fatal = true + parser.diagnostics.ignore_warnings = true + end + + # @param version [String] a presentation of the ruby version as a string + # Eg. ruby 2.7.4 => 27 + # ruby 3.4 => 34 + def modern_parser(version) + Solargraph.logger.info("Using modern ruby parser (#{version})") + + Prism::Translation.const_get("Parser#{version}").new(FlawedBuilder.new).tap do |parser| + parser_opts(parser) + end + end + + def legacy_parser(version) + Solargraph.logger.info("Using legacy ruby parser (#{version})") + + require "parser/ruby#{version}" + parser = ::Parser.const_get("Ruby#{version}").new(FlawedBuilder.new) + parser_opts(parser) + + parser + end + + # Forces a new parser with a specified version of ruby + # @param ruby_version [String, :current] # @return [::Parser::Base] - def parser - @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser| - parser.diagnostics.all_errors_are_fatal = true - parser.diagnostics.ignore_warnings = true + def force_new_parser(ruby_version = :current) + Solargraph.logger.debug("Trying to set new parser version for '#{ruby_version}'") + + ruby_version = RUBY_VERSION if ruby_version == :current + major, minor = ruby_version.split('.').map(&:to_i)[..1] + + if major >= MIN_MODERN_PARSER_VERSION[0] && minor >= MIN_MODERN_PARSER_VERSION[1] + # Modern parsers can be memoized, idk why legacy one can't be :/ + @parser = modern_parser([major, minor].join) + elsif FORCED_LEGACY_PARSERS.key?(major) && FORCED_LEGACY_PARSERS[major].include?(minor) + @parser = legacy_parser([major, minor].join) + else + # Ruby < 3 is unsupported, so this shouldn't get into an infinite loop, ever + force_new_parser(:current) end + + @parser + end + + # @return [::Parser::Base] + def parser + @parser ||= force_new_parser end # @param source [Source] diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 0782d414e..ac875fbe0 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -171,6 +171,8 @@ def source_hash # @return [void] def load_sources source_hash.clear + Solargraph::Parser.force_new_parser(config.ruby_version == 'current' ? :current : config.ruby_version) + unless directory.empty? || directory == '*' size = config.calculated.length raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index 1514ff617..21c73fb50 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -111,6 +111,10 @@ def max_files raw_data['max_files'] end + def ruby_version + raw_data['ruby_version'] + end + private # @return [String] @@ -156,6 +160,7 @@ def default_config 'require' => [], 'domains' => [], 'reporters' => %w[rubocop require_not_found], + 'ruby_version' => 'current', 'formatter' => { 'rubocop' => { 'cops' => 'safe', diff --git a/spec/parser/node_chainer_spec.rb b/spec/parser/node_chainer_spec.rb index e92431aae..c861014e9 100644 --- a/spec/parser/node_chainer_spec.rb +++ b/spec/parser/node_chainer_spec.rb @@ -163,18 +163,19 @@ class Foo expect(arg).to be_a(Solargraph::Source::Chain::BlockSymbol) end - # feature added in Ruby 3.1 - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1') - it 'tracks anonymous block forwarding' do - source = Solargraph::Source.load_string(%( + it 'tracks anonymous block forwarding' do + Solargraph::Parser.force_new_parser('3.1') # (added in ruby 3.1) + + source = Solargraph::Source.load_string(%( def foo(&) bar(&) end )) - anonymous_block_pass = source.node.children[2].children[2] - chain = Solargraph::Parser.chain(anonymous_block_pass) - block_variable_node = chain.links.first - expect(block_variable_node.word).to be_nil - end + anonymous_block_pass = source.node.children[2].children[2] + chain = Solargraph::Parser.chain(anonymous_block_pass) + block_variable_node = chain.links.first + expect(block_variable_node.word).to be_nil + + Solargraph::Parser.force_new_parser(:current) end end diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 267f412f4..efba45976 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -8,4 +8,53 @@ code = "# encoding: utf-\nx = 'y'" expect { Solargraph::Parser.parse(code) }.to raise_error(Solargraph::Parser::SyntaxError) end + + describe '#force_new_parser' do + after do + Solargraph::Parser.force_new_parser :current + end + + it 'should handle :current' do + Solargraph::Parser.force_new_parser :current + expect(Solargraph::Parser.version).to eql(RUBY_VERSION.split('.')[..1].join.to_i) + end + + it 'should fall back to using :current in case of bad input' do + Solargraph::Parser.force_new_parser 'not a version string' + expect(Solargraph::Parser.version).to eql(RUBY_VERSION.split('.')[..1].join.to_i) + end + + it 'should use modern parser for supported versions' do + Solargraph::Parser.force_new_parser '3.4.4' + expect(Solargraph::Parser.version).to eql(34) + expect(Solargraph::Parser.parser).to be_a(Prism::Translation::Parser) + end + + it 'should use legacy parser for when modern parser cannot be used' do + Solargraph::Parser.force_new_parser '2.7.4' + expect(Solargraph::Parser.version).to eql(27) + expect(Solargraph::Parser.parser).to be_a(Parser::Ruby27) + end + end + + describe 'different versions' do + after do + Solargraph::Parser.force_new_parser :current + end + + it 'fails to parser ruby 3 syntax when using ruby 2 parsing' do + Solargraph::Parser.force_new_parser('2.7') + + expect(Solargraph::Parser.version).to eql(27) + expect { Solargraph::Parser.parse('def available? = !@internal.any?') }.to raise_error(Solargraph::Parser::SyntaxError) + end + + it 'succeeds in parsing ruby 3 syntax when using ruby 3 parsing' do + Solargraph::Parser.force_new_parser('3.3') + + expect(Solargraph::Parser.version).to eql(33) + node = Solargraph::Parser.parse('def available? = !@internal.any?', 'test.rb') + expect(Solargraph::Parser.is_ast_node?(node)).to be(true) + end + end end diff --git a/spec/workspace_spec.rb b/spec/workspace_spec.rb index 572c3e131..1a9b1dbc4 100644 --- a/spec/workspace_spec.rb +++ b/spec/workspace_spec.rb @@ -50,7 +50,7 @@ end it "raises an exception for workspace size limits" do - config = double(:config, calculated: Array.new(Solargraph::Workspace::Config::MAX_FILES + 1), max_files: Solargraph::Workspace::Config::MAX_FILES) + config = double(:config, calculated: Array.new(Solargraph::Workspace::Config::MAX_FILES + 1), max_files: Solargraph::Workspace::Config::MAX_FILES, ruby_version: 'current') expect { Solargraph::Workspace.new('.', config) @@ -62,7 +62,7 @@ File.write(gemspec_file, '') calculated = Array.new(Solargraph::Workspace::Config::MAX_FILES + 1) { gemspec_file } # @todo Mock reveals tight coupling - config = double(:config, calculated: calculated, max_files: 0, allow?: true, require_paths: [], plugins: []) + config = double(:config, calculated: calculated, max_files: 0, allow?: true, require_paths: [], plugins: [], ruby_version: 'current') expect { Solargraph::Workspace.new('.', config) }.not_to raise_error @@ -134,7 +134,7 @@ end it 'rescues errors loading files into sources' do - config = double(:Config, directory: './path', calculated: ['./path/does_not_exist.rb'], max_files: 5000, require_paths: [], plugins: []) + config = double(:config, directory: './path', calculated: ['./path/does_not_exist.rb'], max_files: 5000, require_paths: [], plugins: [], ruby_version: 'current') expect { Solargraph::Workspace.new('./path', config) }.not_to raise_error