[Feature #22212] Add Thread::Backtrace::Location#source_range

This commit is contained in:
Benoit Daloze committed 2026-08-06 13:20:23 +02:00
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

+5
View File
@@ -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
+76 -6
View File
@@ -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;
+3
View File
@@ -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);
+3
View File
@@ -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. */
+57
View File
@@ -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);
/**
+3
View File
@@ -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);
+21 -16
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
+111
View File
@@ -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
View File
@@ -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);