DEV Community

Shuichi Tamayose
Shuichi Tamayose

Posted on

Get a ruby method block source

Overview

I try it.

block_source.rb (require RUBY_VERSION >= '2.6.0'

def inspector(ast:, &block)
  ast.children.each do |child|
    next unless child.instance_of?(RubyVM::AbstractSyntaxTree::Node)
    yield child
    inspector(ast: child, &block)
  end
end

def find_node(ast:, type:, lineno:)
  inspector(ast: ast) do |node|
    return node if node.type == type && node.first_lineno == lineno
  end

  nil
end

def extract_source(node:, source:)
  first_lineno = node.first_lineno - 1
  first_column = node.first_column
  last_lineno = node.last_lineno - 1
  last_column = node.last_column - 1

  if first_lineno == last_lineno
    source[first_lineno][first_column..last_column]
  else
    src = ' ' * first_column + source[first_lineno][first_column..]
    ((first_lineno + 1)...last_lineno).each do |lineno|
      src << source[lineno]
    end
    src << source[last_lineno][0..last_column]
  end
end

module Kernel
  # RUBY_VERSION >= '2.6.0'
  def block_source
    @file_specs ||= {}
    bl = caller_locations.last
    source = @file_specs.dig(bl.path, :source) || File.readlines(bl.path)
    @file_specs[bl.path] ||= { source: source, ast: RubyVM::AbstractSyntaxTree.parse(source.join) }
    node = find_node(
      ast: @file_specs.dig(bl.path, :ast),
      type: :ITER,
      lineno: bl.lineno
    )
    extract_source(node: node.children[1], source: source) if node
  end
end

Usage

require_relative 'block_source'

def foo
  pp block_source
end

foo { 'hello' }   #=> " { 'hello' }"
foo { |i| i * 3 } #=> " { |i| i * 3 }"
foo               #=> nil

Detail

First, get a caller information by bl = caller_locations.last.

bl = caller_locations.last

Second, find ITER node. (ITER node is method call with block.)

node = find_node(
  ast: @file_specs.dig(bl.path, :ast),
  type: :ITER,
  lineno: bl.lineno
)

find_node implement is simple. call inspector and find node which match conditions.

def find_node(ast:, type:, lineno:)
  inspector(ast: ast) do |node|
    return node if node.type == type && node.first_lineno == lineno
  end

  nil
end

Finarlly, extract block source from ITER. (ITER node has block body in a second children element.

extract_source(node: node.children[1], source: source) if node

TODO

Currently, it isn't possible to get a column number from the information of the caller_locations. so, this case not working.

foo { 'hello' }; bar { 'world' }
" { 'hello' }"
" { 'hello' }"

Top comments (0)