module ErrorHighlight
Constants
- VERSION
Public Class Methods
formatter()
click to toggle source
# File lib/error_highlight/formatter.rb, line 16 def self.formatter Ractor.current[:__error_highlight_formatter__] || DefaultFormatter end
formatter=(formatter)
click to toggle source
# File lib/error_highlight/formatter.rb, line 20 def self.formatter=(formatter) Ractor.current[:__error_highlight_formatter__] = formatter end
spot(obj, **opts)
click to toggle source
Identify the code fragment at that 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.
backtrace_location: Thread::Backtrace::Location
It locates the code fragment of the given backtrace_location. By default, it uses the first frame of backtrace_locations of the given exception.
Returns:
{ first_lineno: Integer, first_column: Integer, last_lineno: Integer, last_column: Integer, snippet: String, script_lines: [String], } | nil
Limitations:
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+), the method will return nil. This restriction may be removed in the future.
# File lib/error_highlight/base.rb, line 33 def self.spot(obj, **opts) case obj when Exception exc = obj loc = opts[:backtrace_location] opts = { point_type: opts.fetch(:point_type, :name) } unless loc case exc when TypeError, ArgumentError opts[:point_type] = :args end locs = exc.backtrace_locations return nil unless locs loc = locs.first return nil unless loc opts[:name] = exc.name if NameError === obj end return nil unless Thread::Backtrace::Location === loc node = begin RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true) 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 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, Prism::Node Spotter.new(obj, **opts).spot else raise TypeError, "Exception is expected" end rescue SyntaxError, SystemCallError, # file not found or something ArgumentError # eval'ed code return nil end
Private Class Methods
prism_find(loc, point_type: :name, name: nil)
click to toggle source
Accepts a Thread::Backtrace::Location
object and returns a Prism::Node
corresponding to the location in the source code.
# File lib/error_highlight/base.rb, line 86 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