summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryui-knk <[email protected]>2024-07-10 22:28:22 +0900
committerYuichiro Kaneko <[email protected]>2024-07-23 12:36:00 +0900
commitf23485a8d693cb69fd7b84c1ab93cb4198ecfe4a (patch)
tree12b99fc07f809edbd9ac32f3951a07f46e30a8cd
parent5617fec1f81d0f05563b70fd04e9494896f6abc7 (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.md7
-rw-r--r--ast.c123
-rw-r--r--ast.rb57
-rw-r--r--test/ruby/test_ast.rb19
4 files changed, 206 insertions, 0 deletions
diff --git a/NEWS.md b/NEWS.md
index 6844332585..7819504947 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -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
diff --git a/ast.c b/ast.c
index 2f346cb727..79458eb266 100644
--- a/ast.c
+++ b/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
@@ -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);
}
diff --git a/ast.rb b/ast.rb
index 51ee5b3d59..aae68fd9a7 100644
--- a/ast.rb
+++ b/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
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