Skip to content

Commit 3095014

Browse files
committed
Use translated Prism AST to run RuboCop
1 parent fd1ac60 commit 3095014

File tree

7 files changed

+145
-8
lines changed

7 files changed

+145
-8
lines changed

.rubocop.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require:
99
- ./lib/rubocop/cop/ruby_lsp/use_register_with_handler_method
1010

1111
AllCops:
12+
ParserEngine: parser_prism
1213
NewCops: disable
1314
SuggestExtensions: false
1415
Include:

lib/ruby_lsp/document.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def initialize(source:, version:, uri:, encoding: Constant::PositionEncodingKind
3636

3737
sig { returns(Prism::ProgramNode) }
3838
def tree
39-
@parse_result.value
39+
T.unsafe(@parse_result.value).first
4040
end
4141

4242
sig { returns(T::Array[Prism::Comment]) }
@@ -113,7 +113,7 @@ def create_scanner
113113
).returns([T.nilable(Prism::Node), T.nilable(Prism::Node), T::Array[String]])
114114
end
115115
def locate_node(position, node_types: [])
116-
locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
116+
locate(T.unsafe(@parse_result.value).first, create_scanner.find_char_position(position), node_types: node_types)
117117
end
118118

119119
sig do

lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def initialize
2222
def run(uri, document)
2323
filename = T.must(uri.to_standardized_path || uri.opaque)
2424
# Invoke RuboCop with just this file in `paths`
25-
@runner.run(filename, document.source)
25+
@runner.run(filename, document.source, document.parse_result)
2626

2727
@runner.offenses.map do |offense|
2828
Support::RuboCopDiagnostic.new(document, offense, uri)

lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def run(uri, document)
2525
filename = T.must(uri.to_standardized_path || uri.opaque)
2626

2727
# Invoke RuboCop with just this file in `paths`
28-
@runner.run(filename, document.source)
28+
@runner.run(filename, document.source, document.parse_result)
2929

3030
@runner.formatted_source
3131
end

lib/ruby_lsp/requests/support/rubocop_runner.rb

+133-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# typed: strict
1+
# typed: true
22
# frozen_string_literal: true
33

44
begin
@@ -17,6 +17,8 @@
1717
RuboCop::LSP.enable
1818
end
1919

20+
require "prism/translation/parser/rubocop"
21+
2022
module RubyLsp
2123
module Requests
2224
module Support
@@ -74,6 +76,7 @@ def initialize(*args)
7476
@offenses = T.let([], T::Array[RuboCop::Cop::Offense])
7577
@errors = T.let([], T::Array[String])
7678
@warnings = T.let([], T::Array[String])
79+
@parse_result = T.let(nil, T.nilable(Prism::ParseResult))
7780

7881
args += DEFAULT_ARGS
7982
rubocop_options = ::RuboCop::Options.new.parse(args).first
@@ -82,14 +85,15 @@ def initialize(*args)
8285
super(rubocop_options, config_store)
8386
end
8487

85-
sig { params(path: String, contents: String).void }
86-
def run(path, contents)
88+
sig { params(path: String, contents: String, parse_result: Prism::ParseResult).void }
89+
def run(path, contents, parse_result)
8790
# Clear Runner state between runs since we get a single instance of this class
8891
# on every use site.
8992
@errors = []
9093
@warnings = []
9194
@offenses = []
9295
@options[:stdin] = contents
96+
@parse_result = parse_result
9397

9498
super([path])
9599

@@ -109,6 +113,28 @@ def formatted_source
109113
@options[:stdin]
110114
end
111115

116+
sig { params(file: String).returns(RuboCop::ProcessedSource) }
117+
def get_processed_source(file)
118+
config = @config_store.for_file(file)
119+
parser_engine = config.parser_engine
120+
return super unless parser_engine == :parser_prism
121+
122+
processed_source = T.unsafe(::RuboCop::AST::ProcessedSource).new(
123+
@options[:stdin],
124+
Prism::Translation::Parser::VERSION_3_3,
125+
file,
126+
parser_engine: parser_engine,
127+
prism_result: @parse_result,
128+
)
129+
processed_source.config = config
130+
processed_source.registry = mobilized_cop_classes(config)
131+
# We have to reset the result to nil after returning the processed source the first time. This is needed for
132+
# formatting because RuboCop will keep re-parsing the same file until no more auto-corrects can be applied. If
133+
# we didn't reset it, we would end up operating in a stale AST
134+
@parse_result = nil
135+
processed_source
136+
end
137+
112138
class << self
113139
extend T::Sig
114140

