ruby-changes:66629
From: Yusuke <ko1@a...>
Date: Tue, 29 Jun 2021 23:46:13 +0900 (JST)
Subject: [ruby-changes:66629] e946049665 (master): [WIP] add error_squiggle gem
https://git.ruby-lang.org/ruby.git/commit/?id=e946049665 From e94604966572bb43fc887856d54aa54b8e9f7719 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh <mame@r...> Date: Fri, 18 Jun 2021 17:11:39 +0900 Subject: [WIP] add error_squiggle gem ``` $ ./local/bin/ruby -e '1.time {}' -e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError) 1.time {} ^^^^^ Did you mean? times ``` https://bugs.ruby-lang.org/issues/17930 --- gem_prelude.rb | 6 + lib/error_squiggle.rb | 2 + lib/error_squiggle/base.rb | 446 ++++++++++ lib/error_squiggle/core_ext.rb | 48 ++ lib/error_squiggle/version.rb | 3 + ruby.c | 6 + spec/ruby/core/exception/no_method_error_spec.rb | 4 +- test/error_squiggle/test_error_squiggle.rb | 984 +++++++++++++++++++++++ test/ruby/marshaltestlib.rb | 2 +- test/ruby/test_marshal.rb | 2 +- test/ruby/test_module.rb | 2 +- test/ruby/test_name_error.rb | 2 +- test/ruby/test_nomethod_error.rb | 2 +- test/ruby/test_object.rb | 2 +- 14 files changed, 1503 insertions(+), 8 deletions(-) create mode 100644 lib/error_squiggle.rb create mode 100644 lib/error_squiggle/base.rb create mode 100644 lib/error_squiggle/core_ext.rb create mode 100644 lib/error_squiggle/version.rb create mode 100644 test/error_squiggle/test_error_squiggle.rb diff --git a/gem_prelude.rb b/gem_prelude.rb index c4debb6..f5616e6 100644 --- a/gem_prelude.rb +++ b/gem_prelude.rb @@ -5,6 +5,12 @@ rescue LoadError https://github.com/ruby/ruby/blob/trunk/gem_prelude.rb#L5 end if defined?(Gem) begin + require 'error_squiggle' +rescue LoadError + warn "`error_squiggle' was not loaded." +end if defined?(ErrorSquiggle) + +begin require 'did_you_mean' rescue LoadError warn "`did_you_mean' was not loaded." diff --git a/lib/error_squiggle.rb b/lib/error_squiggle.rb new file mode 100644 index 0000000..02b0193 --- /dev/null +++ b/lib/error_squiggle.rb @@ -0,0 +1,2 @@ https://github.com/ruby/ruby/blob/trunk/lib/error_squiggle.rb#L1 +require_relative "error_squiggle/base" +require_relative "error_squiggle/core_ext" diff --git a/lib/error_squiggle/base.rb b/lib/error_squiggle/base.rb new file mode 100644 index 0000000..1ff2db9 --- /dev/null +++ b/lib/error_squiggle/base.rb @@ -0,0 +1,446 @@ https://github.com/ruby/ruby/blob/trunk/lib/error_squiggle/base.rb#L1 +require_relative "version" + +module ErrorSquiggle + # Identify the code fragment that seems associated with a given error + # + # Arguments: + # node: RubyVM::AbstractSyntaxTree::Node + # point: :name | :args + # name: The name associated with the NameError/NoMethodError + # fetch: A block to fetch a specified code line (or lines) + # + # Returns: + # { + # first_lineno: Integer, + # first_column: Integer, + # last_lineno: Integer, + # last_column: Integer, + # line: String, + # } | nil + def self.spot(...) + Spotter.new(...).spot + end + + class Spotter + def initialize(node, point, name: nil, &fetch) + @node = node + @point = point + @name = name + + # Not-implemented-yet options + @arg = nil # Specify the index or keyword at which argument caused the TypeError/ArgumentError + @multiline = false # Allow multiline spot + + @fetch = fetch + end + + def spot + return nil unless @node + + case @node.type + + when :CALL, :QCALL + case @point + when :name + spot_call_for_name + when :args + spot_call_for_args + end + + when :ATTRASGN + case @point + when :name + spot_attrasgn_for_name + when :args + spot_attrasgn_for_args + end + + when :OPCALL + case @point + when :name + spot_opcall_for_name + when :args + spot_opcall_for_args + end + + when :FCALL + case @point + when :name + spot_fcall_for_name + when :args + spot_fcall_for_args + end + + when :VCALL + spot_vcall + + when :OP_ASGN1 + case @point + when :name + spot_op_asgn1_for_name + when :args + spot_op_asgn1_for_args + end + + when :OP_ASGN2 + case @point + when :name + spot_op_asgn2_for_name + when :args + spot_op_asgn2_for_args + end + + when :CONST + spot_vcall + + when :COLON2 + spot_colon2 + + when :COLON3 + spot_vcall + + when :OP_CDECL + spot_op_cdecl + end + + if @line && @beg_column && @end_column && @beg_column < @end_column + return { + first_lineno: @beg_lineno, + first_column: @beg_column, + last_lineno: @end_lineno, + last_column: @end_column, + line: @line, + } + else + return nil + end + end + + private + + # Example: + # x.foo + # ^^^^ + # x.foo(42) + # ^^^^ + # x&.foo + # ^^^^^ + # x[42] + # ^^^^ + # x += 1 + # ^ + def spot_call_for_name + nd_recv, mid, nd_args = @node.children + lineno = nd_recv.last_lineno + lines = @fetch[lineno, @node.last_lineno] + if mid == :[] && lines.match(/\G\s*(\[(?:\s*\])?)/, nd_recv.last_column) + @beg_column = $~.begin(1) + @line = lines[/.*\n/] + @beg_lineno = @end_lineno = lineno + if nd_args + if nd_recv.last_lineno == nd_args.last_lineno && @line.match(/\s*\]/, nd_args.last_column) + @end_column = $~.end(0) + end + else + if lines.match(/\G\s*?\[\s*\]/, nd_recv.last_column) + @end_column = $~.end(0) + end + end + elsif lines.match(/\G\s*?(\&?\.)(\s*?)(#{ Regexp.quote(mid) }).*\n/, nd_recv.last_column) + lines = $` + $& + @beg_column = $~.begin($2.include?("\n") ? 3 : 1) + @end_column = $~.end(3) + if i = lines[..@beg_column].rindex("\n") + @beg_lineno = @end_lineno = lineno + lines[..@beg_column].count("\n") + @line = lines[i + 1..] + @beg_column -= i + 1 + @end_column -= i + 1 + else + @line = lines + @beg_lineno = @end_lineno = lineno + end + elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column) + @line = $` + $& + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + + # Example: + # x.foo(42) + # ^^ + # x[42] + # ^^ + # x += 1 + # ^ + def spot_call_for_args + _nd_recv, _mid, nd_args = @node.children + if nd_args && nd_args.first_lineno == nd_args.last_lineno + fetch_line(nd_args.first_lineno) + @beg_column = nd_args.first_column + @end_column = nd_args.last_column + end + # TODO: support @arg + end + + # Example: + # x.foo = 1 + # ^^^^^^ + # x[42] = 1 + # ^^^^^^ + def spot_attrasgn_for_name + nd_recv, mid, nd_args = @node.children + *nd_args, _nd_last_arg, _nil = nd_args.children + fetch_line(nd_recv.last_lineno) + if mid == :[]= && @line.match(/\G\s*(\[)/, nd_recv.last_column) + @beg_column = $~.begin(1) + args_last_column = $~.end(0) + if nd_args.last && nd_recv.last_lineno == nd_args.last.last_lineno + args_last_column = nd_args.last.last_column + end + if @line.match(/\s*\]\s*=/, args_last_column) + @end_column = $~.end(0) + end + elsif @line.match(/\G\s*(\.\s*#{ Regexp.quote(mid.to_s.sub(/=\z/, "")) }\s*=)/, nd_recv.last_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + + # Example: + # x.foo = 1 + # ^ + # x[42] = 1 + # ^^^^^^^ + # x[] = 1 + # ^^^^^ + def spot_attrasgn_for_args + nd_recv, mid, nd_args = @node.children + fetch_line(nd_recv.last_lineno) + if mid == :[]= && @line.match(/\G\s*\[/, nd_recv.last_column) + @beg_column = $~.end(0) + if nd_recv.last_lineno == nd_args.last_lineno + @end_column = nd_args.last_column + end + elsif nd_args && nd_args.first_lineno == nd_args.last_lineno + @beg_column = nd_args.first_column + @end_column = nd_args.last_column + end + # TODO: support @arg + end + + # Example: + # x + 1 + # ^ + # +x + # ^ + def spot_opcall_for_name + nd_recv, op, nd_arg = @node.children + fetch_line(nd_recv.last_lineno) + if nd_arg + # binary operator + if @line.match(/\G\s*(#{ Regexp.quote(op) })/, nd_recv.last_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + else + # unary operator + if @line[...nd_recv.first_column].match(/(#{ Regexp.quote(op.to_s.sub(/@\z/, "")) })\s*\(?\s*\z/) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + end + + # Example: + # x + 1 + # ^ + def spot_opcall_for_args + _nd_recv, _op, nd_arg = @node.children + if nd_arg && nd_arg.first_lineno == nd_arg.last_lineno + # binary operator + fetch_line(nd_arg.first_lineno) + @beg_column = nd_arg.first_column + @end_column = nd_arg.last_column + end + end + + # Example: + # foo(42) + # ^^^ + # foo 42 + # ^^^ + def spot_fcall_for_name + mid, _nd_args = @node.children + fetch_line(@node.first_lineno) + if @line.match(/(#{ Regexp.quote(mid) })/, @node.first_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/