summaryrefslogtreecommitdiff
path: root/lib/error_highlight
diff options
context:
space:
mode:
authorYusuke Endoh <mame@ruby-lang.org>2021-06-28 13:27:35 +0900
committerYusuke Endoh <mame@ruby-lang.org>2021-06-29 23:45:49 +0900
commit9438c99590f5476a81cee8b4cf2de25084a40b42 (patch)
treec7416588e060d079a082fb62b3a96e40164e4268 /lib/error_highlight
parente94604966572bb43fc887856d54aa54b8e9f7719 (diff)
Rename error_squiggle to error_highlight
Notes
Notes: Merged: https://github.com/ruby/ruby/pull/4586
Diffstat (limited to 'lib/error_highlight')
-rw-r--r--lib/error_highlight/base.rb446
-rw-r--r--lib/error_highlight/core_ext.rb48
-rw-r--r--lib/error_highlight/version.rb3
3 files changed, 497 insertions, 0 deletions
diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb
new file mode 100644
index 0000000000..49c772501c
--- /dev/null
+++ b/lib/error_highlight/base.rb
@@ -0,0 +1,446 @@
+require_relative "version"
+
+module ErrorHighlight
+ # 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
+ end
+
+ # Example:
+ # foo(42)
+ # ^^
+ # foo 42
+ # ^^
+ 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
+ end
+ end
+
+ # Example:
+ # foo
+ # ^^^
+ def spot_vcall
+ if @node.first_lineno == @node.last_lineno
+ fetch_line(@node.last_lineno)
+ @beg_column = @node.first_column
+ @end_column = @node.last_column
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^ (for [])
+ # x[1] += 42
+ # ^ (for +)
+ # x[1] += 42
+ # ^^^^^^ (for []=)
+ def spot_op_asgn1_for_name
+ nd_recv, op, nd_args, _nd_rhs = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @line.match(/\G\s*(\[)/, nd_recv.last_column)
+ bracket_beg_column = $~.begin(1)
+ args_last_column = $~.end(0)
+ if nd_args && nd_recv.last_lineno == nd_args.last_lineno
+ args_last_column = nd_args.last_column
+ end
+ if @line.match(/\s*\](\s*)(#{ Regexp.quote(op) })=()/, args_last_column)
+ case @name
+ when :[], :[]=
+ @beg_column = bracket_beg_column
+ @end_column = $~.begin(@name == :[] ? 1 : 3)
+ when op
+ @beg_column = $~.begin(2)
+ @end_column = $~.end(2)
+ end
+ end
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^^^^^^
+ def spot_op_asgn1_for_args
+ nd_recv, mid, nd_args, nd_rhs = @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_rhs.last_lineno
+ @end_column = nd_rhs.last_column
+ end
+ elsif nd_args && nd_args.first_lineno == nd_rhs.last_lineno
+ @beg_column = nd_args.first_column
+ @end_column = nd_rhs.last_column
+ end
+ # TODO: support @arg
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^^ (for foo)
+ # x.foo += 42
+ # ^ (for +)
+ # x.foo += 42
+ # ^^^^^^^ (for foo=)
+ def spot_op_asgn2_for_name
+ nd_recv, _qcall, attr, op, _nd_rhs = @node.children
+ fetch_line(nd_recv.last_lineno)
+ if @line.match(/\G\s*(\.)\s*#{ Regexp.quote(attr) }()\s*(#{ Regexp.quote(op) })(=)/, nd_recv.last_column)
+ case @name
+ when attr
+ @beg_column = $~.begin(1)
+ @end_column = $~.begin(2)
+ when op
+ @beg_column = $~.begin(3)
+ @end_column = $~.end(3)
+ when :"#{ attr }="
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(4)
+ end
+ end
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^
+ def spot_op_asgn2_for_args
+ _nd_recv, _qcall, _attr, _op, nd_rhs = @node.children
+ if nd_rhs.first_lineno == nd_rhs.last_lineno
+ fetch_line(nd_rhs.first_lineno)
+ @beg_column = nd_rhs.first_column
+ @end_column = nd_rhs.last_column
+ end
+ end
+
+ # Example:
+ # Foo::Bar
+ # ^^^^^
+ def spot_colon2
+ nd_parent, const = @node.children
+ if nd_parent.last_lineno == @node.last_lineno
+ fetch_line(nd_parent.last_lineno)
+ @beg_column = nd_parent.last_column
+ @end_column = @node.last_column
+ else
+ @line = @fetch[@node.last_lineno]
+ if @line[...@node.last_column].match(/#{ Regexp.quote(const) }\z/)
+ @beg_column = $~.begin(0)
+ @end_column = $~.end(0)
+ end
+ end
+ end
+
+ # Example:
+ # Foo::Bar += 1
+ # ^^^^^^^^
+ def spot_op_cdecl
+ nd_lhs, op, _nd_rhs = @node.children
+ *nd_parent_lhs, _const = nd_lhs.children
+ if @name == op
+ @line = @fetch[nd_lhs.last_lineno]
+ if @line.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column)
+ @beg_column = $~.begin(1)
+ @end_column = $~.end(1)
+ end
+ else
+ # constant access error
+ @end_column = nd_lhs.last_column
+ if nd_parent_lhs.empty? # example: ::C += 1
+ if nd_lhs.first_lineno == nd_lhs.last_lineno
+ @line = @fetch[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
+ @line = @fetch[nd_lhs.last_lineno]
+ @beg_column = nd_parent_lhs.last.last_column
+ end
+ end
+ end
+ end
+
+ def fetch_line(lineno)
+ @beg_lineno = @end_lineno = lineno
+ @line = @fetch[lineno]
+ end
+ end
+
+ private_constant :Spotter
+end
diff --git a/lib/error_highlight/core_ext.rb b/lib/error_highlight/core_ext.rb
new file mode 100644
index 0000000000..ad1c15a485
--- /dev/null
+++ b/lib/error_highlight/core_ext.rb
@@ -0,0 +1,48 @@
+module ErrorHighlight
+ module CoreExt
+ SKIP_TO_S_FOR_SUPER_LOOKUP = true
+ private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP
+
+ def to_s
+ msg = super.dup
+
+ locs = backtrace_locations
+ return msg unless locs
+
+ loc = locs.first
+ begin
+ node = RubyVM::AbstractSyntaxTree.of(loc, save_script_lines: true)
+ opts = {}
+
+ case self
+ when NoMethodError, NameError
+ point = :name
+ opts[:name] = name
+ when TypeError, ArgumentError
+ point = :args
+ end
+
+ spot = ErrorHighlight.spot(node, point, **opts) do |lineno, last_lineno|
+ last_lineno ||= lineno
+ node.script_lines[lineno - 1 .. last_lineno - 1].join("")
+ end
+
+ rescue Errno::ENOENT
+ end
+
+ if spot
+ marker = " " * spot[:first_column] + "^" * (spot[:last_column] - spot[:first_column])
+ points = "\n\n#{ spot[:line] }#{ marker }"
+ msg << points if !msg.include?(points)
+ end
+
+ msg
+ end
+ end
+
+ NameError.prepend(CoreExt)
+
+ # temporarily disabled
+ #TypeError.prepend(CoreExt)
+ #ArgumentError.prepend(CoreExt)
+end
diff --git a/lib/error_highlight/version.rb b/lib/error_highlight/version.rb
new file mode 100644
index 0000000000..c41ebd94f1
--- /dev/null
+++ b/lib/error_highlight/version.rb
@@ -0,0 +1,3 @@
+module ErrorHighlight
+ VERSION = "0.1.0"
+end