How to find where a method is defined at runtime?

2019-01-01 06:16发布

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 we ran it directly from Mongrel in production mode.

I 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 the runtime Rails code.

Anyway, long story short, is there a way, at runtime, to ask Ruby where a method has been defined? Something like whereami( :foo ) that returns /path/to/some/file.rb line #45? In this case, telling me that it 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.

10条回答
回忆,回不去的记忆
2楼-- · 2019-01-01 06:35

Copying my answer from a newer similar question that adds new information to this problem.

Ruby 1.9 has method called source_location:

Returns the Ruby source filename and line number containing this method or nil if this method was not defined in Ruby (i.e. native)

This has been backported to 1.8.7 by this gem:

So you can request for the method:

m = Foo::Bar.method(:create)

And then ask for the source_location of that method:

m.source_location

This will return an array with filename and line number. E.g for ActiveRecord::Base#validates this returns:

ActiveRecord::Base.method(:validates).source_location
# => ["/Users/laas/.rvm/gems/ruby-1.9.2-p0@arveaurik/gems/activemodel-3.2.2/lib/active_model/validations/validates.rb", 81]

For classes and modules, Ruby does not offer built in support, but there is an excellent Gist out there that builds upon source_location to return file for a given method or first file for a class if no method was specified:

In action:

where_is(ActiveRecord::Base, :validates)

# => ["/Users/laas/.rvm/gems/ruby-1.9.2-p0@arveaurik/gems/activemodel-3.2.2/lib/active_model/validations/validates.rb", 81]

On Macs with TextMate installed, this also pops up the editor at the specified location.

查看更多
流年柔荑漫光年
3楼-- · 2019-01-01 06:38

You can actually go a bit further than the solution above. For Ruby 1.8 Enterprise Edition, there is 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

For Ruby 1.9 and beyond, there is source_location (thanks Jonathan!):

require 'active_support/all'
m = 2.days.method(:ago)
# => #<Method: Fixnum(Numeric)#ago>    # comes from the Numeric module

m.source_location   # show file and line
# => ["/var/lib/gems/1.9.1/gems/activesupport-3.0.6/.../numeric/time.rb", 63]
查看更多
刘海飞了
4楼-- · 2019-01-01 06:39

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

查看更多
十年一品温如言
5楼-- · 2019-01-01 06:43

I'm coming late to this thread, and am surprised that nobody mentioned Method#owner.

class A; def hello; puts "hello"; end end
class B < A; end
b = B.new
b.method(:hello).owner
=> A
查看更多
忆尘夕之涩
6楼-- · 2019-01-01 06:48

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.

查看更多
冷夜・残月
7楼-- · 2019-01-01 06:50

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
查看更多
登录 后发表回答