Skip to content

Commit d82fc33

Browse files
committed
Reapply "[Feature ruby#6012] Extend source_location for end position
* This reverts commit 065c48c. * This functionality is very valuable and has already taken 14 years to agree on the API. * Let's just document it's byte columns (in the next commit). * See https://bugs.ruby-lang.org/issues/21783#note-9
1 parent 19e539c commit d82fc33

7 files changed

Lines changed: 93 additions & 48 deletions

File tree

NEWS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ Note that each entry is kept to a minimum, see links for details.
1111

1212
Note: We're only listing outstanding class updates.
1313

14+
* Method
15+
16+
* `Method#source_location`, `Proc#source_location`, and
17+
`UnboundMethod#source_location` now return extended location
18+
information with 5 elements: `[path, start_line, start_column,
19+
end_line, end_column]`. The previous 2-element format `[path,
20+
line]` can still be obtained by calling `.take(2)` on the result.
21+
[[Feature #6012]]
22+
1423
* Set
1524

1625
* A deprecated behavior, `Set#to_set`, `Range#to_set`, and
@@ -66,4 +75,5 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
6675

6776
## JIT
6877

78+
[Feature #6012]: https://bugs.ruby-lang.org/issues/6012
6979
[Feature #21390]: https://bugs.ruby-lang.org/issues/21390

proc.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,14 +1513,20 @@ proc_eq(VALUE self, VALUE other)
15131513
static VALUE
15141514
iseq_location(const rb_iseq_t *iseq)
15151515
{
1516-
VALUE loc[2];
1516+
VALUE loc[5];
1517+
int i = 0;
15171518

15181519
if (!iseq) return Qnil;
15191520
rb_iseq_check(iseq);
1520-
loc[0] = rb_iseq_path(iseq);
1521-
loc[1] = RB_INT2NUM(ISEQ_BODY(iseq)->location.first_lineno);
1522-
1523-
return rb_ary_new4(2, loc);
1521+
loc[i++] = rb_iseq_path(iseq);
1522+
const rb_code_location_t *cl = &ISEQ_BODY(iseq)->location.code_location;
1523+
loc[i++] = RB_INT2NUM(cl->beg_pos.lineno);
1524+
loc[i++] = RB_INT2NUM(cl->beg_pos.column);
1525+
loc[i++] = RB_INT2NUM(cl->end_pos.lineno);
1526+
loc[i++] = RB_INT2NUM(cl->end_pos.column);
1527+
RUBY_ASSERT_ALWAYS(i == numberof(loc));
1528+
1529+
return rb_ary_new_from_values(i, loc);
15241530
}
15251531

15261532
VALUE

spec/ruby/core/method/source_location_spec.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@
1111
end
1212

1313
it "sets the first value to the path of the file in which the method was defined" do
14-
file = @method.source_location.first
14+
file = @method.source_location[0]
1515
file.should be_an_instance_of(String)
1616
file.should == File.realpath('fixtures/classes.rb', __dir__)
1717
end
1818

1919
it "sets the last value to an Integer representing the line on which the method was defined" do
20-
line = @method.source_location.last
20+
line = @method.source_location[1]
2121
line.should be_an_instance_of(Integer)
2222
line.should == 5
2323
end
2424

2525
it "returns the last place the method was defined" do
26-
MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13
26+
MethodSpecs::SourceLocation.method(:redefined).source_location[1].should == 13
2727
end
2828

2929
it "returns the location of the original method even if it was aliased" do
30-
MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17
30+
MethodSpecs::SourceLocation.new.method(:aka).source_location[1].should == 17
3131
end
3232

3333
it "works for methods defined with a block" do
@@ -108,7 +108,13 @@ def f
108108
c = Class.new do
109109
eval('def self.m; end', nil, "foo", 100)
110110
end
111-
c.method(:m).source_location.should == ["foo", 100]
111+
location = c.method(:m).source_location
112+
ruby_version_is(""..."4.0") do
113+
location.should == ["foo", 100]
114+
end
115+
ruby_version_is("4.0") do
116+
location.should == ["foo", 100, 0, 100, 15]
117+
end
112118
end
113119

114120
describe "for a Method generated by respond_to_missing?" do

spec/ruby/core/proc/source_location_spec.rb

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,64 @@
1717
end
1818

1919
it "sets the first value to the path of the file in which the proc was defined" do
20-
file = @proc.source_location.first
20+
file = @proc.source_location[0]
2121
file.should be_an_instance_of(String)
2222
file.should == File.realpath('fixtures/source_location.rb', __dir__)
2323

24-
file = @proc_new.source_location.first
24+
file = @proc_new.source_location[0]
2525
file.should be_an_instance_of(String)
2626
file.should == File.realpath('fixtures/source_location.rb', __dir__)
2727

28-
file = @lambda.source_location.first
28+
file = @lambda.source_location[0]
2929
file.should be_an_instance_of(String)
3030
file.should == File.realpath('fixtures/source_location.rb', __dir__)
3131

32-
file = @method.source_location.first
32+
file = @method.source_location[0]
3333
file.should be_an_instance_of(String)
3434
file.should == File.realpath('fixtures/source_location.rb', __dir__)
3535
end
3636

37-
it "sets the last value to an Integer representing the line on which the proc was defined" do
38-
line = @proc.source_location.last
37+
it "sets the second value to an Integer representing the line on which the proc was defined" do
38+
line = @proc.source_location[1]
3939
line.should be_an_instance_of(Integer)
4040
line.should == 4
4141

42-
line = @proc_new.source_location.last
42+
line = @proc_new.source_location[1]
4343
line.should be_an_instance_of(Integer)
4444
line.should == 12
4545

46-
line = @lambda.source_location.last
46+
line = @lambda.source_location[1]
4747
line.should be_an_instance_of(Integer)
4848
line.should == 8
4949

50-
line = @method.source_location.last
50+
line = @method.source_location[1]
5151
line.should be_an_instance_of(Integer)
5252
line.should == 15
5353
end
5454

5555
it "works even if the proc was created on the same line" do
56-
proc { true }.source_location.should == [__FILE__, __LINE__]
57-
Proc.new { true }.source_location.should == [__FILE__, __LINE__]
58-
-> { true }.source_location.should == [__FILE__, __LINE__]
56+
ruby_version_is(""..."4.0") do
57+
proc { true }.source_location.should == [__FILE__, __LINE__]
58+
Proc.new { true }.source_location.should == [__FILE__, __LINE__]
59+
-> { true }.source_location.should == [__FILE__, __LINE__]
60+
end
61+
ruby_version_is("4.0") do
62+
proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19]
63+
Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23]
64+
-> { true }.source_location.should == [__FILE__, __LINE__, 8, __LINE__, 17]
65+
end
5966
end
6067

6168
it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do
62-
ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20
63-
ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34
64-
ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27
69+
ProcSpecs::SourceLocation.my_multiline_proc.source_location[1].should == 20
70+
ProcSpecs::SourceLocation.my_multiline_proc_new.source_location[1].should == 34
71+
ProcSpecs::SourceLocation.my_multiline_lambda.source_location[1].should == 27
6572
end
6673

6774
it "returns the location of the proc's body; not necessarily the proc itself" do
68-
ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41
69-
ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51
70-
ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46
75+
ProcSpecs::SourceLocation.my_detached_proc.source_location[1].should == 41
76+
ProcSpecs::SourceLocation.my_detached_proc_new.source_location[1].should == 51
77+
ProcSpecs::SourceLocation.my_detached_lambda.source_location[1].should == 46
7178
end
7279

7380
it "returns the same value for a proc-ified method as the method reports" do
@@ -86,6 +93,12 @@
8693

8794
it "works for eval with a given line" do
8895
proc = eval('-> {}', nil, "foo", 100)
89-
proc.source_location.should == ["foo", 100]
96+
location = proc.source_location
97+
ruby_version_is(""..."4.0") do
98+
location.should == ["foo", 100]
99+
end
100+
ruby_version_is("4.0") do
101+
location.should == ["foo", 100, 2, 100, 5]
102+
end
90103
end
91104
end

spec/ruby/core/unboundmethod/source_location_spec.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@
77
end
88

99
it "sets the first value to the path of the file in which the method was defined" do
10-
file = @method.source_location.first
10+
file = @method.source_location[0]
1111
file.should be_an_instance_of(String)
1212
file.should == File.realpath('fixtures/classes.rb', __dir__)
1313
end
1414

15-
it "sets the last value to an Integer representing the line on which the method was defined" do
16-
line = @method.source_location.last
15+
it "sets the second value to an Integer representing the line on which the method was defined" do
16+
line = @method.source_location[1]
1717
line.should be_an_instance_of(Integer)
1818
line.should == 5
1919
end
2020

2121
it "returns the last place the method was defined" do
22-
UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == 13
22+
UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location[1].should == 13
2323
end
2424

2525
it "returns the location of the original method even if it was aliased" do
26-
UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == 17
26+
UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location[1].should == 17
2727
end
2828

2929
it "works for define_method methods" do
@@ -54,6 +54,12 @@
5454
c = Class.new do
5555
eval('def m; end', nil, "foo", 100)
5656
end
57-
c.instance_method(:m).source_location.should == ["foo", 100]
57+
location = c.instance_method(:m).source_location
58+
ruby_version_is(""..."4.0") do
59+
location.should == ["foo", 100]
60+
end
61+
ruby_version_is("4.0") do
62+
location.should == ["foo", 100, 0, 100, 10]
63+
end
5864
end
5965
end

test/ruby/test_lambda.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,27 +276,27 @@ def test_break
276276
end
277277

278278
def test_do_lambda_source_location
279-
exp_lineno = __LINE__ + 3
279+
exp = [__LINE__ + 1, 12, __LINE__ + 5, 7]
280280
lmd = ->(x,
281281
y,
282282
z) do
283283
#
284284
end
285-
file, lineno = lmd.source_location
285+
file, *loc = lmd.source_location
286286
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
287-
assert_equal(exp_lineno, lineno, "must be at the beginning of the block")
287+
assert_equal(exp, loc)
288288
end
289289

290290
def test_brace_lambda_source_location
291-
exp_lineno = __LINE__ + 3
291+
exp = [__LINE__ + 1, 12, __LINE__ + 5, 5]
292292
lmd = ->(x,
293293
y,
294294
z) {
295295
#
296296
}
297-
file, lineno = lmd.source_location
297+
file, *loc = lmd.source_location
298298
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
299-
assert_equal(exp_lineno, lineno, "must be at the beginning of the block")
299+
assert_equal(exp, loc)
300300
end
301301

302302
def test_not_orphan_return

test/ruby/test_proc.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def test_binding_source_location
513513

514514
file, lineno = method(:source_location_test).to_proc.binding.source_location
515515
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
516-
assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427')
516+
assert_equal(@@line_of_source_location_test[0], lineno, 'Bug #2427')
517517
end
518518

519519
def test_binding_error_unless_ruby_frame
@@ -1499,15 +1499,19 @@ def test_to_s
14991499
assert_include(EnvUtil.labeled_class(name, Proc).new {}.to_s, name)
15001500
end
15011501

1502-
@@line_of_source_location_test = __LINE__ + 1
1502+
@@line_of_source_location_test = [__LINE__ + 1, 2, __LINE__ + 3, 5]
15031503
def source_location_test a=1,
15041504
b=2
15051505
end
15061506

15071507
def test_source_location
1508-
file, lineno = method(:source_location_test).source_location
1508+
file, *loc = method(:source_location_test).source_location
15091509
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
1510-
assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427')
1510+
assert_equal(@@line_of_source_location_test, loc, 'Bug #2427')
1511+
1512+
file, *loc = self.class.instance_method(:source_location_test).source_location
1513+
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
1514+
assert_equal(@@line_of_source_location_test, loc, 'Bug #2427')
15111515
end
15121516

15131517
@@line_of_attr_reader_source_location_test = __LINE__ + 3
@@ -1540,13 +1544,13 @@ def block_source_location_test(*args, &block)
15401544
end
15411545

15421546
def test_block_source_location
1543-
exp_lineno = __LINE__ + 3
1544-
file, lineno = block_source_location_test(1,
1547+
exp_loc = [__LINE__ + 3, 49, __LINE__ + 4, 49]
1548+
file, *loc = block_source_location_test(1,
15451549
2,
15461550
3) do
15471551
end
15481552
assert_match(/^#{ Regexp.quote(__FILE__) }$/, file)
1549-
assert_equal(exp_lineno, lineno)
1553+
assert_equal(exp_loc, loc)
15501554
end
15511555

15521556
def test_splat_without_respond_to

0 commit comments

Comments
 (0)