¿Cómo encontrar dónde se define un método en tiempo de ejecución?

328

Recientemente tuvimos un problema en el que, después de una serie de confirmaciones, no se pudo ejecutar un proceso de fondo. Ahora, éramos buenos niños y niñas y corríamos rake testdespués de cada registro, pero, debido a algunas rarezas en la carga de la biblioteca de Rails, solo ocurría cuando lo ejecutamos directamente desde Mongrel en modo de producción.

Rastreé el error y se debió a una nueva gema de Rails que sobrescribió un método en la clase String de una manera que rompió un uso limitado en el código de Rails en tiempo de ejecución.

De todos modos, para resumir, ¿hay alguna manera, en tiempo de ejecución, de preguntarle a Ruby dónde se ha definido un método? Algo así whereami( :foo )regresa /path/to/some/file.rb line #45? En este caso, decirme que se definió en la clase String no sería útil, porque alguna biblioteca lo sobrecargó.

No puedo garantizar que la fuente viva en mi proyecto, por lo que buscarla 'def foo'no necesariamente me dará lo que necesito, sin mencionar si tengo muchas def foo , a veces no sé hasta el momento de ejecución cuál puedo estar usando.

Matt Rogish
fuente
1
En Ruby 1.8.7, se agregó un método especial específicamente para encontrar esta información (y todavía está allí en 1.9.3) ... detalles en mi respuesta a continuación.
Alex D

Respuestas:

419

Esto es realmente tarde, pero así es cómo puedes encontrar dónde se define un método:

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) # The "2" here is an instance of Fixnum.
#<Method: Fixnum(Perpetrator)#crime>

Si estás usando Ruby 1.9+, puedes usar source_location

require 'csv'

p CSV.new('string').method(:flock)
# => #<Method: CSV#flock>

CSV.new('string').method(:flock).source_location
# => ["/path/to/ruby/1.9.2-p290/lib/ruby/1.9.1/forwardable.rb", 180]

Tenga en cuenta que esto no funcionará en todo, como el código compilado nativo. La clase Method también tiene algunas funciones ordenadas, como Method # owner, que devuelve el archivo donde se define el método.

EDIT: consulta las __file__y los __line__y las notas para REE en la otra respuesta, que están muy bien también. - wg

Wesgarrison
fuente
1
source_location parece funcionar para 1.8.7-p334 usando activesupport-2.3.14
Jeff Maass
después de encontrar el método, pruebe el método del ownerMétodo
Juguang
1
¿Cuál es el número dos 2.method(:crime)?
stack1
1
una instancia de la claseFixnum
kitteehh
1
Nota importante: Esto no extraerá ningún método definido dinámicamente de method_missing. Entonces, si tiene un módulo o una clase ancestral con un class_evalo define_methoddentro de method_missing, entonces este método no funcionará.
cdpalmer
83

De hecho, puede ir un poco más allá de la solución anterior. Para Ruby 1.8 Enterprise Edition, existen los métodos __file__y __line__en las Methodinstancias:

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

Para Ruby 1.9 y más allá, hay source_location(¡gracias 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]
James Adam
fuente
2
Me sale "NoMethodError: método no definido" para ambos __file__y __line__en cualquier Methodinstancia de clase, por ejemplo: method(:method).__file__.
Vikrant Chaudhary
¿Qué versión de ruby ​​tienes?
James Adam
ruby 1.8.7 (2010-06-23 patchlevel 299) [x86_64-linux]
Vikrant Chaudhary
19
En Ruby 1.9, m.__file__y m.__line__han sido reemplazados por m.source_location.
Jonathan Tran
¿Qué hay de Ruby 2.1?
Lokesh
38

Llego tarde a este hilo y me sorprende que nadie lo haya mencionado Method#owner.

class A; def hello; puts "hello"; end end
class B < A; end
b = B.new
b.method(:hello).owner
=> A
Alex D
fuente
2
Me sorprende que seas el primero en hacer referencia explícita a la clase Method Otro tesoro menos conocido introdujo en 1,9: Method#parameters.
fny
13

