Ruby

Ruby の Proc オブジェクトのソースを表示するコード

More than 1 year has passed since last update.

Proc オブジェクトのソースを表示するコードを書いてみました。

proc_source.rb
require "ripper"

class Proc
  def proc_source
    return @source if @source
    filepath, start_pos = source_location

    code = File.readlines(filepath)[(start_pos-1)..-1].join
    tokens = Ripper.lex(code).drop_while do |t|
      !proc_token?(t)
    end

    @source = " " * tokens[0][0][1].to_i

    while token = tokens.shift
      @source += token[2]
      break if valid_proc_source?(@source)
    end

    @source
  end

  private

  def proc_token?(token)
    _pos, event, ident = token
    return true if event == :on_const && ident == "Proc"
    return true if event == :on_ident && ident == "lambda"
    return true if event == :on_ident && ident == "proc"
    return true if event == :on_kw && ident == "do"
    return true if event == :on_lbrace
    return true if event == :on_tlambda
    false
  end

  def valid_proc_source?(source)
    source = "Proc.new " + source if source =~ /\A\s*(do|{)/
    eval(source).instance_of?(Proc)
  rescue SyntaxError, ArgumentError
    false
  end
end

こんな感じのコードを書くと

a = -> { 'hi' }
puts a.proc_source

こんな感じで表示されます。

    -> { 'hi' }

Gem にしてみました

せっかくなので gem にしました。

https://github.com/siman-man/proc_source

うまくいかない例

いくつかうまくソースが取得出来ないケースを見つけているのですが、対応するのがつらいのでやる気があるときに頑張りたいと思います。

eval で作られる

lambda1 = eval('lambda { "hi" }')
puts lambda1.proc_source  #=> No such file or directory @ rb_sysopen - (eval) (Errno::ENOENT)

多重代入される

proc1, proc2 = Proc.new { "hi" }, Proc.new { "hey" }
puts proc1.proc_source #=>                Proc.new { "hi" }
puts proc2.proc_source #=>                Proc.new { "hi" }

etc...

#source_location が列番号も返してくれると問題も解決しやすくなりそうです。

参考サイト

https://docs.ruby-lang.org/ja/latest/class/Proc.html