summaryrefslogtreecommitdiff
path: root/lib/error_highlight/base.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/error_highlight/base.rb')
-rw-r--r--lib/error_highlight/base.rb243
1 files changed, 163 insertions, 80 deletions
diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb
index 81b1b574de..5fffe5ec34 100644
--- a/lib/error_highlight/base.rb
+++ b/lib/error_highlight/base.rb
@@ -1,13 +1,13 @@
require_relative "version"
module ErrorHighlight
- # Identify the code fragment at that a given exception occurred.
+ # Identify the code fragment where a given exception occurred.
#
# Options:
#
# point_type: :name | :args
- # :name (default) points the method/variable name that the exception occurred.
- # :args points the arguments of the method call that the exception occurred.
+ # :name (default) points to the method/variable name where the exception occurred.
+ # :args points to the arguments of the method call where the exception occurred.
#
# backtrace_location: Thread::Backtrace::Location
# It locates the code fragment of the given backtrace_location.
@@ -28,7 +28,7 @@ module ErrorHighlight
# Currently, ErrorHighlight.spot only supports a single-line code fragment.
# Therefore, if the return value is not nil, first_lineno and last_lineno will have
# the same value. If the relevant code fragment spans multiple lines
- # (e.g., Array#[] of +ary[(newline)expr(newline)]+), the method will return nil.
+ # (e.g., Array#[] of <tt>ary[(newline)expr(newline)]</tt>), the method will return nil.
# This restriction may be removed in the future.
def self.spot(obj, **opts)
case obj
@@ -62,7 +62,7 @@ module ErrorHighlight
# includes "prism" when the ISEQ was compiled with the prism compiler.
# In this case, we'll try to parse again with prism instead.
raise unless error.message.include?("prism")
- prism_find(loc, **opts)
+ prism_find(loc)
end
Spotter.new(node, **opts).spot
@@ -82,66 +82,16 @@ module ErrorHighlight
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)
+ # corresponding to the backtrace location in the source code.
+ def self.prism_find(location)
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
+ return nil if Prism::VERSION < "1.0.0"
- # 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
+ absolute_path = location.absolute_path
+ return unless absolute_path
+
+ node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
+ Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id }
end
private_class_method :prism_find
@@ -163,7 +113,7 @@ module ErrorHighlight
snippet = @node.script_lines[lineno - 1 .. last_lineno - 1].join("")
snippet += "\n" unless snippet.end_with?("\n")
- # It require some work to support Unicode (or multibyte) characters.
+ # It requires some work to support Unicode (or multibyte) characters.
# Tentatively, we stop highlighting if the code snippet has non-ascii characters.
# See https://github.com/ruby/error_highlight/issues/4
raise NonAscii unless snippet.ascii_only?
@@ -172,19 +122,17 @@ module ErrorHighlight
end
end
- OPT_GETCONSTANT_PATH = (RUBY_VERSION.split(".").map {|s| s.to_i } <=> [3, 2]) >= 0
- private_constant :OPT_GETCONSTANT_PATH
-
def spot
return nil unless @node
- if OPT_GETCONSTANT_PATH && @node.type == :COLON2
- # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
- # is compiled to one instruction (opt_getconstant_path).
- # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
- # or `Foo::Bar` causes NameError.
- # So we try to spot the sub-node that causes the NameError by using
- # `NameError#name`.
+ # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
+ # is compiled to one instruction (opt_getconstant_path).
+ # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
+ # or `Foo::Bar` causes NameError.
+ # So we try to spot the sub-node that causes the NameError by using
+ # `NameError#name`.
+ case @node.type
+ when :COLON2
subnodes = []
node = @node
while node.type == :COLON2
@@ -204,6 +152,21 @@ module ErrorHighlight
# Do nothing; opt_getconstant_path is used only when the const base is
# NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
end
+ when :constant_path_node
+ subnodes = []
+ node = @node
+
+ begin
+ subnodes << node if node.name == @name
+ end while (node = node.parent).is_a?(Prism::ConstantPathNode)
+
+ if node.is_a?(Prism::ConstantReadNode) && node.name == @name
+ subnodes << node
+ end
+
+ # If we found only one sub-node whose name is equal to @name, use it
+ return nil if subnodes.size != 1
+ @node = subnodes.first
end
case @node.type
@@ -271,6 +234,20 @@ module ErrorHighlight
when :OP_CDECL
spot_op_cdecl
+ when :DEFN
+ raise NotImplementedError if @point_type != :name
+ spot_defn
+
+ when :DEFS
+ raise NotImplementedError if @point_type != :name
+ spot_defs
+
+ when :LAMBDA
+ spot_lambda
+
+ when :ITER
+ spot_iter
+
when :call_node
case @point_type
when :name
@@ -312,6 +289,30 @@ module ErrorHighlight
when :constant_path_operator_write_node
prism_spot_constant_path_operator_write
+ when :def_node
+ case @point_type
+ when :name
+ prism_spot_def_for_name
+ when :args
+ raise NotImplementedError
+ end
+
+ when :lambda_node
+ case @point_type
+ when :name
+ prism_spot_lambda_for_name
+ when :args
+ raise NotImplementedError
+ end
+
+ when :block_node
+ case @point_type
+ when :name
+ prism_spot_block_for_name
+ when :args
+ raise NotImplementedError
+ end
+
end
if @snippet && @beg_column && @end_column && @beg_column < @end_column
@@ -376,6 +377,7 @@ module ErrorHighlight
end
elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column)
@snippet = $` + $&
+ @beg_lineno = @end_lineno = lineno
@beg_column = $~.begin(1)
@end_column = $~.end(1)
end
@@ -502,7 +504,6 @@ module ErrorHighlight
def spot_fcall_for_args
_mid, nd_args = @node.children
if nd_args && nd_args.first_lineno == nd_args.last_lineno
- # binary operator
fetch_line(nd_args.first_lineno)
@beg_column = nd_args.first_column
@end_column = nd_args.last_column
@@ -614,8 +615,9 @@ module ErrorHighlight
@beg_column = nd_parent.last_column
@end_column = @node.last_column
else
- @snippet = @fetch[@node.last_lineno]
+ fetch_line(@node.last_lineno)
if @snippet[...@node.last_column].match(/#{ Regexp.quote(const) }\z/)
+ @beg_lineno = @end_lineno = @node.last_lineno
@beg_column = $~.begin(0)
@end_column = $~.end(0)
end
@@ -629,7 +631,7 @@ module ErrorHighlight
nd_lhs, op, _nd_rhs = @node.children
*nd_parent_lhs, _const = nd_lhs.children
if @name == op
- @snippet = @fetch[nd_lhs.last_lineno]
+ fetch_line(nd_lhs.last_lineno)
if @snippet.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column)
@beg_column = $~.begin(1)
@end_column = $~.end(1)
@@ -639,18 +641,67 @@ module ErrorHighlight
@end_column = nd_lhs.last_column
if nd_parent_lhs.empty? # example: ::C += 1
if nd_lhs.first_lineno == nd_lhs.last_lineno
- @snippet = @fetch[nd_lhs.last_lineno]
+ fetch_line(nd_lhs.last_lineno)
@beg_column = nd_lhs.first_column
end
else # example: Foo::Bar::C += 1
if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno
- @snippet = @fetch[nd_lhs.last_lineno]
+ fetch_line(nd_lhs.last_lineno)
@beg_column = nd_parent_lhs.last.last_column
end
end
end
end
+ # Example:
+ # def bar; end
+ # ^^^
+ def spot_defn
+ mid, = @node.children
+ fetch_line(@node.first_lineno)
+ if @snippet.match(/\Gdef\s+(#{ Regexp.quote(mid) }\b)/, @node.first_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # def Foo.bar; end
+ # ^^^^
+ def spot_defs
+ nd_recv, mid, = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @snippet.match(/\G\s*(\.\s*#{ Regexp.quote(mid) }\b)/, nd_recv.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ end
+
+ # Example:
+ # -> { ... }
+ # ^^
+ def spot_lambda
+ fetch_line(@node.first_lineno)
+ if @snippet.match(/\G->/, @node.first_column)
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+
+ # Example:
+ # lambda { ... }
+ # ^
+ # define_method :foo do
+ # ^^
+ def spot_iter
+ _nd_fcall, nd_scope = @node.children
+ fetch_line(nd_scope.first_lineno)
+ if @snippet.match(/\G(?:do\b|\{)/, nd_scope.first_column)
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+
def fetch_line(lineno)
@beg_lineno = @end_lineno = lineno
@snippet = @fetch[lineno]
@@ -732,6 +783,9 @@ module ErrorHighlight
# foo 42
# ^^
def prism_spot_call_for_args
+ # Disallow highlighting arguments if there are no arguments.
+ return if @node.arguments.nil?
+
# Explicitly turn off foo.() syntax because error_highlight expects this
# to not work.
return nil if @node.name == :call && @node.message_loc.nil?
@@ -847,7 +901,36 @@ module ErrorHighlight
# Foo::Bar += 1
# ^^^^^^^^
def prism_spot_constant_path_operator_write
- prism_location(@node.binary_operator_loc.chop)
+ if @name == (target = @node.target).name
+ prism_location(target.delimiter_loc.join(target.name_loc))
+ else
+ prism_location(@node.binary_operator_loc.chop)
+ end
+ end
+
+ # Example:
+ # def foo()
+ # ^^^
+ def prism_spot_def_for_name
+ location = @node.name_loc
+ location = @node.operator_loc.join(location) if @node.operator_loc
+ prism_location(location)
+ end
+
+ # Example:
+ # -> x, y { }
+ # ^^
+ def prism_spot_lambda_for_name
+ prism_location(@node.operator_loc)
+ end
+
+ # Example:
+ # lambda { }
+ # ^
+ # define_method :foo do |x, y|
+ # ^
+ def prism_spot_block_for_name
+ prism_location(@node.opening_loc)
end
end