Ruby运行时能否获取call stack的行列范围而非仅文件名与起始行?
在Ruby中获取调用栈的行列范围
Ruby标准库的caller_locations默认仅返回调用的起始行号,不会包含结束行列信息——这是因为Ruby运行时在执行阶段已将代码编译为字节码,不再保留语法层面的调用范围数据。不过,不借助Prism等外部工具,我们可以通过Ruby内置的Ripper解析器结合调用栈信息,在运行时计算出调用的完整行列范围。
实现思路
- 用Ripper解析调用方文件的AST(抽象语法树),提前收集目标方法调用的起始/结束位置;
- 在方法内部,通过
caller_locations获取调用的起始行和列(Ruby 2.6+支持column方法); - 匹配预收集的位置数据,输出完整的行列区间。
代码示例
修改原sample.rb如下:
# sample.rb require 'ripper' # 解析指定文件,收集所有`sample`方法调用的位置范围 def collect_sample_call_ranges(filename) code = File.read(filename) call_ranges = {} # 递归遍历AST节点 traverse = lambda do |node| return unless node.is_a?(Array) # 识别`sample`方法调用节点(适配Ripper AST结构) if node[0] == :call && node[1].is_a?(Array) && node[1][0] == :var_ref && node[1][1].is_a?(Array) && node[1][1][0] == :@ident && node[1][1][1] == 'sample' # 提取调用的起始/结束行列(Ripper节点位置格式:[type, start_line, start_col, end_line, end_col]) pos = node[2] start_key = [pos[1], pos[2]] call_ranges[start_key] = [pos[3], pos[4]] end node.each { |child| traverse.call(child) } end traverse.call(Ripper.sexp(code)) call_ranges end # 预收集当前文件中`sample`调用的位置范围 SAMPLE_CALL_RANGES = collect_sample_call_ranges(__FILE__) def sample(a = nil, b = nil, c = nil, &block) yield if block_given? caller_locations.each do |loc| start_line = loc.lineno start_col = loc.column # 匹配预收集的范围并格式化输出 if end_pos = SAMPLE_CALL_RANGES[[start_line, start_col]] end_line, end_col = end_pos puts "#{loc.path}:#{start_line}:#{start_col}-#{end_line}:#{end_col}:in '#{loc.label}'" else # 无匹配时输出默认格式 puts loc end end end sample( 1, 2, 3 ) sample do puts "lol" end
执行效果
运行修改后的文件,输出将符合预期:
$ ruby sample.rb sample.rb:8:0-12:1:in '<main>' lol sample.rb:14:0-16:3:in '<main>'
注意事项
- 该方案依赖静态解析AST,仅能识别静态编写的方法调用,动态生成的调用(如
send(:sample))无法匹配; - Ripper的AST结构可能随Ruby版本变动,需要根据使用的Ruby版本调整节点判断逻辑;
- 若方法调用来自其他文件,需要解析对应文件的AST并收集位置信息。
内容的提问来源于stack exchange,提问作者schmijos
相关产品推荐
相关产品推荐