Copiando mi respuesta de una pregunta similar más reciente que agrega nueva información a este problema.

Ruby 1.9 tiene un método llamado source_location :

Devuelve el nombre del archivo fuente de Ruby y el número de línea que contiene este método o nulo si este método no se definió en Ruby (es decir, nativo)

Esto ha sido respaldado a respaldado 1.8.7 por esta gema:

Entonces puede solicitar el método:

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

Y luego pregunta por el source_location ese método:

m.source_location

Esto devolverá una matriz con nombre de archivo y número de línea. Por ejemplo paraActiveRecord::Base#validates esto vuelve:

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]

Para las clases y los módulos, Ruby no ofrece soporte integrado, pero existe una excelente esencia que se basa en source_location devolver el archivo para un método dado o el primer archivo para una clase si no se especificó ningún método:

En acción:

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]

En Macs con TextMate instalado, esto también muestra el editor en la ubicación especificada.

Laas
fuente
7

Esto puede ayudar, pero tendría que codificarlo usted mismo. Pegado del blog:

Ruby proporciona una devolución de llamada method_added () que se invoca cada vez que se agrega o redefine un método dentro de una clase. Es parte de la clase Módulo, y cada Clase es un Módulo. También hay dos devoluciones de llamada relacionadas llamadas method_removed () y method_undefined ().

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

Conocido
fuente
6

Si puede bloquear el método, obtendrá una traza inversa que le dirá exactamente dónde está.

Desafortunadamente, si no puede bloquearlo, entonces no puede averiguar dónde se ha definido. Si intenta simular el método sobrescribiéndolo o anulándolo, entonces cualquier bloqueo vendrá de su método sobrescrito o anulado, y no servirá de nada.

Formas útiles de métodos de bloqueo:

  1. Pase nildonde lo prohíba: muchas veces el método generará una ArgumentErrorpresencia siempre presenteNoMethodError en una clase nula.
  2. Si tiene conocimiento interno del método, y sabe que el método a su vez llama a otro método, entonces puede sobrescribir el otro método y aumentarlo.
Orion Edwards
fuente
Si tiene acceso al código, puede insertarlo con la misma facilidad require 'ruby-debug'; debugger donde desee ingresar.
The Tin Man
"Desafortunadamente, si no puedes bloquearlo, entonces no puedes averiguar dónde se ha definido". No es verdad. Se otras respuestas.
Martin T.
6

Tal vez el #source_locationpuede ayudar a encontrar de dónde viene el método.

ex:

ModelName.method(:has_one).source_location

Regreso

[project_path/vendor/ruby/version_number/gems/activerecord-number/lib/active_record/associations.rb", line_number_of_where_method_is]

O

ModelName.new.method(:valid?).source_location

Regreso

[project_path/vendor/ruby/version_number/gems/activerecord-number/lib/active_record/validations.rb", line_number_of_where_method_is]
Samda
fuente
4

Respuesta muy tardía :) Pero las respuestas anteriores no me ayudaron

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
fuente
¿Por qué estás pasando nil?
Arup Rakshit
@ArupRakshit de la documentación: «Establece proc como el controlador para el rastreo, o deshabilita el rastreo si el parámetro es" nil.
tig
3

Es posible que pueda hacer algo como esto:

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

Luego, asegúrese de que foo_finder se cargue primero con algo como

ruby -r foo_finder.rb railsapp

(Solo me he metido con los rieles, así que no sé exactamente, pero imagino que hay una manera de comenzar así).

Esto le mostrará todas las redefiniciones de String # foo. Con un poco de metaprogramación, puede generalizarlo para cualquier función que desee. Pero debe cargarse ANTES del archivo que realmente realiza la redefinición.

AShelly
fuente
3

Siempre puede obtener un seguimiento de dónde está utilizando caller().

el hombre de hojalata
fuente
1
Esto es útil para encontrar lo que te llamó, pero no es bueno cuando intentas encontrar dónde se definió algo.
The Tin Man