mirror of
https://github.com/ruby/ruby.git
synced 2026-08-03 11:18:06 +08:00
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:
parent
aaf54aa08b
commit
d677c4f1df
Notes:
git
2026-07-10 10:14:52 +00:00
273
proc.c
273
proc.c
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
133
spec/ruby/core/method/shared/source_range.rb
Normal file
133
spec/ruby/core/method/shared/source_range.rb
Normal 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 été; 42; end$
|
||||
end
|
||||
|
||||
klass.new.method(:été)
|
||||
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
|
||||
@ -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
|
||||
|
||||
12
spec/ruby/core/method/source_range_spec.rb
Normal file
12
spec/ruby/core/method/source_range_spec.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
31
spec/ruby/core/proc/fixtures/source_range_helpers.rb
Normal file
31
spec/ruby/core/proc/fixtures/source_range_helpers.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
146
spec/ruby/core/proc/source_range_spec.rb
Normal file
146
spec/ruby/core/proc/source_range_spec.rb
Normal 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) { été }$
|
||||
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
|
||||
18
spec/ruby/core/ruby/source_range/inspect_spec.rb
Normal file
18
spec/ruby/core/ruby/source_range/inspect_spec.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
12
spec/ruby/core/unboundmethod/source_range_spec.rb
Normal file
12
spec/ruby/core/unboundmethod/source_range_spec.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user