[ruby/error_highlight] Support for the prism compiler

https://github.com/ruby/error_highlight/commit/69fbacfd49
This commit is contained in:
Kevin Newton 2024-05-20 13:50:03 -04:00 committed by git
parent 0e5640b56f
commit 40ec860de7
2 changed files with 313 additions and 4 deletions

View File

@ -60,14 +60,14 @@ module ErrorHighlight
rescue RuntimeError => error
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
# includes "prism" when the ISEQ was compiled with the prism compiler.
# In this case, we'll set the node to `nil`. In the future, we will
# reparse with the prism parser and pass the parsed node to Spotter.
# In this case, we'll try to parse again with prism instead.
raise unless error.message.include?("prism")
prism_find(loc, **opts)
end
Spotter.new(node, **opts).spot
when RubyVM::AbstractSyntaxTree::Node
when RubyVM::AbstractSyntaxTree::Node, Prism::Node
Spotter.new(obj, **opts).spot
else
@ -81,6 +81,71 @@ module ErrorHighlight
return nil
end
# Accepts a Thread::Backtrace::Location object and returns a Prism::Node
# corresponding to the location in the source code.
def self.prism_find(loc, point_type: :name, name: nil)
require "prism"
return nil if Prism::VERSION < "0.29.0"
path = loc.absolute_path
return unless path
lineno = loc.lineno
column = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc)
tunnel = Prism.parse_file(path).value.tunnel(lineno, column)
# Prism provides the Prism::Node#tunnel API to find all of the nodes that
# correspond to the given line and column in the source code, with the first
# node in the list being the top-most node and the last node in the list
# being the bottom-most node.
tunnel.each_with_index.reverse_each.find do |part, index|
case part
when Prism::CallNode, Prism::CallOperatorWriteNode, Prism::IndexOperatorWriteNode, Prism::LocalVariableOperatorWriteNode
# If we find any of these nodes, we can stop searching as these are the
# nodes that triggered the exceptions.
break part
when Prism::ConstantReadNode, Prism::ConstantPathNode
if index != 0 && tunnel[index - 1].is_a?(Prism::ConstantPathOperatorWriteNode)
# If we're inside of a constant path operator write node, then this
# constant path may be highlighting a couple of different kinds of
# parts.
if part.name == name
# Explicitly turn off Foo::Bar += 1 where Foo and Bar are on
# different lines because error highlight expects this to not work.
break nil if part.delimiter_loc.end_line != part.name_loc.start_line
# Otherwise, because we have matched the name we can return this
# part.
break part
end
# If we haven't matched the name, it's the operator that we're looking
# for, and we can return the parent node here.
break tunnel[index - 1]
elsif part.name == name
# If we have matched the name of the constant, then we can return this
# inner node as the node that triggered the exception.
break part
else
# If we are at the beginning of the tunnel or we are at the beginning
# of a constant lookup chain, then we will return this node.
break part if index == 0 || !tunnel[index - 1].is_a?(Prism::ConstantPathNode)
end
when Prism::LocalVariableReadNode, Prism::ParenthesesNode
# If we find any of these nodes, we want to continue searching up the
# tree because these nodes cannot trigger the exceptions.
false
else
# If we find a different kind of node that we haven't already handled,
# we don't know how to handle it so we'll stop searching and assume this
# is not an exception we can decorate.
break nil
end
end
end
private_class_method :prism_find
class Spotter
class NonAscii < Exception; end
private_constant :NonAscii
@ -205,6 +270,48 @@ module ErrorHighlight
when :OP_CDECL
spot_op_cdecl
when :call_node
case @point_type
when :name
prism_spot_call_for_name
when :args
prism_spot_call_for_args
end
when :local_variable_operator_write_node
case @point_type
when :name
prism_spot_local_variable_operator_write_for_name
when :args
prism_spot_local_variable_operator_write_for_args
end
when :call_operator_write_node
case @point_type
when :name
prism_spot_call_operator_write_for_name
when :args
prism_spot_call_operator_write_for_args
end
when :index_operator_write_node
case @point_type
when :name
prism_spot_index_operator_write_for_name
when :args
prism_spot_index_operator_write_for_args
end
when :constant_read_node
prism_spot_constant_read
when :constant_path_node
prism_spot_constant_path
when :constant_path_operator_write_node
prism_spot_constant_path_operator_write
end
if @snippet && @beg_column && @end_column && @beg_column < @end_column
@ -548,6 +655,200 @@ module ErrorHighlight
@beg_lineno = @end_lineno = lineno
@snippet = @fetch[lineno]
end
# Take a location from the prism parser and set the necessary instance
# variables.
def prism_location(location)
@beg_lineno = location.start_line
@beg_column = location.start_column
@end_lineno = location.end_line
@end_column = location.end_column
@snippet = @fetch[@beg_lineno, @end_lineno]
end
# Example:
# x.foo
# ^^^^
# x.foo(42)
# ^^^^
# x&.foo
# ^^^^^
# x[42]
# ^^^^
# x.foo = 1
# ^^^^^^
# x[42] = 1
# ^^^^^^
# x + 1
# ^
# +x
# ^
# foo(42)
# ^^^
# foo 42
# ^^^
# foo
# ^^^
def prism_spot_call_for_name
# Explicitly turn off foo.() syntax because error_highlight expects this
# to not work.
return nil if @node.name == :call && @node.message_loc.nil?
location = @node.message_loc || @node.call_operator_loc || @node.location
location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line
# If the method name ends with "=" but the message does not, then this is
# a method call using the "attribute assignment" syntax
# (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
# add it to the location.
if (name = @node.name).end_with?("=") && !@node.message.end_with?("=")
location = location.adjoin("=")
end
prism_location(location)
if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/)
# If the method name is an operator, then error_highlight only
# highlights the first line.
fetch_line(location.start_line)
end
end
# Example:
# x.foo(42)
# ^^
# x[42]
# ^^
# x.foo = 1
# ^
# x[42] = 1
# ^^^^^^^
# x[] = 1
# ^^^^^
# x + 1
# ^
# foo(42)
# ^^
# foo 42
# ^^
def prism_spot_call_for_args
# Explicitly turn off foo.() syntax because error_highlight expects this
# to not work.
return nil if @node.name == :call && @node.message_loc.nil?
if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1
prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location))
else
prism_location(@node.arguments.location)
end
end
# Example:
# x += 1
# ^
def prism_spot_local_variable_operator_write_for_name
prism_location(@node.binary_operator_loc.chop)
end
# Example:
# x += 1
# ^
def prism_spot_local_variable_operator_write_for_args
prism_location(@node.value.location)
end
# Example:
# x.foo += 42
# ^^^ (for foo)
# x.foo += 42
# ^ (for +)
# x.foo += 42
# ^^^^^^^ (for foo=)
def prism_spot_call_operator_write_for_name
if !@name.start_with?(/[[:alpha:]_]/)
prism_location(@node.binary_operator_loc.chop)
else
location = @node.message_loc
if @node.call_operator_loc.start_line == location.start_line
location = @node.call_operator_loc.join(location)
end
location = location.adjoin("=") if @name.end_with?("=")
prism_location(location)
end
end
# Example:
# x.foo += 42
# ^^
def prism_spot_call_operator_write_for_args
prism_location(@node.value.location)
end
# Example:
# x[1] += 42
# ^^^ (for [])
# x[1] += 42
# ^ (for +)
# x[1] += 42
# ^^^^^^ (for []=)
def prism_spot_index_operator_write_for_name
case @name
when :[]
prism_location(@node.opening_loc.join(@node.closing_loc))
when :[]=
prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("="))
else
# Explicitly turn off foo[] += 1 syntax when the operator is not on
# the same line because error_highlight expects this to not work.
return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line
prism_location(@node.binary_operator_loc.chop)
end
end
# Example:
# x[1] += 42
# ^^^^^^^^
def prism_spot_index_operator_write_for_args
opening_loc =
if @node.arguments.nil?
@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1)
else
@node.arguments.location
end
prism_location(opening_loc.join(@node.value.location))
end
# Example:
# Foo
# ^^^
def prism_spot_constant_read
prism_location(@node.location)
end
# Example:
# Foo::Bar
# ^^^^^
def prism_spot_constant_path
if @node.parent && @node.parent.location.end_line == @node.location.end_line
fetch_line(@node.parent.location.end_line)
prism_location(@node.delimiter_loc.join(@node.name_loc))
else
fetch_line(@node.location.end_line)
location = @node.name_loc
location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line
prism_location(location)
end
end
# Example:
# Foo::Bar += 1
# ^^^^^^^^
def prism_spot_constant_path_operator_write
prism_location(@node.binary_operator_loc.chop)
end
end
private_constant :Spotter

View File

@ -5,6 +5,13 @@ require "did_you_mean"
require "tempfile"
class ErrorHighlightTest < Test::Unit::TestCase
# We can't revisit instruction sequences to find node ids if the prism
# compiler was used instead of the parse.y compiler. In that case, we'll omit
# some tests.
def self.compiling_with_prism?
RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
end
class DummyFormatter
def self.message_for(corrections)
""
@ -869,7 +876,7 @@ uninitialized constant ErrorHighlightTest::NotDefined
end
end
if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH)
if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH) && !compiling_with_prism?
def test_COLON2_5
# Unfortunately, we cannot identify which `NotDefined` caused the NameError
assert_error_message(NameError, <<~END) do
@ -1335,6 +1342,7 @@ undefined method `foo' for #{ NIL_RECV_MESSAGE }
def test_spot_with_node
omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)
omit if ErrorHighlightTest.compiling_with_prism?
begin
raise_name_error