diff options
author | yui-knk <[email protected]> | 2024-07-10 22:28:22 +0900 |
---|---|---|
committer | Yuichiro Kaneko <[email protected]> | 2024-07-23 12:36:00 +0900 |
commit | f23485a8d693cb69fd7b84c1ab93cb4198ecfe4a (patch) | |
tree | 12b99fc07f809edbd9ac32f3951a07f46e30a8cd | |
parent | 5617fec1f81d0f05563b70fd04e9494896f6abc7 (diff) |
[Feature #20624] Enhance `RubyVM::AbstractSyntaxTree::Node#locations`
This commit introduce `RubyVM::AbstractSyntaxTree::Node#locations` method
and `RubyVM::AbstractSyntaxTree::Location` class.
Ruby AST node will hold multiple locations information.
`RubyVM::AbstractSyntaxTree::Node#locations` provides a way to access
these locations information.
`RubyVM::AbstractSyntaxTree::Location` is a class which holds these location information:
* `#first_lineno`
* `#first_column`
* `#last_lineno`
* `#last_column`
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/11226
-rw-r--r-- | NEWS.md | 7 | ||||
-rw-r--r-- | ast.c | 123 | ||||
-rw-r--r-- | ast.rb | 57 | ||||
-rw-r--r-- | test/ruby/test_ast.rb | 19 |
4 files changed, 206 insertions, 0 deletions
@@ -44,6 +44,12 @@ Note: We're only listing outstanding class updates. * Range#size now raises TypeError if the range is not iterable. [[Misc #18984]] +* RubyVM::AbstractSyntaxTree + + * Add `RubyVM::AbstractSyntaxTree::Node#locations` method which returns location objects + associated with the AST node. [[Feature #20624]] + * Add `RubyVM::AbstractSyntaxTree::Location` class which holds location information. [[Feature #20624]] + ## Stdlib updates * Tempfile @@ -160,3 +166,4 @@ See GitHub releases like [GitHub Releases of Logger](https://github.com/ruby/log [Feature #20429]: https://bugs.ruby-lang.org/issues/20429 [Feature #20443]: https://bugs.ruby-lang.org/issues/20443 [Feature #20497]: https://bugs.ruby-lang.org/issues/20497 +[Feature #20624]: https://bugs.ruby-lang.org/issues/20624 @@ -14,6 +14,7 @@ static VALUE rb_mAST; static VALUE rb_cNode; +static VALUE rb_cLocation; struct ASTNodeData { VALUE ast_value; @@ -43,6 +44,32 @@ static const rb_data_type_t rb_node_type = { RUBY_TYPED_FREE_IMMEDIATELY, }; +struct ASTLocationData { + int first_lineno; + int first_column; + int last_lineno; + int last_column; +}; + +static void +location_gc_mark(void *ptr) +{ +} + +static size_t +location_memsize(const void *ptr) +{ + return sizeof(struct ASTLocationData); +} + +static const rb_data_type_t rb_location_type = { + "AST/location", + {location_gc_mark, RUBY_TYPED_DEFAULT_FREE, location_memsize,}, + 0, 0, + RUBY_TYPED_FREE_IMMEDIATELY, +}; + + static VALUE rb_ast_node_alloc(VALUE klass); static void @@ -721,6 +748,45 @@ ast_node_children(rb_execution_context_t *ec, VALUE self) } static VALUE +location_new(rb_code_location_t *loc) +{ + VALUE obj; + struct ASTLocationData *data; + + obj = TypedData_Make_Struct(rb_cLocation, struct ASTLocationData, &rb_location_type, data); + data->first_lineno = loc->beg_pos.lineno; + data->first_column = loc->beg_pos.column; + data->last_lineno = loc->end_pos.lineno; + data->last_column = loc->end_pos.column; + + return obj; +} + +static VALUE +node_locations(VALUE ast_value, const NODE *node) +{ + enum node_type type = nd_type(node); + switch (type) { + case NODE_ARGS_AUX: + case NODE_LAST: + break; + default: + return rb_ary_new_from_args(1, location_new(nd_code_loc(node))); + } + + rb_bug("node_locations: unknown node: %s", ruby_node_name(type)); +} + +static VALUE +ast_node_locations(rb_execution_context_t *ec, VALUE self) +{ + struct ASTNodeData *data; + TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); + + return node_locations(data->ast_value, data->node); +} + +static VALUE ast_node_first_lineno(rb_execution_context_t *ec, VALUE self) { struct ASTNodeData *data; @@ -823,6 +889,61 @@ ast_node_script_lines(rb_execution_context_t *ec, VALUE self) return rb_parser_build_script_lines_from(ret); } +static VALUE +ast_location_first_lineno(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->first_lineno); +} + +static VALUE +ast_location_first_column(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->first_column); +} + +static VALUE +ast_location_last_lineno(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->last_lineno); +} + +static VALUE +ast_location_last_column(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->last_column); +} + +static VALUE +ast_location_inspect(rb_execution_context_t *ec, VALUE self) +{ + VALUE str; + VALUE cname; + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + cname = rb_class_path(rb_obj_class(self)); + str = rb_str_new2("#<"); + + rb_str_append(str, cname); + rb_str_catf(str, ":@%d:%d-%d:%d>", + data->first_lineno, data->first_column, + data->last_lineno, data->last_column); + + return str; +} + #include "ast.rbinc" void @@ -830,5 +951,7 @@ Init_ast(void) { rb_mAST = rb_define_module_under(rb_cRubyVM, "AbstractSyntaxTree"); rb_cNode = rb_define_class_under(rb_mAST, "Node", rb_cObject); + rb_cLocation = rb_define_class_under(rb_mAST, "Location", rb_cObject); rb_undef_alloc_func(rb_cNode); + rb_undef_alloc_func(rb_cLocation); } @@ -272,5 +272,62 @@ module RubyVM::AbstractSyntaxTree nil end end + + # call-seq: + # node.locations -> array + # + # Returns location objects associated with the AST node. + # The returned array contains RubyVM::AbstractSyntaxTree::Location. + def locations + Primitive.ast_node_locations + end + end + + # RubyVM::AbstractSyntaxTree::Location instances are created by + # RubyVM::AbstractSyntaxTree#locations. + # + # This class is MRI specific. + # + class Location + + # call-seq: + # location.first_lineno -> integer + # + # The line number in the source code where this AST's text began. + def first_lineno + Primitive.ast_location_first_lineno + end + + # call-seq: + # location.first_column -> integer + # + # The column number in the source code where this AST's text began. + def first_column + Primitive.ast_location_first_column + end + + # call-seq: + # location.last_lineno -> integer + # + # The line number in the source code where this AST's text ended. + def last_lineno + Primitive.ast_location_last_lineno + end + + # call-seq: + # location.last_column -> integer + # + # The column number in the source code where this AST's text ended. + def last_column + Primitive.ast_location_last_column + end + + # call-seq: + # location.inspect -> string + # + # Returns debugging information about this location as a string. + def inspect + Primitive.ast_location_inspect + end end end diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index ebef14c14b..4c579287a9 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -1297,6 +1297,13 @@ dummy end; end + def test_locations + node = RubyVM::AbstractSyntaxTree.parse("1 + 2") + locations = node.locations + + assert_equal(RubyVM::AbstractSyntaxTree::Location, locations[0].class) + end + private def assert_error_tolerant(src, expected, keep_tokens: false) @@ -1316,4 +1323,16 @@ dummy assert_equal(expected, str) node end + + class TestLocation < Test::Unit::TestCase + def test_lineno_and_column + node = RubyVM::AbstractSyntaxTree.parse("1 + 2") + location = node.locations[0] + + assert_equal(1, location.first_lineno) + assert_equal(0, location.first_column) + assert_equal(1, location.last_lineno) + assert_equal(5, location.last_column) + end + end end |