Thursday, June 22, 2006

Ruby: Meta Programming and Stack Traces

A couple of times I have run into trouble debugging or tracing a method in Ruby. Usually you can just call Kernal.caller to get a stack trace. But what if the method was generated? You don't get the correct location. For example:

class Kung
mstr = %-
def foo
puts 'Hello World from Kung.foo'
puts caller(0)
end
-
module_eval mstr
end

Kung.new.foo


Which generates the following output:

Hello World from Kung.foo
(eval):4:in `foo'
Kung-foo.rb:11


The stack trace only shows "(Eval):4:in 'foo'" which is almost useless. The "(Eval)" is a clue that the method was dynamically created using meta-programming. In this simple example, it is easy to find the dynamic code since it is near the caller "Kung-foo.rb:11". However in a real project it is frequently located far away, possibly in other source files.

To fix the stack trace, the author should use the optional arguments to method_eval as follows:

class Monkey
line, mstr = __LINE__, %-
def see
puts 'Hello World from Monkey.see'
puts caller(0)
end
-
module_eval mstr, __FILE__, line
end

Monkey.new.see


The output now shows the correct line number and file name:

Hello World from Monkey.see
Monkey-see.rb:5:in `see'
Monkey-see.rb:11


Update after reading the code in ActiveSupport core_ext\attribute_accessors.rb I found a nice way to do the above with fewer lines of code:

class Monkey
module_eval(<<-EOS, __FILE__, __LINE__)
def see
puts 'Hello World from Monkey.see'
puts caller(0)
end
EOS
end

Monkey.new.see

No comments: