mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
[Feature #22212] Add Thread::Backtrace::Location#source_range
This commit is contained in:
1 parent
66c00fd536
commit
84a9e6bed7
13 files changed
+829
-55
Notes:
git
2026-08-06 11:21:08 +00:00
No files matched your search
@@ -113,6 +113,11 @@ Note: We're only listing outstanding class updates.
|
||||
|
||||
* `Symbol#to_s` now returns a frozen string. [[Feature #22137]]
|
||||
|
||||
* Thread::Backtrace::Location
|
||||
|
||||
* `Thread::Backtrace::Location#source_range` is added. It returns a
|
||||
`Ruby::SourceRange` for the Ruby expression associated with the frame.
|
||||
|
||||
## Stdlib updates
|
||||
|
||||
* Psych
|
||||
|
||||
@@ -179,15 +179,24 @@ rb_ast_parse_array(VALUE array, VALUE keep_script_lines, VALUE error_tolerant, V
|
||||
|
||||
static VALUE node_children(VALUE, const NODE*);
|
||||
|
||||
static VALUE
|
||||
node_find(VALUE self, const int node_id)
|
||||
struct node_find_result {
|
||||
VALUE node;
|
||||
VALUE parent;
|
||||
};
|
||||
|
||||
static bool
|
||||
node_find_with_parent(VALUE self, VALUE parent, const int node_id, struct node_find_result *result)
|
||||
{
|
||||
VALUE ary;
|
||||
long i;
|
||||
struct ASTNodeData *data;
|
||||
TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data);
|
||||
|
||||
if (nd_node_id(data->node) == node_id) return self;
|
||||
if (nd_node_id(data->node) == node_id) {
|
||||
result->node = self;
|
||||
result->parent = parent;
|
||||
return true;
|
||||
}
|
||||
|
||||
ary = node_children(data->ast_value, data->node);
|
||||
|
||||
@@ -195,12 +204,73 @@ node_find(VALUE self, const int node_id)
|
||||
VALUE child = RARRAY_AREF(ary, i);
|
||||
|
||||
if (CLASS_OF(child) == rb_cNode) {
|
||||
VALUE result = node_find(child, node_id);
|
||||
if (RTEST(result)) return result;
|
||||
if (node_find_with_parent(child, self, node_id, result)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Qnil;
|
||||
return false;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
node_find(VALUE self, const int node_id)
|
||||
{
|
||||
struct node_find_result result = { Qnil, Qnil };
|
||||
node_find_with_parent(self, Qnil, node_id, &result);
|
||||
return result.node;
|
||||
}
|
||||
|
||||
bool
|
||||
rb_ast_node_source_location(VALUE source, VALUE path, int first_lineno,
|
||||
int node_id, bool block_iseq, int iseq_node_id,
|
||||
rb_code_location_t *location)
|
||||
{
|
||||
VALUE ast;
|
||||
|
||||
if (NIL_P(source)) {
|
||||
ast = rb_ast_parse_file(path, Qfalse, Qfalse, Qfalse);
|
||||
}
|
||||
else {
|
||||
VALUE ast_value;
|
||||
VALUE vparser = setup_vparser(Qfalse, Qfalse, Qfalse);
|
||||
|
||||
if (RB_TYPE_P(source, T_ARRAY)) {
|
||||
ast_value = rb_parser_compile_array(vparser, path, source, first_lineno);
|
||||
}
|
||||
else {
|
||||
StringValue(source);
|
||||
ast_value = rb_parser_compile_string_path(vparser, path, source, first_lineno);
|
||||
}
|
||||
ast = ast_parse_done(ast_value);
|
||||
}
|
||||
|
||||
struct node_find_result result = { Qnil, Qnil };
|
||||
if (!node_find_with_parent(ast, Qnil, node_id, &result)) return false;
|
||||
|
||||
struct ASTNodeData *data;
|
||||
TypedData_Get_Struct(result.node, struct ASTNodeData, &rb_node_type, data);
|
||||
const NODE *node = data->node;
|
||||
|
||||
if (!NIL_P(result.parent)) {
|
||||
struct ASTNodeData *parent_data;
|
||||
TypedData_Get_Struct(result.parent, struct ASTNodeData, &rb_node_type, parent_data);
|
||||
const NODE *parent = parent_data->node;
|
||||
|
||||
/* Prism's call node includes its literal block. */
|
||||
if (nd_type(parent) == NODE_ITER && RNODE_ITER(parent)->nd_iter == node) {
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prism's block node excludes the call that produced the block. */
|
||||
if (block_iseq && node_id == iseq_node_id && nd_type(node) == NODE_ITER) {
|
||||
const NODE *scope = RNODE_ITER(node)->nd_body;
|
||||
if (scope && nd_type(scope) == NODE_SCOPE) {
|
||||
node = scope;
|
||||
}
|
||||
}
|
||||
|
||||
*location = *nd_code_loc(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
extern VALUE rb_e_script;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "ruby/ruby.h" /* for rb_block_call_func_t */
|
||||
#include "ruby/st.h" /* for st_index_t */
|
||||
struct rb_block; /* in vm_core.h */
|
||||
struct rb_code_location_struct; /* in rubyparser.h */
|
||||
struct rb_iseq_struct; /* in vm_core.h */
|
||||
|
||||
/* proc.c */
|
||||
@@ -21,6 +22,8 @@ int rb_block_arity(void);
|
||||
int rb_block_min_max_arity(int *max);
|
||||
VALUE rb_block_to_s(VALUE self, const struct rb_block *block, const char *additional_info);
|
||||
VALUE rb_callable_receiver(VALUE);
|
||||
VALUE rb_source_range_new(VALUE path, VALUE absolute_path,
|
||||
const struct rb_code_location_struct *location);
|
||||
|
||||
VALUE rb_func_proc_dup(VALUE src_obj);
|
||||
VALUE rb_func_lambda_new(rb_block_call_func_t func, VALUE val, int min_argc, int max_argc);
|
||||
|
||||
@@ -56,6 +56,9 @@ VALUE rb_parser_compile_string(VALUE, const char*, VALUE, int);
|
||||
VALUE rb_parser_compile_file_path(VALUE vparser, VALUE fname, VALUE input, int line);
|
||||
VALUE rb_parser_compile_generic(VALUE vparser, rb_parser_lex_gets_func *lex_gets, VALUE fname, VALUE input, int line);
|
||||
VALUE rb_parser_compile_array(VALUE vparser, VALUE fname, VALUE array, int start);
|
||||
bool rb_ast_node_source_location(VALUE source, VALUE path, int first_lineno,
|
||||
int node_id, bool block_iseq, int iseq_node_id,
|
||||
rb_code_location_t *location);
|
||||
|
||||
enum lex_state_bits {
|
||||
EXPR_BEG_bit, /* ignore newline, +/- is a sign. */
|
||||
|
||||
@@ -11051,6 +11051,63 @@ pm_parse_string(pm_parse_result_t *result, VALUE source, VALUE filepath, VALUE *
|
||||
return error;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint32_t node_id;
|
||||
const pm_node_t *node;
|
||||
} pm_node_find_context_t;
|
||||
|
||||
static bool
|
||||
pm_node_find(const pm_node_t *node, void *data)
|
||||
{
|
||||
pm_node_find_context_t *context = data;
|
||||
|
||||
if (context->node == NULL && node->node_id == context->node_id) {
|
||||
context->node = node;
|
||||
return false;
|
||||
}
|
||||
|
||||
return context->node == NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
pm_node_source_location(VALUE source, VALUE filepath, int start_line,
|
||||
int node_id, rb_code_location_t *location)
|
||||
{
|
||||
pm_parse_result_t result;
|
||||
pm_parse_result_init(&result);
|
||||
|
||||
VALUE error;
|
||||
if (NIL_P(source)) {
|
||||
error = pm_load_parse_file(&result, filepath, NULL);
|
||||
}
|
||||
else {
|
||||
pm_options_line_set(result.options, start_line);
|
||||
error = pm_parse_string(&result, source, filepath, NULL);
|
||||
}
|
||||
|
||||
if (!NIL_P(error)) {
|
||||
pm_parse_result_free(&result);
|
||||
rb_exc_raise(error);
|
||||
}
|
||||
|
||||
pm_node_find_context_t context = {
|
||||
.node_id = (uint32_t) node_id,
|
||||
.node = NULL
|
||||
};
|
||||
pm_visit_node(result.node.ast_node, pm_node_find, &context);
|
||||
|
||||
bool found = context.node != NULL;
|
||||
if (found) {
|
||||
*location = pm_code_location(&result.node, context.node);
|
||||
}
|
||||
|
||||
RB_GC_GUARD(source);
|
||||
RB_GC_GUARD(filepath);
|
||||
|
||||
pm_parse_result_free(&result);
|
||||
return found;
|
||||
}
|
||||
|
||||
VALUE rb_io_gets_limit_internal(VALUE io, long limit);
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ typedef struct pm_local_index_struct {
|
||||
|
||||
// A declaration for the struct that lives in compile.c.
|
||||
struct iseq_link_anchor;
|
||||
struct rb_code_location_struct;
|
||||
|
||||
/**
|
||||
* A direct-indexed lookup table mapping constant IDs to local variable indices.
|
||||
@@ -187,6 +188,8 @@ VALUE pm_parse_string(pm_parse_result_t *result, VALUE source, VALUE filepath, V
|
||||
VALUE pm_parse_stdin(pm_parse_result_t *result);
|
||||
void pm_options_version_for_current_ruby_set(pm_options_t *options);
|
||||
void pm_parse_result_free(pm_parse_result_t *result);
|
||||
bool pm_node_source_location(VALUE source, VALUE filepath, int start_line,
|
||||
int node_id, struct rb_code_location_struct *location);
|
||||
|
||||
rb_iseq_t *pm_iseq_new(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpath, const rb_iseq_t *parent, enum rb_iseq_type, int *error_state);
|
||||
rb_iseq_t *pm_iseq_new_top(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpath, const rb_iseq_t *parent, int *error_state);
|
||||
|
||||
@@ -82,6 +82,22 @@ static const rb_data_type_t source_range_data_type = {
|
||||
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_DECL_MARKING
|
||||
};
|
||||
|
||||
VALUE
|
||||
rb_source_range_new(VALUE path, VALUE absolute_path, const rb_code_location_t *location)
|
||||
{
|
||||
struct source_range_data *data;
|
||||
VALUE obj = TypedData_Make_Struct(
|
||||
rb_cSourceRange, struct source_range_data, &source_range_data_type, data);
|
||||
RB_OBJ_WRITE(obj, &data->path, path);
|
||||
RB_OBJ_WRITE(obj, &data->absolute_path, absolute_path);
|
||||
data->start_line = location->beg_pos.lineno;
|
||||
data->start_column = location->beg_pos.column;
|
||||
data->end_line = location->end_pos.lineno;
|
||||
data->end_column = location->end_pos.column;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
source_range_new(const rb_iseq_t *iseq)
|
||||
{
|
||||
@@ -96,19 +112,7 @@ source_range_new(const rb_iseq_t *iseq)
|
||||
return Qnil;
|
||||
}
|
||||
|
||||
int start_line, start_column, end_line, end_column;
|
||||
rb_iseq_code_location(iseq, &start_line, &start_column, &end_line, &end_column);
|
||||
|
||||
struct source_range_data *data;
|
||||
VALUE obj = TypedData_Make_Struct(rb_cSourceRange, struct source_range_data, &source_range_data_type, data);
|
||||
RB_OBJ_WRITE(obj, &data->path, path);
|
||||
RB_OBJ_WRITE(obj, &data->absolute_path, absolute_path);
|
||||
data->start_line = start_line;
|
||||
data->start_column = start_column;
|
||||
data->end_line = end_line;
|
||||
data->end_column = end_column;
|
||||
|
||||
return obj;
|
||||
return rb_source_range_new(path, absolute_path, &ISEQ_BODY(iseq)->location.code_location);
|
||||
}
|
||||
|
||||
static struct source_range_data *
|
||||
@@ -4909,11 +4913,12 @@ proc_ruby2_keywords(VALUE procval)
|
||||
/*
|
||||
* Document-class: Ruby::SourceRange
|
||||
*
|
||||
* An object representing the source-code range for a Ruby callable.
|
||||
* An object representing a range of Ruby source code.
|
||||
*
|
||||
* Source ranges are returned by Proc#source_range, Method#source_range, and
|
||||
* UnboundMethod#source_range. They include the source path, absolute path when
|
||||
* available, start line, start byte column, end line, and end byte column.
|
||||
* UnboundMethod#source_range, as well as Thread::Backtrace::Location#source_range.
|
||||
* They include the source path, absolute path when available,
|
||||
* start line, start byte column, end line, and end byte column.
|
||||
*
|
||||
* The primary purpose of this class is to implement `Prism.find` precisely and cleanly on all Ruby implementations,
|
||||
* in a way which does not depend on implementation details like `node_id`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require_relative '../../proc/fixtures/source_range_helpers'
|
||||
require_relative '../../../fixtures/source_range_helpers'
|
||||
|
||||
describe :method_source_range, shared: true do
|
||||
it "sets absolute_path to the real path of the source file" do
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
def source_range_values(range)
|
||||
[range.start_line, range.start_column, range.end_line, range.end_column]
|
||||
end
|
||||
|
||||
# Use <<-RUBY and not <<~RUBY to keep some spaces in front to make it more representative of a Proc in some file
|
||||
def check_source_range(source)
|
||||
raise "Expected 2 '$' to mark start and end of source_range" unless source.count('$') == 2
|
||||
from = source.byteindex('$')
|
||||
to = source.byteindex('$', from+1)
|
||||
lines = source.lines
|
||||
from_line = 1 + source.byteslice(0, from).count("\n")
|
||||
from_column = lines[from_line-1].byteindex('$')
|
||||
to_line = 1 + source.byteslice(0, to).count("\n")
|
||||
if from_line == to_line
|
||||
to_column = lines[to_line-1].byteindex('$', from_column + 1) - 1
|
||||
else
|
||||
to_column = lines[to_line-1].byteindex('$')
|
||||
end
|
||||
|
||||
eval_source = source.gsub('$', '')
|
||||
result = eval(eval_source)
|
||||
source_range = result.source_range
|
||||
source_range.should.instance_of?(Ruby::SourceRange)
|
||||
source_range.start_line.should == from_line
|
||||
source_range.start_column.should == from_column
|
||||
source_range.end_line.should == to_line
|
||||
source_range.end_column.should == to_column
|
||||
|
||||
# Check consistency with source_location start line
|
||||
result.source_location[1].should == from_line
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
require_relative '../../spec_helper'
|
||||
require_relative 'fixtures/source_range_helpers'
|
||||
require_relative '../../fixtures/source_range_helpers'
|
||||
|
||||
ruby_version_is "4.1" do
|
||||
describe "Proc#source_range" do
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
require_relative '../../../../spec_helper'
|
||||
require_relative '../../../../fixtures/source_range_helpers'
|
||||
|
||||
ruby_version_is "4.1" do
|
||||
describe "Thread::Backtrace::Location#source_range" do
|
||||
it "returns a Ruby::SourceRange with the location paths" do
|
||||
location, range, path, absolute_path = capture_backtrace_location_source_range(<<-RUBY)
|
||||
$nil.foo$
|
||||
RUBY
|
||||
|
||||
range.should.instance_of?(Ruby::SourceRange)
|
||||
range.path.should == path
|
||||
range.absolute_path.should == absolute_path
|
||||
location.path.should == path
|
||||
location.absolute_path.should == absolute_path
|
||||
end
|
||||
|
||||
{
|
||||
"receiver calls with arguments" => <<-RUBY,
|
||||
$nil.foo(42)$
|
||||
RUBY
|
||||
|
||||
"receiver calls split across lines" => <<-RUBY,
|
||||
$nil
|
||||
.foo(
|
||||
42
|
||||
)$
|
||||
RUBY
|
||||
|
||||
"safe navigation calls" => <<-RUBY,
|
||||
$1&.foo(42)$
|
||||
RUBY
|
||||
|
||||
".() call syntax" => <<-RUBY,
|
||||
$nil.(42)$
|
||||
RUBY
|
||||
|
||||
"calls to send" => <<-RUBY,
|
||||
$nil.send(:foo, 42)$
|
||||
RUBY
|
||||
|
||||
"index reads" => <<-RUBY,
|
||||
$nil[0]$
|
||||
RUBY
|
||||
|
||||
"index writes" => <<-RUBY,
|
||||
$nil[0] = 42$
|
||||
RUBY
|
||||
|
||||
"explicit index write calls" => <<-RUBY,
|
||||
$nil.[]=$
|
||||
RUBY
|
||||
|
||||
"attribute writes" => <<-RUBY,
|
||||
$nil.foo = 42$
|
||||
RUBY
|
||||
|
||||
"binary operator calls split by a comment" => <<-RUBY,
|
||||
$nil + # comment
|
||||
42$
|
||||
RUBY
|
||||
|
||||
"unary operator calls" => <<-RUBY,
|
||||
$+nil$
|
||||
RUBY
|
||||
|
||||
"function calls" => <<-RUBY,
|
||||
"str".instance_eval { $gsub("foo", :sym)$ }
|
||||
RUBY
|
||||
|
||||
"function calls without ()" => <<-RUBY,
|
||||
"str".instance_eval { $gsub "foo", :sym$ }
|
||||
RUBY
|
||||
|
||||
"variable calls" => <<-RUBY,
|
||||
nil.instance_eval { $foo$ }
|
||||
RUBY
|
||||
|
||||
"local variable operator assignments" => <<-RUBY,
|
||||
value = nil
|
||||
$value += 42$
|
||||
RUBY
|
||||
|
||||
"index operator assignments failing while reading" => <<-RUBY,
|
||||
value = nil
|
||||
$value[0] += 42$
|
||||
RUBY
|
||||
|
||||
"index operator assignments failing in the operator" => <<-RUBY,
|
||||
value = Object.new
|
||||
def value.[](index) = nil
|
||||
$value[0] += 42$
|
||||
RUBY
|
||||
|
||||
"index operator assignments failing while writing" => <<-RUBY,
|
||||
value = Object.new
|
||||
def value.[](index) = 1
|
||||
$value[0] += 42$
|
||||
RUBY
|
||||
|
||||
"index operator assignments failing on an argument" => <<-RUBY,
|
||||
value = []
|
||||
$value[nil] += 42$
|
||||
RUBY
|
||||
|
||||
"attribute operator assignments failing while reading" => <<-RUBY,
|
||||
value = nil
|
||||
$value.foo += 42$
|
||||
RUBY
|
||||
|
||||
"attribute operator assignments failing in the operator" => <<-RUBY,
|
||||
value = Object.new
|
||||
def value.foo = nil
|
||||
$value.foo += 42$
|
||||
RUBY
|
||||
|
||||
"attribute operator assignments failing while writing" => <<-RUBY,
|
||||
value = Object.new
|
||||
def value.foo = 1
|
||||
$value.foo += 42$
|
||||
RUBY
|
||||
|
||||
"attribute operator assignments failing on the value" => <<-RUBY,
|
||||
value = Object.new
|
||||
def value.foo = 1
|
||||
def value.foo=(new_value)
|
||||
new_value
|
||||
end
|
||||
$value.foo += nil$
|
||||
RUBY
|
||||
|
||||
"bare constants" => <<-RUBY,
|
||||
$SourceRangeNotDefined$
|
||||
RUBY
|
||||
|
||||
"qualified constants" => <<-RUBY,
|
||||
$Object::SourceRangeNotDefined$
|
||||
RUBY
|
||||
|
||||
"qualified constants split across lines" => <<-RUBY,
|
||||
$Object::
|
||||
SourceRangeNotDefined$
|
||||
RUBY
|
||||
|
||||
"top-level constants" => <<-RUBY,
|
||||
$::SourceRangeNotDefined$
|
||||
RUBY
|
||||
|
||||
"constant operator assignments" => <<-RUBY,
|
||||
namespace = Module.new
|
||||
namespace.const_set(:Nil, nil)
|
||||
$namespace::Nil += 1$
|
||||
RUBY
|
||||
|
||||
"constant operator assignments failing while reading" => <<-RUBY,
|
||||
namespace = Module.new
|
||||
$namespace::NotDefined += 1$
|
||||
RUBY
|
||||
|
||||
"top-level constant operator assignments" => <<-RUBY,
|
||||
$::SourceRangeNotDefined += 1$
|
||||
RUBY
|
||||
|
||||
"explicit raises" => <<-RUBY,
|
||||
$raise NameError$
|
||||
RUBY
|
||||
|
||||
"calls failing while converting arguments" => <<-RUBY,
|
||||
$1.+(nil)$
|
||||
RUBY
|
||||
|
||||
"calls with brace blocks" => <<-RUBY,
|
||||
$nil.foo(1) { 2 }$
|
||||
RUBY
|
||||
|
||||
"calls with do-end blocks" => <<-RUBY,
|
||||
$nil.foo(1) do
|
||||
2
|
||||
end$
|
||||
RUBY
|
||||
|
||||
"calls with heredoc arguments" => <<-RUBY,
|
||||
$nil.foo(<<~TEXT)$
|
||||
heredoc
|
||||
TEXT
|
||||
RUBY
|
||||
|
||||
"multibyte identifiers with byte columns" => <<-RUBY,
|
||||
value = "été"
|
||||
$value.あいうえお$
|
||||
RUBY
|
||||
|
||||
"hard tabs" => "\t \t$1.time {}$\n",
|
||||
|
||||
"a missing final newline" => "$1.time {}$",
|
||||
|
||||
"very long source lines" => ("1" * 100) + " + $1.time {}$\n",
|
||||
}.each do |description, source|
|
||||
it "returns the precise range for #{description}" do
|
||||
capture_backtrace_location_source_range(source)
|
||||
end
|
||||
end
|
||||
|
||||
it "returns the method definition for a method arity error" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
target = Class.new do
|
||||
$def source_range_target(first, second)
|
||||
first + second
|
||||
end$
|
||||
end.new
|
||||
target.source_range_target(1)
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns the call for the caller frame of a method arity error" do
|
||||
capture_backtrace_location_source_range(<<-RUBY, frame: 1)
|
||||
target = Class.new do
|
||||
def source_range_target(first, second)
|
||||
first + second
|
||||
end
|
||||
end.new
|
||||
$target.source_range_target(1)$
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns a multiline method definition for a method arity error" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
target = Class.new do
|
||||
$def source_range_target(
|
||||
first,
|
||||
second
|
||||
)
|
||||
first + second
|
||||
end$
|
||||
end.new
|
||||
target.source_range_target(1)
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns a singleton method definition with spacing for a keyword arity error" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
target = Object.new
|
||||
$def target . source_range_target(value:)
|
||||
value
|
||||
end$
|
||||
target.source_range_target
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns a stabby lambda for an arity error" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
value = $->(argument) {}$
|
||||
value.call
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns only the block for an arity error in a Kernel#lambda" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
value = lambda ${ |argument| }$
|
||||
value.call
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "returns only the block for an arity error in a define_method" do
|
||||
capture_backtrace_location_source_range(<<-RUBY)
|
||||
target = Class.new do
|
||||
define_method(:source_range_target) $do |first, second|
|
||||
first + second
|
||||
end$
|
||||
end.new
|
||||
target.source_range_target(1)
|
||||
RUBY
|
||||
end
|
||||
|
||||
it "propagates an error when the absolute source file no longer exists" do
|
||||
keep_source(false) do
|
||||
location, path = capture_backtrace_location_from_source("nil.foo\n")
|
||||
rm_r path
|
||||
|
||||
-> {
|
||||
location.source_range
|
||||
}.should.raise(Errno::ENOENT)
|
||||
ensure
|
||||
rm_r path if path
|
||||
end
|
||||
end
|
||||
|
||||
it "propagates a syntax error from changed source" do
|
||||
keep_source(false) do
|
||||
location, path = capture_backtrace_location_from_source("nil.foo\n")
|
||||
File.binwrite(path, "(\n")
|
||||
|
||||
-> {
|
||||
location.source_range
|
||||
}.should.raise(SyntaxError)
|
||||
ensure
|
||||
rm_r path if path
|
||||
end
|
||||
end
|
||||
|
||||
it "raises when changed source no longer contains the node ID" do
|
||||
keep_source(false) do
|
||||
location, path = capture_backtrace_location_from_source("first = 1\nsecond = 2\nnil.foo\n")
|
||||
File.binwrite(path, "nil\n")
|
||||
|
||||
-> {
|
||||
location.source_range
|
||||
}.should.raise(RuntimeError, /cannot find node ID \d+ in parsed source/)
|
||||
ensure
|
||||
rm_r path if path
|
||||
end
|
||||
end
|
||||
|
||||
it "uses retained eval source and preserves its starting line" do
|
||||
keep_source do
|
||||
path = File.realpath(__FILE__)
|
||||
|
||||
location, range = capture_eval_backtrace_location_source_range(
|
||||
"$nil.foo$",
|
||||
path,
|
||||
100
|
||||
)
|
||||
|
||||
range.path.should == path
|
||||
range.absolute_path.should == nil
|
||||
range.start_line.should == 100
|
||||
location.lineno.should == 100
|
||||
end
|
||||
end
|
||||
|
||||
it "preserves the starting line for blocks in retained eval source" do
|
||||
keep_source do
|
||||
location, range = capture_eval_backtrace_location_source_range(<<-RUBY, "source_range_eval.rb", 100)
|
||||
value = lambda ${ |argument| }$
|
||||
value.call
|
||||
RUBY
|
||||
|
||||
range.start_line.should == 100
|
||||
location.lineno.should == 100
|
||||
end
|
||||
end
|
||||
|
||||
it "does not open an eval path even when it names an existing absolute file" do
|
||||
keep_source(false) do
|
||||
path = File.realpath(__FILE__)
|
||||
|
||||
exception = nil
|
||||
begin
|
||||
eval("nil.foo", binding, path)
|
||||
rescue Exception => error
|
||||
exception = error
|
||||
end
|
||||
|
||||
-> {
|
||||
exception.backtrace_locations.first.source_range
|
||||
}.should.raise(ArgumentError, "cannot get source range for location in eval")
|
||||
end
|
||||
end
|
||||
|
||||
it "does not treat an eval path named -e as command-line source" do
|
||||
keep_source(false) do
|
||||
exception = nil
|
||||
begin
|
||||
eval("nil.foo", binding, "-e")
|
||||
rescue Exception => error
|
||||
exception = error
|
||||
end
|
||||
|
||||
-> {
|
||||
exception.backtrace_locations.first.source_range
|
||||
}.should.raise(ArgumentError, "cannot get source range for location in eval")
|
||||
end
|
||||
end
|
||||
|
||||
it "does not treat a method from eval named -e as command-line source" do
|
||||
code = "eval(%q{def spoofed_source_range_target; nil.foo; end}, binding, %q{-e}); " \
|
||||
"begin; spoofed_source_range_target; rescue => e; " \
|
||||
"begin; e.backtrace_locations.first.source_range; rescue => source_error; " \
|
||||
"p source_error; end; end"
|
||||
ruby_exe(code, escape: false).should == "#<ArgumentError: cannot get source range for location in eval>\n"
|
||||
end
|
||||
|
||||
it "works for -e source" do
|
||||
code = "def source_range_target; nil.foo; end; " \
|
||||
"begin; source_range_target; rescue => e; " \
|
||||
"r = e.backtrace_locations.first.source_range; " \
|
||||
"p [r.path, r.absolute_path, r.start_line, r.start_column, r.end_line, r.end_column]; end"
|
||||
start_column = code.byteindex("nil.foo")
|
||||
expected = ["-e", nil, 1, start_column, 1, start_column + "nil.foo".bytesize]
|
||||
ruby_exe(code, escape: false).should == "#{expected.inspect}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
def source_range_values(range)
|
||||
[range.start_line, range.start_column, range.end_line, range.end_column]
|
||||
end
|
||||
|
||||
def keep_source(value = true)
|
||||
return yield unless defined?(RubyVM.keep_script_lines)
|
||||
|
||||
previous = RubyVM.keep_script_lines
|
||||
begin
|
||||
RubyVM.keep_script_lines = value
|
||||
yield
|
||||
ensure
|
||||
RubyVM.keep_script_lines = previous
|
||||
end
|
||||
end
|
||||
|
||||
def source_range_source(source)
|
||||
raise "Expected 2 '$' to mark start and end of source_range" unless source.count('$') == 2
|
||||
from = source.byteindex('$')
|
||||
to = source.byteindex('$', from + 1)
|
||||
lines = source.lines
|
||||
from_line = 1 + source.byteslice(0, from).count("\n")
|
||||
from_column = lines[from_line-1].byteindex('$')
|
||||
to_line = 1 + source.byteslice(0, to).count("\n")
|
||||
if from_line == to_line
|
||||
to_column = lines[to_line-1].byteindex('$', from_column + 1) - 1
|
||||
else
|
||||
to_column = lines[to_line-1].byteindex('$')
|
||||
end
|
||||
|
||||
eval_source = source.gsub('$', '')
|
||||
[eval_source, [from_line, from_column, to_line, to_column]]
|
||||
end
|
||||
|
||||
# Use <<-RUBY and not <<~RUBY to keep some spaces in front to make it more representative of a Proc in some file
|
||||
def check_source_range(marked_source)
|
||||
source, expected = source_range_source(marked_source)
|
||||
result = eval(source)
|
||||
range = result.source_range
|
||||
range.should.instance_of?(Ruby::SourceRange)
|
||||
source_range_values(range).should == expected
|
||||
|
||||
# Check consistency with source_location start line
|
||||
result.source_location[1].should == expected[0]
|
||||
end
|
||||
|
||||
def capture_backtrace_location_source_range(marked_source, frame: 0)
|
||||
source, expected = source_range_source(marked_source)
|
||||
path = tmp("backtrace_location_source_range.rb")
|
||||
File.binwrite(path, source)
|
||||
absolute_path = File.realpath(path)
|
||||
|
||||
exception = nil
|
||||
begin
|
||||
load path
|
||||
rescue Exception => error
|
||||
exception = error
|
||||
end
|
||||
|
||||
raise "Expected source to raise an exception" unless exception
|
||||
|
||||
location = exception.backtrace_locations.fetch(frame)
|
||||
range = location.source_range
|
||||
range.should.instance_of?(Ruby::SourceRange)
|
||||
source_range_values(range).should == expected
|
||||
|
||||
[location, range, path, absolute_path]
|
||||
ensure
|
||||
rm_r path if path
|
||||
end
|
||||
|
||||
def capture_backtrace_location_from_source(source, frame: 0)
|
||||
path = tmp("backtrace_location_source_range.rb")
|
||||
File.binwrite(path, source)
|
||||
|
||||
exception = nil
|
||||
begin
|
||||
load path
|
||||
rescue Exception => error
|
||||
exception = error
|
||||
end
|
||||
|
||||
raise "Expected source to raise an exception" unless exception
|
||||
|
||||
[exception.backtrace_locations.fetch(frame), path]
|
||||
end
|
||||
|
||||
def capture_eval_backtrace_location_source_range(marked_source, path, first_lineno)
|
||||
source, expected = source_range_source(marked_source)
|
||||
exception = nil
|
||||
|
||||
begin
|
||||
eval(source, binding, path, first_lineno)
|
||||
rescue Exception => error
|
||||
exception = error
|
||||
end
|
||||
|
||||
raise "Expected source to raise an exception" unless exception
|
||||
|
||||
location = exception.backtrace_locations.first
|
||||
range = location.source_range
|
||||
range.should.instance_of?(Ruby::SourceRange)
|
||||
source_range_values(range).should == [
|
||||
expected[0] + first_lineno - 1,
|
||||
expected[1],
|
||||
expected[2] + first_lineno - 1,
|
||||
expected[3]
|
||||
]
|
||||
|
||||
[location, range]
|
||||
end
|
||||
+155
@@ -14,6 +14,8 @@
|
||||
#include "internal/class.h"
|
||||
#include "internal/error.h"
|
||||
#include "internal/object.h"
|
||||
#include "internal/proc.h"
|
||||
#include "internal/ruby_parser.h"
|
||||
#include "internal/vm.h"
|
||||
#include "iseq.h"
|
||||
#include "ruby/debug.h"
|
||||
@@ -407,8 +409,160 @@ location_node_id(rb_backtrace_location_t *loc)
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
extern VALUE rb_e_script;
|
||||
|
||||
static bool
|
||||
location_code_location_equal(const rb_code_location_t *left, const rb_code_location_t *right)
|
||||
{
|
||||
return left->beg_pos.lineno == right->beg_pos.lineno &&
|
||||
left->beg_pos.column == right->beg_pos.column &&
|
||||
left->end_pos.lineno == right->end_pos.lineno &&
|
||||
left->end_pos.column == right->end_pos.column;
|
||||
}
|
||||
|
||||
static bool
|
||||
iseq_from_e_script_p(const rb_iseq_t *iseq, VALUE path)
|
||||
{
|
||||
if (!RB_TYPE_P(path, T_STRING) ||
|
||||
RSTRING_LEN(path) != 2 ||
|
||||
memcmp(RSTRING_PTR(path), "-e", 2) != 0 ||
|
||||
!RTEST(rb_e_script)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rb_iseq_t *source_iseq = iseq;
|
||||
for (; source_iseq; source_iseq = ISEQ_BODY(source_iseq)->parent_iseq) {
|
||||
if (ISEQ_BODY(source_iseq)->type == ISEQ_TYPE_EVAL) return false;
|
||||
if (ISEQ_BODY(source_iseq)->type == ISEQ_TYPE_MAIN) return true;
|
||||
}
|
||||
|
||||
int node_id = ISEQ_BODY(iseq)->location.node_id;
|
||||
if (node_id == -1) return false;
|
||||
|
||||
rb_code_location_t source_location;
|
||||
bool found;
|
||||
if (ISEQ_BODY(iseq)->prism) {
|
||||
found = pm_node_source_location(rb_e_script, path, 1, node_id, &source_location);
|
||||
}
|
||||
else {
|
||||
found = rb_ast_node_source_location(
|
||||
rb_e_script,
|
||||
path,
|
||||
1,
|
||||
node_id,
|
||||
ISEQ_BODY(iseq)->type == ISEQ_TYPE_BLOCK,
|
||||
node_id,
|
||||
&source_location
|
||||
);
|
||||
}
|
||||
|
||||
return found && location_code_location_equal(
|
||||
&source_location, &ISEQ_BODY(iseq)->location.code_location);
|
||||
}
|
||||
|
||||
static int
|
||||
location_source_first_lineno(const rb_iseq_t *iseq, VALUE script_lines)
|
||||
{
|
||||
const rb_iseq_t *source_iseq = iseq;
|
||||
|
||||
while (ISEQ_BODY(source_iseq)->parent_iseq) {
|
||||
const rb_iseq_t *parent = ISEQ_BODY(source_iseq)->parent_iseq;
|
||||
if (ISEQ_BODY(parent)->variable.script_lines != script_lines) break;
|
||||
source_iseq = parent;
|
||||
}
|
||||
|
||||
return ISEQ_BODY(source_iseq)->location.first_lineno;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* call-seq:
|
||||
* location.source_range -> Ruby::SourceRange or nil
|
||||
*
|
||||
* Returns the Ruby::SourceRange for the Ruby expression associated with this
|
||||
* backtrace location, or +nil+ when the location is not available
|
||||
* (e.g., the source is not Ruby code).
|
||||
*
|
||||
* This method requires re-reading the source file from the filesystem
|
||||
* (since this information is not kept in the bytecode to avoid memory overhead).
|
||||
* Errno::ENOENT if the source file no longer exists.
|
||||
* RuntimeError is raised if the file has been modified.
|
||||
*
|
||||
* On CRuby, `RubyVM.keep_script_lines = true` can be used to avoid to re-read
|
||||
* source files from the filesystem, however this will increase memory usage,
|
||||
* by keeping all source files in memory.
|
||||
*
|
||||
* Locations from eval'd code are only available with `RubyVM.keep_script_lines = true`.
|
||||
*/
|
||||
static VALUE
|
||||
location_source_range_m(VALUE self)
|
||||
{
|
||||
#ifdef USE_ISEQ_NODE_ID
|
||||
rb_backtrace_location_t *backtrace_location = location_ptr(self);
|
||||
const rb_iseq_t *iseq = location_iseq(backtrace_location);
|
||||
if (!iseq) return Qnil;
|
||||
|
||||
rb_iseq_check(iseq);
|
||||
int node_id = location_node_id(backtrace_location);
|
||||
if (node_id == -1) return Qnil;
|
||||
|
||||
VALUE path = rb_iseq_path(iseq);
|
||||
VALUE absolute_path = rb_iseq_realpath(iseq);
|
||||
VALUE script_lines = ISEQ_BODY(iseq)->variable.script_lines;
|
||||
VALUE source = script_lines;
|
||||
VALUE parser_path = path;
|
||||
int first_lineno = 1;
|
||||
|
||||
if (!NIL_P(script_lines)) {
|
||||
first_lineno = location_source_first_lineno(iseq, script_lines);
|
||||
}
|
||||
else if (iseq_from_e_script_p(iseq, path)) {
|
||||
source = rb_e_script;
|
||||
}
|
||||
else if (!NIL_P(absolute_path)) {
|
||||
source = Qnil;
|
||||
parser_path = absolute_path;
|
||||
}
|
||||
else {
|
||||
rb_raise(rb_eArgError, "cannot get source range for location in eval");
|
||||
}
|
||||
|
||||
if (NIL_P(parser_path)) {
|
||||
parser_path = rb_str_new_cstr("(eval)");
|
||||
}
|
||||
|
||||
rb_code_location_t code_location;
|
||||
bool found;
|
||||
|
||||
if (ISEQ_BODY(iseq)->prism) {
|
||||
if (RB_TYPE_P(source, T_ARRAY)) {
|
||||
source = rb_ary_join(source, Qnil);
|
||||
}
|
||||
found = pm_node_source_location(source, parser_path, first_lineno, node_id, &code_location);
|
||||
}
|
||||
else {
|
||||
found = rb_ast_node_source_location(
|
||||
source,
|
||||
parser_path,
|
||||
first_lineno,
|
||||
node_id,
|
||||
ISEQ_BODY(iseq)->type == ISEQ_TYPE_BLOCK,
|
||||
ISEQ_BODY(iseq)->location.node_id,
|
||||
&code_location
|
||||
);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
rb_raise(rb_eRuntimeError, "cannot find node ID %d in parsed source", node_id);
|
||||
}
|
||||
|
||||
return rb_source_range_new(path, absolute_path, &code_location);
|
||||
#else
|
||||
return Qnil;
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
rb_get_node_id_from_frame_info(VALUE obj)
|
||||
{
|
||||
@@ -1530,6 +1684,7 @@ Init_vm_backtrace(void)
|
||||
rb_define_method(rb_cBacktraceLocation, "base_label", location_base_label_m, 0);
|
||||
rb_define_method(rb_cBacktraceLocation, "path", location_path_m, 0);
|
||||
rb_define_method(rb_cBacktraceLocation, "absolute_path", location_absolute_path_m, 0);
|
||||
rb_define_method(rb_cBacktraceLocation, "source_range", location_source_range_m, 0);
|
||||
rb_define_method(rb_cBacktraceLocation, "to_s", location_to_str_m, 0);
|
||||
rb_define_method(rb_cBacktraceLocation, "inspect", location_inspect_m, 0);
|
||||
|
||||
|
||||
Reference in new issue
Block a user