Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -9221,6 +9221,7 @@ compile_builtin_mandatory_only_method(rb_iseq_t *iseq, const NODE *node, const N
rb_node_init(RNODE(&scope_node), NODE_SCOPE);
scope_node.nd_tbl = tbl;
scope_node.nd_body = mandatory_node(iseq, node);
scope_node.nd_parent = NULL;
scope_node.nd_args = &args_node;

VALUE ast_value = rb_ruby_ast_new(RNODE(&scope_node));
Expand Down
14 changes: 13 additions & 1 deletion iseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,18 @@ new_arena(void)
return new_arena;
}

static int
prepare_node_id(const NODE *node)
{
if (!node) return -1;

if (nd_type(node) == NODE_SCOPE && RNODE_SCOPE(node)->nd_parent) {
return nd_node_id(RNODE_SCOPE(node)->nd_parent);
}

return nd_node_id(node);
}

static VALUE
prepare_iseq_build(rb_iseq_t *iseq,
VALUE name, VALUE path, VALUE realpath, int first_lineno, const rb_code_location_t *code_location, const int node_id,
Expand Down Expand Up @@ -1031,7 +1043,7 @@ rb_iseq_new_with_opt(VALUE ast_value, VALUE name, VALUE path, VALUE realpath,
script_lines = ISEQ_BODY(parent)->variable.script_lines;
}

prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, node ? nd_node_id(node) : -1,
prepare_iseq_build(iseq, name, path, realpath, first_lineno, node ? &node->nd_loc : NULL, prepare_node_id(node),
parent, isolated_depth, type, script_lines, option);

rb_iseq_compile_node(iseq, node);
Expand Down
187 changes: 147 additions & 40 deletions lib/error_highlight/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,56 +122,51 @@ def initialize(node, point_type: :name, name: nil)
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
Expand Down Expand Up @@ -239,6 +234,20 @@ def spot
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
Expand Down Expand Up @@ -280,6 +289,30 @@ def spot
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
Expand Down Expand Up @@ -621,6 +654,55 @@ def spot_op_cdecl
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]
Expand Down Expand Up @@ -826,6 +908,31 @@ def prism_spot_constant_path_operator_write
prism_location(@node.binary_operator_loc.chop)
end
end

# Example:
# def foo()
# ^^^
def prism_spot_def_for_name
location = @node.name_loc
location = location.join(@node.operator_loc) 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
Expand Down
35 changes: 32 additions & 3 deletions lib/error_highlight/core_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,38 @@
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|unknown keyword|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
out += "\n (cannot create a snippet of the method definition; use Ruby 3.5 or later)"
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)
Expand Down
2 changes: 1 addition & 1 deletion lib/error_highlight/error_highlight.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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)/}) }
Expand Down
Loading