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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
doc
coverage
/tmp/
pkg/*
.tool-versions
.ruby-version
.ruby_version
67 changes: 62 additions & 5 deletions lib/solargraph/parser/parser_gem/class_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions lib/solargraph/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/solargraph/workspace/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def max_files
raw_data['max_files']
end

def ruby_version
raw_data['ruby_version']
end

private

# @return [String]
Expand Down Expand Up @@ -156,6 +160,7 @@ def default_config
'require' => [],
'domains' => [],
'reporters' => %w[rubocop require_not_found],
'ruby_version' => 'current',
'formatter' => {
'rubocop' => {
'cops' => 'safe',
Expand Down
19 changes: 10 additions & 9 deletions spec/parser/node_chainer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 49 additions & 0 deletions spec/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions spec/workspace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down