diff --git a/proc.c b/proc.c index dfc7501252..ec08f8ad52 100644 --- a/proc.c +++ b/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("#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); diff --git a/spec/ruby/core/method/fixtures/classes.rb b/spec/ruby/core/method/fixtures/classes.rb index 41904df1d1..9e180abb09 100644 --- a/spec/ruby/core/method/fixtures/classes.rb +++ b/spec/ruby/core/method/fixtures/classes.rb @@ -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 diff --git a/spec/ruby/core/method/shared/source_range.rb b/spec/ruby/core/method/shared/source_range.rb new file mode 100644 index 0000000000..70884484e0 --- /dev/null +++ b/spec/ruby/core/method/shared/source_range.rb @@ -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 diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb index 22fcb98c74..6ab3af9f42 100644 --- a/spec/ruby/core/method/source_location_spec.rb +++ b/spec/ruby/core/method/source_location_spec.rb @@ -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 diff --git a/spec/ruby/core/method/source_range_spec.rb b/spec/ruby/core/method/source_range_spec.rb new file mode 100644 index 0000000000..46a1c6601c --- /dev/null +++ b/spec/ruby/core/method/source_range_spec.rb @@ -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 diff --git a/spec/ruby/core/proc/fixtures/source_location.rb b/spec/ruby/core/proc/fixtures/source_location.rb index 5572094630..e7a1bf5a17 100644 --- a/spec/ruby/core/proc/fixtures/source_location.rb +++ b/spec/ruby/core/proc/fixtures/source_location.rb @@ -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 diff --git a/spec/ruby/core/proc/fixtures/source_range_helpers.rb b/spec/ruby/core/proc/fixtures/source_range_helpers.rb new file mode 100644 index 0000000000..d5a12d28ee --- /dev/null +++ b/spec/ruby/core/proc/fixtures/source_range_helpers.rb @@ -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 diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index d27ad0559e..5394bed050 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -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 diff --git a/spec/ruby/core/proc/source_range_spec.rb b/spec/ruby/core/proc/source_range_spec.rb new file mode 100644 index 0000000000..81c803cc6b --- /dev/null +++ b/spec/ruby/core/proc/source_range_spec.rb @@ -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 diff --git a/spec/ruby/core/ruby/source_range/inspect_spec.rb b/spec/ruby/core/ruby/source_range/inspect_spec.rb new file mode 100644 index 0000000000..a23673397e --- /dev/null +++ b/spec/ruby/core/ruby/source_range/inspect_spec.rb @@ -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 == "#" + 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 == "#" + end + end +end diff --git a/spec/ruby/core/unboundmethod/fixtures/classes.rb b/spec/ruby/core/unboundmethod/fixtures/classes.rb index 58120c2f88..29d4902e50 100644 --- a/spec/ruby/core/unboundmethod/fixtures/classes.rb +++ b/spec/ruby/core/unboundmethod/fixtures/classes.rb @@ -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 diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 927600bfcb..8ed94a395e 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -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 diff --git a/spec/ruby/core/unboundmethod/source_range_spec.rb b/spec/ruby/core/unboundmethod/source_range_spec.rb new file mode 100644 index 0000000000..9ae55daf59 --- /dev/null +++ b/spec/ruby/core/unboundmethod/source_range_spec.rb @@ -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 diff --git a/template/fake.rb.in b/template/fake.rb.in index fed640aee7..a2f90d677b 100644 --- a/template/fake.rb.in +++ b/template/fake.rb.in @@ -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