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
1 change: 1 addition & 0 deletions changelog/new_enable_reusable_prism_parse_result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#359](https://github.com/rubocop/rubocop-ast/pull/359): Enable reusable Prism parse result. ([@koic][])
23 changes: 19 additions & 4 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,31 @@ In RuboCop AST, you can specify Prism as the parser engine backend.

If running through Bundler, please first add `gem 'prism'` to your Gemfile:

```ruby
[source,ruby]
----
gem 'prism'
```
----

By specifying `parser_engine: :parser_prism`, parsing with Prism can be processed:

```ruby
[source,ruby]
----
# Using the Parser gem with `parser_engine: parser_whitequark` is the default.
ProcessedSource.new(@options[:stdin], ruby_version, file, parser_engine: :parser_prism)
```
----

Furthermore, by passing an instance of `Prism::ParseLexResult` to the `:prism_result` keyword argument,
the Prism parsing process can be bypassed. This is a useful API for Ruby LSP, where `Prism::ParseLexResult` has
already been obtained externally from RuboCop. A `Prism::ParseLexResult` instance is a value that can be obtained,
for example, as the return value of `Prism.parse_lex(source)`.
The bypass occurs only when `parser_engine: :parser_prism` is set and an instance of `Prism::ParseLexResult` is specified
for the `:prism_result` keyword argument. Otherwise, the source code is parsed.

[source,ruby]
----
# Using the Parser gem with `prism_result: nil` is the default, meaning the source code is parsed.
ProcessedSource.new(@options[:stdin], ruby_version, file, parser_engine: :parser_prism, prism_result: :prism_result)
----

This is an experimental feature. If you encounter any incompatibilities between
Prism and the Parser gem, please check the following URL:
Expand Down
56 changes: 50 additions & 6 deletions lib/rubocop/ast/processed_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@

module RuboCop
module AST
# A `Prism` interface's class that provides a fixed `Prism::ParseLexResult` instead of parsing.
#
# This class implements the `parse_lex` method to return a preparsed `Prism::ParseLexResult`
# rather than parsing the source code. When the parse result is already available externally,
# such as in Ruby LSP, the Prism parsing process can be bypassed.
class PrismPreparsed
def initialize(prism_result)
unless prism_result.is_a?(Prism::ParseLexResult)
raise ArgumentError, <<~MESSAGE
Expected a `Prism::ParseLexResult` object, but received `#{prism_result.class}`.
MESSAGE
end

@prism_result = prism_result
end

def parse_lex(_source, **_prism_options)
@prism_result
end
end

# ProcessedSource contains objects which are generated by Parser
# and other information such as disabled lines for cops.
# It also provides a convenient way to access source lines.
Expand All @@ -25,7 +46,9 @@ def self.from_file(path, ruby_version, parser_engine: :parser_whitequark)
new(file, ruby_version, path, parser_engine: parser_engine)
end

