[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]