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 にしました。
うまくいかない例
いくつかうまくソースが取得出来ないケースを見つけているのですが、対応するのがつらいのでやる気があるときに頑張りたいと思います。
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
が使えるようになり、より簡潔にソースを取得出来るようになりました。
参考サイト