views:

2254

answers:

8

We recently had a problem where, after a series of commits had occurred, a backend process failed to run. Now, we were good little boys and girls and ran rake test after every check-in but due to some oddities in Rails' library loading, it only occurred when you ran it directly from mongrel in production mode.

Tracked the bug down and it was due to a new Rails gem overwriting a method in the String class in a way that broke one narrow use in runtime Rails code.

Anyway, long story short -- is there a way to, at runtime, ask Ruby where a method has been defined? Something like whereami( :foo ) that returned /path/to/some/file.rb line #45 (in this case, telling me that is was defined in class String would be unhelpful, because it was overloaded by some library) ? I cannot guarantee the source lives in my project, so grepping for 'def foo' won't necessarily give me what I need (not to mention if I have many def foo's, sometimes I don't know until runtime which one I may be using).

Thanks!!

+4  A: 

This may help but you would have to code it yourself. Pasted from the blog:

Ruby provides a method_added() callback that is invoked every time a method is added or redefined within a class. It’s part of the Module class, and every Class is a Module. There are also two related callbacks called method_removed() and method_undefined().

http://scie.nti.st/2008/9/17/making-methods-immutable-in-ruby

Ken
+3  A: 

If you can crash the method, you'll get a backtrace which will tell you exactly where it is.

Unfortunately, if you can't crash it then you can't find out where it has been defined. If you attempt to monkey with the method by overwriting it or overriding it, then any crash will come from your overwritten or overridden method, and it won't be any use.

Useful ways of crashing methods:

  1. Pass nil where it forbids it - a lot of the time the method will raise an ArgumentError or the ever-present NoMethodError on nilclass

  2. If you have inside knowledge of the method, and you know that the method in turn calls some other method, then you can overrwrite the other method, and raise inside that.

Orion Edwards
+3  A: 

You might be able to do something like this:

foo_finder.rb:

 class String
   def String.method_added(name)
     if (name==:foo)
        puts "defining #{name} in:\n\t"
        puts caller.join("\n\t")
     end
   end
 end

Then ensure foo_finder is loaded first with something like

ruby -r foo_finder.rb railsapp

(I've only messed with rails, so I don't know exactly, but I imagine there's a way to start it sort of like this.)

This will show you all the re-definitions of String#foo. With a little meta-programming, you could generalize it for whatever function you want. But it does need to be loaded BEFORE the file that actually does the re-definition.

AShelly
+3  A: 

You can always get a backtrace of where you are by using caller().

+1  A: 

I'm interested to know if you got this working.

I was trying to trace how the ActiveSupport::CoreExtensions::Float::Rounding module was giving us the "round_with_precision()" method...

require 'rubygems' 
require 'active_support'

print 1.12345678.round_with_precision(5)

Alas, no cigar. Any ideas?

+17  A: 

This is really late, but here's how you can find where a method is defined:

http://gist.github.com/76951

# How to find out where a method comes from.
# Learned this from Dave Thomas while teaching Advanced Ruby Studio
# Makes the case for separating method definitions into
# modules, especially when enhancing built-in classes.
module Perpetrator
  def crime
  end
end

class Fixnum
  include Perpetrator
end

p 2.method(:crime)
#<Method: Fixnum(Perpetrator)#crime>
wesgarrison
+2  A: 

Very late answer :) But earlier answers did not help me

set_trace_func proc{ |event, file, line, id, binding, classname|
  printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname
}
# call your method
set_trace_func nil
tig
+2  A: 

You can actually go a bit further than the solution above, using the __file__ and __line__ methods on Method instances:

require 'rubygems'
require 'activesupport'

m = 2.days.method(:ago)
# => #<Method: Fixnum(ActiveSupport::CoreExtensions::Numeric::Time)#ago>

m.__file__
# => "/Users/james/.rvm/gems/ree-1.8.7-2010.01/gems/activesupport-2.3.8/lib/active_support/core_ext/numeric/time.rb"
m.__line__
# => 64
James Adam
I like that one.
mikezter