diff options
author | Yusuke Endoh <[email protected]> | 2021-06-28 13:27:35 +0900 |
---|---|---|
committer | Yusuke Endoh <[email protected]> | 2021-06-29 23:45:49 +0900 |
commit | 9438c99590f5476a81cee8b4cf2de25084a40b42 (patch) | |
tree | c7416588e060d079a082fb62b3a96e40164e4268 /lib/error_highlight | |
parent | e94604966572bb43fc887856d54aa54b8e9f7719 (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.rb | 446 | ||||
-rw-r--r-- | lib/error_highlight/core_ext.rb | 48 | ||||
-rw-r--r-- | lib/error_highlight/version.rb | 3 |
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[[email protected]_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 |