def initialize(source, ruby_version, path = nil, parser_engine: :parser_whitequark)
def initialize(
source, ruby_version, path = nil, parser_engine: :parser_whitequark, prism_result: nil
)
parser_engine = parser_engine.to_sym
unless PARSER_ENGINES.include?(parser_engine)
raise ArgumentError, 'The keyword argument `parser_engine` accepts `parser_whitequark` ' \
Expand All @@ -44,7 +67,7 @@ def initialize(source, ruby_version, path = nil, parser_engine: :parser_whitequa
@parser_engine = parser_engine
@parser_error = nil

parse(source, ruby_version, parser_engine)
parse(source, ruby_version, parser_engine, prism_result)
end

def ast_with_comments
Expand Down Expand Up @@ -202,7 +225,7 @@ def comment_index
end
end

def parse(source, ruby_version, parser_engine)
def parse(source, ruby_version, parser_engine, prism_result)
buffer_name = @path || STRING_SOURCE_NAME
@buffer = Parser::Source::Buffer.new(buffer_name, 1)

Expand All @@ -216,7 +239,9 @@ def parse(source, ruby_version, parser_engine)
return
end

@ast, @comments, @tokens = tokenize(create_parser(ruby_version, parser_engine))
parser = create_parser(ruby_version, parser_engine, prism_result)

@ast, @comments, @tokens = tokenize(parser)
end

def tokenize(parser)
Expand Down Expand Up @@ -326,10 +351,28 @@ def require_prism_translation_parser(version)
exit!
end

def create_parser(ruby_version, parser_engine)
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def create_parser(ruby_version, parser_engine, prism_result)
builder = RuboCop::AST::Builder.new

parser_class(ruby_version, parser_engine).new(builder).tap do |parser|
parser_class = parser_class(ruby_version, parser_engine)

# NOTE: Check if the `Prism#initialize` method has the `:parser` keyword argument.
# The `:parser` keyword argument cannot be used to switch parsers because older versions of
# Prism do not support it.
parser_switch_available = parser_class.instance_method(:initialize).parameters.assoc(:key)

parser_instance = if prism_result && parser_switch_available
# NOTE: Since it is intended for use with Ruby LSP, it targets only Prism.
# If there is no reuse of a pre-parsed result, such as in Ruby LSP,
# regular parsing with Prism occurs, and `else` branch will be executed.
prism_reparsed = PrismPreparsed.new(prism_result)
parser_class.new(builder, parser: prism_reparsed)
else
parser_class.new(builder)
end

parser_instance.tap do |parser|
# On JRuby there's a risk that we hang in tokenize() if we
# don't set the all errors as fatal flag. The problem is caused by a bug
# in Racc that is discussed in issue #93 of the whitequark/parser
Expand All @@ -341,6 +384,7 @@ def create_parser(ruby_version, parser_engine)
end
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

def first_token_index(range_or_node)
begin_pos = source_range(range_or_node).begin_pos
Expand Down
49 changes: 48 additions & 1 deletion spec/rubocop/ast/processed_source_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# frozen_string_literal: true

require 'prism'

RSpec.describe RuboCop::AST::ProcessedSource do
subject(:processed_source) do
described_class.new(source, ruby_version, path, parser_engine: parser_engine)
described_class.new(
source, ruby_version, path, parser_engine: parser_engine, prism_result: prism_result
)
end

let(:source) { <<~RUBY }
Expand All @@ -12,6 +16,9 @@ def some_method
end
some_method
RUBY
let(:prism_result) { nil }
let(:prism_parse_lex_result) { Prism.parse_lex(source) }

let(:ast) { processed_source.ast }
let(:path) { 'ast/and_node_spec.rb' }

Expand Down Expand Up @@ -59,6 +66,16 @@ def some_method

it_behaves_like 'invalid parser_engine'
end

context 'when using `parser_engine: :parser_prism` and `prism_result` with a `ParseLexResult`' do
let(:ruby_version) { 3.4 }
let(:parser_prism) { :parser_prism }
let(:prism_result) { prism_parse_lex_result }

it 'returns an instance of ProcessedSource' do
is_expected.to be_a(described_class)
end
end
end

describe '.from_file' do
Expand Down Expand Up @@ -94,18 +111,48 @@ def some_method
it 'is the path passed to .new' do
expect(processed_source.path).to eq(path)
end

context 'when using `parser_engine: :parser_prism` and `prism_result` with a `ParseLexResult`' do
let(:ruby_version) { 3.4 }
let(:parser_engine) { :parser_prism }
let(:prism_result) { prism_parse_lex_result }

it 'is the path passed to .new' do
expect(processed_source.path).to eq(path)
end
end
end

describe '#buffer' do
it 'is a source buffer' do
expect(processed_source.buffer).to be_a(Parser::Source::Buffer)
end

context 'when using `parser_engine: :parser_prism` and `prism_result` with a `ParseLexResult`' do
let(:ruby_version) { 3.4 }
let(:parser_engine) { :parser_prism }
let(:prism_result) { prism_parse_lex_result }

it 'is a source buffer' do
expect(processed_source.buffer).to be_a(Parser::Source::Buffer)
end
end
end

describe '#ast' do
it 'is the root node of AST' do
expect(processed_source.ast).to be_a(RuboCop::AST::Node)
end

context 'when using `parser_engine: :parser_prism` and `prism_result` with a `ParseLexResult`' do
let(:ruby_version) { 3.4 }
let(:parser_engine) { :parser_prism }
let(:prism_result) { prism_parse_lex_result }

it 'is the root node of AST' do
expect(processed_source.ast).to be_a(RuboCop::AST::Node)
end
end
end

describe '#comments' do
Expand Down