diff --git a/ast.c b/ast.c index 65ad1eaab35..721302c0304 100644 --- a/ast.c +++ b/ast.c @@ -312,6 +312,18 @@ node_id_for_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE l return INT2NUM(node_id); } +static VALUE +iseq_of_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE location) +{ + if (!rb_frame_info_p(location)) { + rb_raise(rb_eTypeError, "Thread::Backtrace::Location object expected"); + } + + const rb_iseq_t *iseq = rb_get_iseq_from_frame_info(location); + if (!iseq) return Qnil; + return rb_iseqw_new(iseq); +} + static VALUE ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { diff --git a/ast.rb b/ast.rb index 872157c48af..6151ab5c50a 100644 --- a/ast.rb +++ b/ast.rb @@ -472,3 +472,39 @@ class UnboundMethod RubyVM::InstructionSequence.of(self)&.syntax_tree end end + +class Thread::Backtrace::Location + # call-seq: + # location.syntax_tree -> Prism::Node | RubyVM::AbstractSyntaxTree::Node | nil + # + # Returns the AST node at this location, by re-parsing the source file. See + # RubyVM::InstructionSequence#syntax_tree for when +nil+ is returned. + # + # This method is experimental and might change without notice. + def syntax_tree + iseq = Primitive.iseq_of_backtrace_location(self) + return nil unless iseq + + node_id = Primitive.node_id_for_backtrace_location(self) + return nil unless node_id + + scope = iseq.syntax_tree + return nil unless scope + + if scope.is_a?(RubyVM::AbstractSyntaxTree::Node) + return Primitive.ast_node_find(scope, node_id) + end + + return scope if scope.node_id == node_id + + queue = [scope] + while (node = queue.shift) + node.compact_child_nodes.each do |child| + return child if child.node_id == node_id + queue << child + end + end + + nil + end +end diff --git a/test/ruby/test_proc_syntax_tree.rb b/test/ruby/test_proc_syntax_tree.rb index fc70ea96ecc..d6f25f04e4a 100644 --- a/test/ruby/test_proc_syntax_tree.rb +++ b/test/ruby/test_proc_syntax_tree.rb @@ -97,6 +97,19 @@ class TestProcSyntaxTree < Test::Unit::TestCase assert_equal iseq.source_hash, iseq.to_a[4][:source_hash] end + def test_backtrace_location_syntax_tree + with_loaded_file("def proc_ast_test_loc = caller_locations(0, 1).first\n") do + node = proc_ast_test_loc.syntax_tree + if PRISM + assert_equal :call_node, node.type + assert_equal "caller_locations(0, 1)", node.slice + else + assert_equal :FCALL, node.type + end + ensure + Object.remove_method(:proc_ast_test_loc) + end + end def test_parse_y_syntax_tree assert_separately(%w[--parser=parse.y], "#{<<~"begin;"}\n#{<<~'end;'}")