[ruby/error_highlight] Support for the prism compiler
https://github.com/ruby/error_highlight/commit/69fbacfd49
This commit is contained in:
parent
0e5640b56f
commit
40ec860de7
@ -60,14 +60,14 @@ module ErrorHighlight
|
|||||||
rescue RuntimeError => error
|
rescue RuntimeError => error
|
||||||
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
|
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
|
||||||
# includes "prism" when the ISEQ was compiled with the prism compiler.
|
# 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
|
# In this case, we'll try to parse again with prism instead.
|
||||||
# reparse with the prism parser and pass the parsed node to Spotter.
|
|
||||||
raise unless error.message.include?("prism")
|
raise unless error.message.include?("prism")
|
||||||
|
prism_find(loc, **opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
Spotter.new(node, **opts).spot
|
Spotter.new(node, **opts).spot
|
||||||
|
|
||||||
when RubyVM::AbstractSyntaxTree::Node
|
when RubyVM::AbstractSyntaxTree::Node, Prism::Node
|
||||||
Spotter.new(obj, **opts).spot
|
Spotter.new(obj, **opts).spot
|
||||||
|
|
||||||
else
|
else
|
||||||
@ -81,6 +81,71 @@ module ErrorHighlight
|
|||||||
return nil
|
return nil
|
||||||
end
|
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 Spotter
|
||||||
class NonAscii < Exception; end
|
class NonAscii < Exception; end
|
||||||
private_constant :NonAscii
|
private_constant :NonAscii
|
||||||
@ -205,6 +270,48 @@ module ErrorHighlight
|
|||||||
|
|
||||||
when :OP_CDECL
|
when :OP_CDECL
|
||||||
spot_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
|
end
|
||||||
|
|
||||||
if @snippet && @beg_column && @end_column && @beg_column < @end_column
|
if @snippet && @beg_column && @end_column && @beg_column < @end_column
|
||||||
@ -548,6 +655,200 @@ module ErrorHighlight
|
|||||||
@beg_lineno = @end_lineno = lineno
|
@beg_lineno = @end_lineno = lineno
|
||||||
@snippet = @fetch[lineno]
|
@snippet = @fetch[lineno]
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
private_constant :Spotter
|
private_constant :Spotter
|
||||||
|
@ -5,6 +5,13 @@ require "did_you_mean"
|
|||||||
require "tempfile"
|
require "tempfile"
|
||||||
|
|
||||||
class ErrorHighlightTest < Test::Unit::TestCase
|
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
|
class DummyFormatter
|
||||||
def self.message_for(corrections)
|
def self.message_for(corrections)
|
||||||
""
|
""
|
||||||
@ -869,7 +876,7 @@ uninitialized constant ErrorHighlightTest::NotDefined
|
|||||||
end
|
end
|
||||||
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
|
def test_COLON2_5
|
||||||
# Unfortunately, we cannot identify which `NotDefined` caused the NameError
|
# Unfortunately, we cannot identify which `NotDefined` caused the NameError
|
||||||
assert_error_message(NameError, <<~END) do
|
assert_error_message(NameError, <<~END) do
|
||||||
@ -1335,6 +1342,7 @@ undefined method `foo' for #{ NIL_RECV_MESSAGE }
|
|||||||
|
|
||||||
def test_spot_with_node
|
def test_spot_with_node
|
||||||
omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)
|
omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)
|
||||||
|
omit if ErrorHighlightTest.compiling_with_prism?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
raise_name_error
|
raise_name_error
|
||||||
|
Loading…
x
Reference in New Issue
Block a user