diff options
Diffstat (limited to 'lib/error_highlight')
| -rw-r--r-- | lib/error_highlight/base.rb | 198 | ||||
| -rw-r--r-- | lib/error_highlight/core_ext.rb | 35 | ||||
| -rw-r--r-- | lib/error_highlight/error_highlight.gemspec | 2 | ||||
| -rw-r--r-- | lib/error_highlight/formatter.rb | 4 | ||||
| -rw-r--r-- | lib/error_highlight/version.rb | 2 |
5 files changed, 188 insertions, 53 deletions
diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb index 14e0ce5785..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 @@ -113,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? @@ -122,56 +122,51 @@ 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 - # 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 - node2, const = node.children - subnodes << node if const == @name - node = node2 - end - if node.type == :CONST || node.type == :COLON3 - if node.children.first == @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 - else - # 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 + # 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 + node2, const = node.children + subnodes << node if const == @name + node = node2 + end + if node.type == :CONST || node.type == :COLON3 + if node.children.first == @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 + else + # 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 @@ -239,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 @@ -280,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 @@ -471,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 @@ -621,6 +653,55 @@ module ErrorHighlight 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] @@ -826,6 +907,31 @@ module ErrorHighlight 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 private_constant :Spotter diff --git a/lib/error_highlight/core_ext.rb b/lib/error_highlight/core_ext.rb index b69093f74e..c3354f46cd 100644 --- a/lib/error_highlight/core_ext.rb +++ b/lib/error_highlight/core_ext.rb @@ -3,9 +3,38 @@ require_relative "formatter" module ErrorHighlight module CoreExt private def generate_snippet - spot = ErrorHighlight.spot(self) - return "" unless spot - return ErrorHighlight.formatter.message_for(spot) + if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/ + locs = self.backtrace_locations + return "" if locs.size < 2 + callee_loc, caller_loc = locs + callee_spot = ErrorHighlight.spot(self, backtrace_location: callee_loc, point_type: :name) + caller_spot = ErrorHighlight.spot(self, backtrace_location: caller_loc, point_type: :name) + if caller_spot && callee_spot && + caller_loc.path == callee_loc.path && + caller_loc.lineno == callee_loc.lineno && + caller_spot == callee_spot + callee_loc = callee_spot = nil + end + ret = +"\n" + [["caller", caller_loc, caller_spot], ["callee", callee_loc, callee_spot]].each do |header, loc, spot| + out = nil + if loc + out = " #{ header }: #{ loc.path }:#{ loc.lineno }" + if spot + _, _, snippet, highlight = ErrorHighlight.formatter.message_for(spot).lines + out += "\n | #{ snippet } #{ highlight }" + else + # do nothing + end + end + ret << "\n" + out if out + end + ret + else + spot = ErrorHighlight.spot(self) + return "" unless spot + return ErrorHighlight.formatter.message_for(spot) + end end if Exception.method_defined?(:detailed_message) diff --git a/lib/error_highlight/error_highlight.gemspec b/lib/error_highlight/error_highlight.gemspec index b2da18df83..edfc4b776f 100644 --- a/lib/error_highlight/error_highlight.gemspec +++ b/lib/error_highlight/error_highlight.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/error_highlight" spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0.dev") + spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0") spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } diff --git a/lib/error_highlight/formatter.rb b/lib/error_highlight/formatter.rb index 5180576405..d2fad9e75c 100644 --- a/lib/error_highlight/formatter.rb +++ b/lib/error_highlight/formatter.rb @@ -56,11 +56,11 @@ module ErrorHighlight end def self.terminal_width - # lazy load io/console, so it's not loaded when 'max_snippet_width' is set + # lazy load io/console to avoid loading it when 'max_snippet_width' is manually set require "io/console" $stderr.winsize[1] if $stderr.tty? rescue LoadError, NoMethodError, SystemCallError - # do not truncate when window size is not available + # skip truncation when terminal window size is unavailable end end diff --git a/lib/error_highlight/version.rb b/lib/error_highlight/version.rb index d7a29c7c1e..f0a5376b14 100644 --- a/lib/error_highlight/version.rb +++ b/lib/error_highlight/version.rb @@ -1,3 +1,3 @@ module ErrorHighlight - VERSION = "0.7.0" + VERSION = "0.7.1" end |
