[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`
This commit is contained in:
yui-knk 2024-07-10 22:28:22 +09:00 коммит произвёл Yuichiro Kaneko
Родитель 5617fec1f8
Коммит f23485a8d6
4 изменённых файлов: 206 добавлений и 0 удалений

Просмотреть файл

@ -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

123
ast.c
Просмотреть файл

@ -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
@ -720,6 +747,45 @@ ast_node_children(rb_execution_context_t *ec, VALUE self)
return node_children(data->ast_value, data->node);
}
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)
{
@ -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);
}

57
ast.rb
Просмотреть файл

@ -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

Просмотреть файл

@ -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