Add {Method,UnboundMethod,Proc}#source_range

* Add exhaustive specs.
* Avoid hardcoded lines in existing method and proc specs.

[Feature #21998]
This commit is contained in:
Benoit Daloze 2026-05-14 13:07:04 +02:00
parent aaf54aa08b
commit d677c4f1df
Notes: git 2026-07-10 10:14:52 +00:00
14 changed files with 706 additions and 25 deletions

273
proc.c
View File

@ -43,12 +43,202 @@ VALUE rb_cUnboundMethod;
VALUE rb_cMethod;
VALUE rb_cBinding;
VALUE rb_cProc;
static VALUE rb_cSourceRange;
static rb_block_call_func bmcall;
static int method_arity(VALUE);
static int method_min_max_arity(VALUE, int *max);
static VALUE proc_binding(VALUE self);
struct source_range_data {
VALUE path;
VALUE absolute_path;
int start_line;
int start_column;
int end_line;
int end_column;
};
static size_t
source_range_memsize(const void *ptr)
{
return sizeof(struct source_range_data);
}
RUBY_REFERENCES(source_range_refs) = {
RUBY_REF_EDGE(struct source_range_data, path),
RUBY_REF_EDGE(struct source_range_data, absolute_path),
RUBY_REF_END
};
static const rb_data_type_t source_range_data_type = {
"source_range",
{
RUBY_REFS_LIST_PTR(source_range_refs),
RUBY_TYPED_DEFAULT_FREE,
source_range_memsize,
},
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_DECL_MARKING
};
static VALUE
source_range_new(const rb_iseq_t *iseq)
{
if (!iseq) {
return Qnil;
}
rb_iseq_check(iseq);
VALUE path = rb_iseq_path(iseq);
VALUE absolute_path = rb_iseq_realpath(iseq);
if (NIL_P(path) && NIL_P(absolute_path)) {
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;
}
static struct source_range_data *
source_range_data_get(VALUE self)
{
struct source_range_data *data;
TypedData_Get_Struct(self, struct source_range_data, &source_range_data_type, data);
return data;
}
/*
* call-seq:
* source_range.path -> String
*
* Returns the source path for the callable associated with this source range.
* This is the same path returned as the first element of #source_location.
*/
static VALUE
source_range_path(VALUE self)
{
return source_range_data_get(self)->path;
}
/*
* call-seq:
* source_range.absolute_path -> String or nil
*
* Returns the absolute source path for the callable associated with this source
* range, or +nil+ if the source has no absolute path, such as eval'd code.
*/
static VALUE
source_range_absolute_path(VALUE self)
{
return source_range_data_get(self)->absolute_path;
}
/*
* call-seq:
* source_range.start_line -> Integer
*
* Returns the 1-indexed line number where this source range starts.
*/
static VALUE
source_range_start_line(VALUE self)
{
return INT2NUM(source_range_data_get(self)->start_line);
}
/*
* call-seq:
* source_range.start_column -> Integer
*
* Returns the 0-indexed byte column where this source range starts.
*
* -> {}.source_range.start_column # => 0 # the '->'
* l = -> {}.source_range.start_column # => 4 # the '->'
* proc {}.source_range.start_column # => 5 # the '{'
* method(def m = 42).source_range.start_column # => 7 # the 'def'
*/
static VALUE
source_range_start_column(VALUE self)
{
return INT2NUM(source_range_data_get(self)->start_column);
}
/*
* call-seq:
* source_range.end_line -> Integer
*
* Returns the 1-indexed line number where this source range ends.
*
* Note that this does not include a potential heredoc that spans beyond the callable's end, for example:
*
* proc { <<~HEREDOC }.source_range.end_line # => 1
* heredoc
* contents
* HEREDOC
*
* To get the location of the final HEREDOC you can use +Prism.find(Proc|Method|UnboundMethod)+ and then compute the maximum end_line and end_column.
*/
static VALUE
source_range_end_line(VALUE self)
{
return INT2NUM(source_range_data_get(self)->end_line);
}
/*
* call-seq:
* source_range.end_column -> Integer
*
* Returns the 0-indexed byte column where this source range ends.
*
* Note that this does not include a potential heredoc that spans beyond the callable's end, for example:
*
* proc { <<~HEREDOC }.source_range.end_column # => 19
* heredoc
* contents
* HEREDOC
*
* To get the location of the final HEREDOC you can use +Prism.find(Proc|Method|UnboundMethod)+ and then compute the maximum end_line and end_column.
*/
static VALUE
source_range_end_column(VALUE self)
{
return INT2NUM(source_range_data_get(self)->end_column);
}
/*
* call-seq:
* source_range.inspect -> String
*
* Returns a human-readable string with the #absolute_path if available,
* otherwise the #path, and the start and end coordinates.
*/
static VALUE
source_range_inspect(VALUE self)
{
struct source_range_data *data = source_range_data_get(self);
VALUE str = rb_str_new_cstr("#<Ruby::SourceRange ");
VALUE path = NIL_P(data->absolute_path) ? data->path : data->absolute_path;
VM_ASSERT(!NIL_P(path));
rb_str_append(str, path);
rb_str_catf(str, ":(%d,%d)-(%d,%d)>",
data->start_line, data->start_column,
data->end_line, data->end_column);
return str;
}
/* Proc */
#define IS_METHOD_PROC_IFUNC(ifunc) ((ifunc)->func == bmcall)
@ -1562,6 +1752,25 @@ rb_proc_location(VALUE self)
return iseq_location(rb_proc_get_iseq(self, 0));
}
/*
* call-seq:
* prc.source_range -> Ruby::SourceRange or nil
*
* Returns a Ruby::SourceRange for this proc, or +nil+ if this proc was
* not defined in Ruby (i.e. native) or has no source path.
*
* The returned Ruby::SourceRange includes the source path, absolute path when
* available, and the start and end line and byte-column coordinates.
*
* See https://github.com/ruby/spec/blob/master/core/proc/source_range_spec.rb
* for the location of start/end line/column in various cases.
*/
static VALUE
rb_proc_source_range(VALUE self)
{
return source_range_new(rb_proc_get_iseq(self, 0));
}
VALUE
rb_unnamed_parameters(int arity)
{
@ -3231,6 +3440,31 @@ rb_method_location(VALUE method)
return method_def_location(rb_method_def(method));
}
static VALUE
method_def_source_range(const rb_method_definition_t *def)
{
return source_range_new(method_def_iseq(def));
}
/*
* call-seq:
* meth.source_range -> Ruby::SourceRange or nil
*
* Returns a Ruby::SourceRange for this method, or +nil+ if this method
* was not defined in Ruby (i.e. native) or has no source path.
*
* The returned Ruby::SourceRange includes the source path, absolute path when
* available, and the start and end line and byte-column coordinates.
*
* See https://github.com/ruby/spec/blob/master/core/method/shared/source_range.rb
* for the location of start/end line/column in various cases.
*/
static VALUE
rb_method_source_range(VALUE method)
{
return method_def_source_range(rb_method_def(method));
}
static const rb_method_definition_t *
vm_proc_method_def(VALUE procval)
{
@ -4278,6 +4512,28 @@ proc_ruby2_keywords(VALUE procval)
* SystemStackError: stack level too deep
*/
/*
* Document-class: Ruby::SourceRange
*
* An object representing the source-code range for a Ruby callable.
*
* 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.
*
* 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`.
* For that we need the start/end line/column and the absolute_path, which is exactly what this class provides.
*
* The user of `Prism.find` can then tweak the result as desired to, for example,
* include heredocs as mentioned in Ruby::SourceRange#end_line.
* Or for Proc#source_range to include the method to which the block is passed.
*
* Note that the returned source range is not always an evaluable fragment by itself,
* notably because heredocs can go beyond the `end` of the method and
* for blocks because the range starts at `{`/`do`.
*/
/*
* Document-class: Proc
*
@ -4645,6 +4901,20 @@ void
Init_Proc(void)
{
#undef rb_intern
VALUE mRuby = rb_define_module("Ruby");
/* Ruby::SourceRange */
rb_cSourceRange = rb_define_class_under(mRuby, "SourceRange", rb_cObject);
rb_undef_alloc_func(rb_cSourceRange);
rb_undef_method(CLASS_OF(rb_cSourceRange), "new");
rb_define_method(rb_cSourceRange, "path", source_range_path, 0);
rb_define_method(rb_cSourceRange, "absolute_path", source_range_absolute_path, 0);
rb_define_method(rb_cSourceRange, "start_line", source_range_start_line, 0);
rb_define_method(rb_cSourceRange, "start_column", source_range_start_column, 0);
rb_define_method(rb_cSourceRange, "end_line", source_range_end_line, 0);
rb_define_method(rb_cSourceRange, "end_column", source_range_end_column, 0);
rb_define_method(rb_cSourceRange, "inspect", source_range_inspect, 0);
/* Proc */
rb_cProc = rb_define_class("Proc", rb_cObject);
rb_undef_alloc_func(rb_cProc);
@ -4677,6 +4947,7 @@ Init_Proc(void)
rb_define_method(rb_cProc, "==", proc_eq, 1);
rb_define_method(rb_cProc, "eql?", proc_eq, 1);
rb_define_method(rb_cProc, "source_location", rb_proc_location, 0);
rb_define_method(rb_cProc, "source_range", rb_proc_source_range, 0);
rb_define_method(rb_cProc, "parameters", rb_proc_parameters, -1);
rb_define_method(rb_cProc, "ruby2_keywords", proc_ruby2_keywords, 0);
// rb_define_method(rb_cProc, "isolate", rb_proc_isolate, 0); is not accepted.
@ -4718,6 +4989,7 @@ Init_Proc(void)
rb_define_method(rb_cMethod, "owner", method_owner, 0);
rb_define_method(rb_cMethod, "unbind", method_unbind, 0);
rb_define_method(rb_cMethod, "source_location", rb_method_location, 0);
rb_define_method(rb_cMethod, "source_range", rb_method_source_range, 0);
rb_define_method(rb_cMethod, "parameters", rb_method_parameters, 0);
rb_define_method(rb_cMethod, "super_method", method_super_method, 0);
rb_define_method(rb_mKernel, "method", rb_obj_method, 1);
@ -4744,6 +5016,7 @@ Init_Proc(void)
rb_define_method(rb_cUnboundMethod, "bind", umethod_bind, 1);
rb_define_method(rb_cUnboundMethod, "bind_call", umethod_bind_call, -1);
rb_define_method(rb_cUnboundMethod, "source_location", rb_method_location, 0);
rb_define_method(rb_cUnboundMethod, "source_range", rb_method_source_range, 0);
rb_define_method(rb_cUnboundMethod, "parameters", rb_method_parameters, 0);
rb_define_method(rb_cUnboundMethod, "super_method", method_super_method, 0);

View File

@ -1,19 +1,20 @@
module MethodSpecs
class SourceLocation
def self.location # This needs to be on this line
:location # for the spec to pass
LOCATION_LINE = __LINE__ + 1
def self.location
:location
end
def self.redefined
:first
end
REDEFINED_LINE = __LINE__ + 1
def self.redefined
:last
end
ORIGINAL_LINE = __LINE__ + 1
def original
end

View File

@ -0,0 +1,133 @@
require_relative '../../proc/fixtures/source_range_helpers'
describe :method_source_range, shared: true do
it "sets absolute_path to the real path of the source file" do
def location
end
method = @object.call(method(:location))
method.source_range.absolute_path.should == File.realpath(__FILE__)
end
it "sets path to the source location path" do
def location
end
method = @object.call(method(:location))
method.source_range.path.should == __FILE__
end
it "works for singleton methods" do
check_source_range <<-RUBY
$def self.location
:location
end$
method(:location)
RUBY
end
it "works for multi-line methods" do
check_source_range <<-RUBY
$def multiline
1
2
end$
method(:multiline)
RUBY
end
it "works for UTF-8 method names and returns byte columns" do
check_source_range <<-RUBY
klass = Class.new do
$def é; 42; end$
end
klass.new.method(:é)
RUBY
end
it "works for inline methods" do
check_source_range <<-RUBY
klass = Class.new do
$def inline = 42$
end
klass.new.method(:inline)
RUBY
end
it "does not cover a heredoc spanning beyond an inline method" do
check_source_range <<-RUBY
$def inline_heredoc = <<~HEREDOC$
heredoc
HEREDOC
method(:inline_heredoc)
RUBY
end
it "does not cover a heredoc spanning beyond a regular method" do
check_source_range <<-RUBY
$def heredoc; <<~HEREDOC; end$;
heredoc
HEREDOC
method(:heredoc)
RUBY
end
it "works for methods defined with define_method" do
check_source_range <<-RUBY
klass = Class.new do
define_method(:define_method_method) ${ 42 }$
end
klass.new.method(:define_method_method)
RUBY
end
it "returns the same range for an unbound method as the method reports" do
def my_method_source_range
true
end
method = method(:my_method_source_range)
unbound = method.unbind
source_range_values(unbound.source_range).should == source_range_values(method.source_range)
unbound.source_range.path.should == method.source_range.path
unbound.source_range.absolute_path.should == method.source_range.absolute_path
end
it "returns nil for core methods" do
method = @object.call("".method(:length))
method.source_range.should == nil
end
it "sets path when absolute_path is nil" do
eval('def m; end', nil, "foo")
method = @object.call(method(:m))
range = method.source_range
range.path.should == "foo"
range.absolute_path.should == nil
end
it "sets #absolute_path to nil even if an absolute path is given to eval" do
eval('def m; end', nil, "/foo")
method = @object.call(method(:m))
range = method.source_range
range.path.should == "/foo"
range.absolute_path.should == nil
end
it "considers eval's start line" do
eval('def m; end', nil, "foo", 100)
method = @object.call(method(:m))
range = method.source_range
range.start_line.should == 100
end
end

View File

@ -19,15 +19,15 @@ describe "Method#source_location" do
it "sets the last value to an Integer representing the line on which the method was defined" do
line = @method.source_location.last
line.should.instance_of?(Integer)
line.should == 5
line.should == MethodSpecs::SourceLocation::LOCATION_LINE
end
it "returns the last place the method was defined" do
MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13
MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == MethodSpecs::SourceLocation::REDEFINED_LINE
end
it "returns the location of the original method even if it was aliased" do
MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17
MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == MethodSpecs::SourceLocation::ORIGINAL_LINE
end
it "works for methods defined with a block" do

View File

@ -0,0 +1,12 @@
require_relative '../../spec_helper'
require_relative 'shared/source_range'
ruby_version_is "4.1" do
describe "Method#source_range" do
before :each do
@object = -> method { method }
end
it_behaves_like :method_source_range, :source_range
end
end

View File

@ -1,21 +1,50 @@
module ProcSpecs
class SourceLocation
MY_PROC_LINE = __LINE__ + 2
def self.my_proc
proc { true }
end
MY_LAMBDA_LINE = __LINE__ + 2
def self.my_lambda
-> { true }
end
def self.my_block_lambda
lambda { 42 }
end
MY_PROC_NEW_LINE = __LINE__ + 2
def self.my_proc_new
Proc.new { true }
end
MY_METHOD_LINE = __LINE__ + 1
def self.my_method
method(__method__).to_proc
end
def self.my_returned_block
return_block { 42 }
end
def self.return_block(&block)
block
end
def self.my_receiver_block
block_receiver.foo { 42 }
end
def self.block_receiver
obj = Object.new
def obj.foo(&block)
block
end
obj
end
MY_MULTILINE_PROC_LINE = __LINE__ + 2
def self.my_multiline_proc
proc do
'a'.upcase
@ -23,6 +52,13 @@ module ProcSpecs
end
end
def self.my_heredoc_proc
proc { <<~END }
heredoc
END
end
MY_MULTILINE_LAMBDA_LINE = __LINE__ + 2
def self.my_multiline_lambda
-> do
'a'.upcase
@ -30,6 +66,7 @@ module ProcSpecs
end
end
MY_MULTILINE_PROC_NEW_LINE = __LINE__ + 2
def self.my_multiline_proc_new
Proc.new do
'a'.upcase
@ -37,19 +74,36 @@ module ProcSpecs
end
end
MY_DETACHED_PROC_LINE = __LINE__ + 2
def self.my_detached_proc
body = proc { true }
proc(&body)
end
MY_DETACHED_LAMBDA_LINE = __LINE__ + 2
def self.my_detached_lambda
body = -> { true }
suppress_warning {lambda(&body)}
end
MY_DETACHED_PROC_NEW_LINE = __LINE__ + 2
def self.my_detached_proc_new
body = Proc.new { true }
Proc.new(&body)
end
iter = Object.new
def iter.each(&block)
block.call(block)
end
for pr in iter
42
end
MY_FOR_BODY_PROC = pr
def self.my_for_body_proc
MY_FOR_BODY_PROC
end
end
end

View File

@ -0,0 +1,31 @@
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

View File

@ -37,19 +37,19 @@ describe "Proc#source_location" do
it "sets the last value to an Integer representing the line on which the proc was defined" do
line = @proc.source_location.last
line.should.instance_of?(Integer)
line.should == 4
line.should == ProcSpecs::SourceLocation::MY_PROC_LINE
line = @proc_new.source_location.last
line.should.instance_of?(Integer)
line.should == 12
line.should == ProcSpecs::SourceLocation::MY_PROC_NEW_LINE
line = @lambda.source_location.last
line.should.instance_of?(Integer)
line.should == 8
line.should == ProcSpecs::SourceLocation::MY_LAMBDA_LINE
line = @method.source_location.last
line.should.instance_of?(Integer)
line.should == 15
line.should == ProcSpecs::SourceLocation::MY_METHOD_LINE
end
it "works even if the proc was created on the same line" do
@ -59,15 +59,15 @@ describe "Proc#source_location" do
end
it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do
ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20
ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34
ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27
ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == ProcSpecs::SourceLocation::MY_MULTILINE_PROC_LINE
ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == ProcSpecs::SourceLocation::MY_MULTILINE_PROC_NEW_LINE
ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == ProcSpecs::SourceLocation::MY_MULTILINE_LAMBDA_LINE
end
it "returns the location of the proc's body; not necessarily the proc itself" do
ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41
ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51
ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46
ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == ProcSpecs::SourceLocation::MY_DETACHED_PROC_LINE
ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == ProcSpecs::SourceLocation::MY_DETACHED_PROC_NEW_LINE
ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == ProcSpecs::SourceLocation::MY_DETACHED_LAMBDA_LINE
end
it "returns the same value for a proc-ified method as the method reports" do

View File

@ -0,0 +1,146 @@
require_relative '../../spec_helper'
require_relative 'fixtures/source_range_helpers'
ruby_version_is "4.1" do
describe "Proc#source_range" do
it "sets absolute_path to the real path of the source file" do
my_proc = proc {}
my_proc.source_range.absolute_path.should == File.realpath(__FILE__)
end
it "sets path to the source location path" do
my_proc = proc {}
my_proc.source_range.path.should == __FILE__
end
it "works for proc {}" do
check_source_range <<-RUBY
proc ${}$
RUBY
end
it "works for Proc.new {}" do
check_source_range <<-RUBY
Proc.new ${}$
RUBY
end
it "works for lambda {}" do
check_source_range <<-RUBY
lambda ${}$
RUBY
end
it "works for -> {}" do
check_source_range <<-RUBY
$-> {}$
RUBY
end
it "works with multibyte characters and return byte columns" do
check_source_range <<-RUBY
$-> (il, était, un) { é }$
RUBY
end
it "works for multi-line procs" do
check_source_range <<-RUBY
proc $do
'a'.upcase
1 + 22
end$
RUBY
end
it "works for returned blocks" do
check_source_range <<-RUBY
def return_block(&block)
block
end
return_block ${ 42 }$
RUBY
end
it "works for blocks passed to calls with receivers" do
check_source_range <<-RUBY
def block_receiver
obj = Object.new
def obj.foo(&block)
block
end
obj
end
block_receiver.foo ${ 42 }$
RUBY
end
it "uses the '}' as the end bound for a Proc with a heredoc inside" do
check_source_range <<-RUBY
proc ${ <<~END }$
heredoc
END
RUBY
end
it "works for for-loop body procs" do
check_source_range <<-RUBY
iter = Object.new
def iter.each(&block)
block.call(block)
end
$for pr in iter
42
end$
pr
RUBY
end
it "works for define_method & to_proc" do
check_source_range <<-RUBY
self.singleton_class.define_method :foo $do
1 + 2
end$
method(:foo).to_proc
RUBY
end
it "returns the same range for a proc-ified method as the method reports" do
def my_proc
proc { true }
end
meth = method(:my_proc)
proc = meth.to_proc
source_range_values(proc.source_range).should == source_range_values(meth.source_range)
proc.source_range.path.should == meth.source_range.path
proc.source_range.absolute_path.should == meth.source_range.absolute_path
end
it "returns nil for a core method that has been proc-ified" do
[].method(:<<).to_proc.source_range.should == nil
end
it "sets #path when #absolute_path is nil" do
range = eval('-> { 1 }', nil, "foo").source_range
range.path.should == "foo"
range.absolute_path.should == nil
end
it "sets #absolute_path to nil even if an absolute path is given to eval" do
range = eval('-> { 1 }', nil, "/foo").source_range
range.path.should == "/foo"
range.absolute_path.should == nil
end
it "considers eval's start line" do
range = eval('-> { 1 }', nil, "foo", 100).source_range
range.start_line.should == 100
end
end
end

View File

@ -0,0 +1,18 @@
require_relative '../../../spec_helper'
ruby_version_is "4.1" do
describe "Ruby::SourceRange#inspect" do
it "includes the absolute path and coordinates" do
range = proc { 1 }.source_range
range.inspect.should == "#<Ruby::SourceRange #{range.absolute_path}:(#{range.start_line},#{range.start_column})-(#{range.end_line},#{range.end_column})>"
end
it "uses path when absolute_path is nil" do
range = eval('proc { 1 }', nil, "foo", 100).source_range
range.absolute_path.should == nil
range.inspect.should == "#<Ruby::SourceRange foo:(#{range.start_line},#{range.start_column})-(#{range.end_line},#{range.end_column})>"
end
end
end

View File

@ -1,19 +1,20 @@
module UnboundMethodSpecs
class SourceLocation
def self.location # This needs to be on this line
:location # for the spec to pass
LOCATION_LINE = __LINE__ + 1
def self.location
:location
end
def self.redefined
:first
end
REDEFINED_LINE = __LINE__ + 1
def self.redefined
:last
end
ORIGINAL_LINE = __LINE__ + 1
def original
end

View File

@ -15,15 +15,15 @@ describe "UnboundMethod#source_location" do
it "sets the last value to an Integer representing the line on which the method was defined" do
line = @method.source_location.last
line.should.instance_of?(Integer)
line.should == 5
line.should == UnboundMethodSpecs::SourceLocation::LOCATION_LINE
end
it "returns the last place the method was defined" do
UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == 13
UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == UnboundMethodSpecs::SourceLocation::REDEFINED_LINE
end
it "returns the location of the original method even if it was aliased" do
UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == 17
UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == UnboundMethodSpecs::SourceLocation::ORIGINAL_LINE
end
it "works for define_method methods" do

View File

@ -0,0 +1,12 @@
require_relative '../../spec_helper'
require_relative '../method/shared/source_range'
ruby_version_is "4.1" do
describe "UnboundMethod#source_range" do
before :each do
@object = -> method { method.unbind }
end
it_behaves_like :method_source_range, :source_range
end
end

View File

@ -51,8 +51,8 @@ class Object
end
v=$VERBOSE;$VERBOSE=nil;module Ruby; end;$VERBOSE=v
module Ruby
constants.each {|n| remove_const n}
% arg['versions'].each {|n, v|
remove_const :<%=n%> if defined?(<%=n%>)
<%=n%> = ::RUBY_<%=n%>
% }
end