@@ -138,3 +164,107 @@ def file_finished(_file, offenses)
138164
end
139165
end
140166
end
167+
168+
# Processed Source patch so that we can pass the existing AST to RuboCop without having to re-parse files a second time
169+
module ProcessedSourcePatch
170+
extend T::Sig
171+
172+
sig do
173+
params(
174+
source: String,
175+
ruby_version: Float,
176+
path: T.nilable(String),
177+
parser_engine: Symbol,
178+
prism_result: T.nilable(Prism::ParseResult),
179+
).void
180+
end
181+
def initialize(source, ruby_version, path = nil, parser_engine: :parser_whitequark, prism_result: nil)
182+
@prism_result = prism_result
183+
184+
# Invoking super will end up invoking our patched version of tokenize, which avoids re-parsing the file
185+
super(source, Prism::Translation::Parser::VERSION_3_3, path, parser_engine: parser_engine)
186+
end
187+
188+
sig { params(parser: T.untyped).returns(T::Array[T.untyped]) }
189+
def tokenize(parser)
190+
begin
191+
ast, comments, tokens = parser.tokenize(@buffer, parse_result: @prism_result)
192+
ast ||= nil
193+
rescue Parser::SyntaxError
194+
comments = []
195+
tokens = []
196+
end
197+
198+
ast&.complete!
199+
tokens.map! { |t| RuboCop::AST::Token.from_parser_token(t) }
200+
201+
[ast, comments, tokens]
202+
end
203+
204+
RuboCop::AST::ProcessedSource.prepend(self)
205+
end
206+
207+
module Prism
208+
module Translation
209+
class Parser < ::Parser::Base
210+
extend T::Sig
211+
212+
sig do
213+
params(
214+
source_buffer: ::Parser::Source::Buffer,
215+
recover: T::Boolean,
216+
parse_result: T.nilable(Prism::ParseResult),
217+
).returns(T::Array[T.untyped])
218+
end
219+
def tokenize(source_buffer, recover = false, parse_result: nil)
220+
@source_buffer = T.let(source_buffer, T.nilable(::Parser::Source::Buffer))
221+
source = source_buffer.source
222+
223+
offset_cache = build_offset_cache(source)
224+
result = if @prism_result
225+
@prism_result
226+
else
227+
begin
228+
unwrap(
229+
Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version)),
230+
offset_cache,
231+
)
232+
rescue ::Parser::SyntaxError
233+
raise unless recover
234+
end
235+
end
236+
237+
program, tokens = result.value
238+
ast = build_ast(program, offset_cache) if result.success?
239+
240+
[
241+
ast,
242+
build_comments(result.comments, offset_cache),
243+
build_tokens(tokens, offset_cache),
244+
]
245+
ensure
246+
@source_buffer = nil
247+
end
248+
249+
module ProcessedSource
250+
extend T::Sig
251+
extend T::Helpers
252+
253+
requires_ancestor { Kernel }
254+
255+
sig { params(ruby_version: Float, parser_engine: Symbol).returns(T.untyped) }
256+
def parser_class(ruby_version, parser_engine)
257+
if ruby_version == Prism::Translation::Parser::VERSION_3_3
258+
require "prism/translation/parser33"
259+
Prism::Translation::Parser33
260+
elsif ruby_version == Prism::Translation::Parser::VERSION_3_4
261+
require "prism/translation/parser34"
262+
Prism::Translation::Parser34
263+
else
264+
super
265+
end
266+
end
267+
end
268+
end
269+
end
270+
end

lib/ruby_lsp/ruby_document.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def parse
88
return @parse_result unless @needs_parsing
99

1010
@needs_parsing = false
11-
@parse_result = Prism.parse(@source)
11+
@parse_result = Prism.parse_lex(@source)
1212
end
1313
end
1414
end

sorbet/rbi/shims/rubocop.rbi

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# typed: true
2+
3+
class Prism::Translation::Parser33; end
4+
class Prism::Translation::Parser34; end
5+
Prism::Translation::Parser::VERSION_3_3 = T.unsafe(nil)
6+
Prism::Translation::Parser::VERSION_3_4 = T.unsafe(nil)

0 commit comments

Comments
 (0)