diff options
Diffstat (limited to 'lib/error_highlight/base.rb')
| -rw-r--r-- | lib/error_highlight/base.rb | 243 |
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 |
