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

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 が列番号も返してくれると問題も解決しやすくなりそうです。


追記 (2019/01/30)

Ruby 2.6 から RubyVM::AbstractSyntaxTree.of が使えるようになり、より簡潔にソースを取得出来るようになりました。

https://qiita.com/siman/items/b6f856263b11df74d852


参考サイト

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