Como descobrir onde um método é definido em tempo de execução?

328

Recentemente, tivemos um problema em que, após uma série de confirmações, um processo de back-end não foi executado. Agora, éramos bons meninos e meninas e corríamos rake testapós cada check-in, mas, devido a algumas esquisitices no carregamento da biblioteca do Rails, isso só ocorreu quando o rodamos diretamente do Mongrel no modo de produção.

Eu rastreei o bug e isso ocorreu devido a uma nova gema do Rails sobrescrevendo um método na classe String de uma maneira que interrompeu um uso restrito no código Rails em tempo de execução.

Enfim, para encurtar a história, existe uma maneira, em tempo de execução, de perguntar a Ruby onde um método foi definido? Algo assim whereami( :foo )retorna /path/to/some/file.rb line #45? Nesse caso, me dizer que ele foi definido na classe String seria inútil, porque estava sobrecarregado por alguma biblioteca.

Eu não posso garantir que a fonte esteja no meu projeto, portanto, aguardar 'def foo'não necessariamente me dará o que eu preciso, para não mencionar se tenho muitos def foo , às vezes não sei até o tempo de execução qual deles posso estar usando.

Matt Rogish
fonte
1
No Ruby 1.8.7, um método especial foi adicionado especificamente para encontrar essas informações (e ainda está lá no 1.9.3) ... detalhes na minha resposta abaixo.
Alex D

Respostas:

419

Isso é realmente tarde, mas eis como você pode encontrar onde um método é definido:

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>

Se você usa Ruby 1.9+, pode 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]

Observe que isso não funcionará em tudo, como código compilado nativo. A classe Method também possui algumas funções interessantes, como Method # owner, que retorna o arquivo em que o método está definido.

EDIT: Veja também os __file__e __line__e notas para REE na outra resposta, que é acessível também. - wg

wesgarrison
fonte
1
source_location parece trabalho para 1.8.7-P334 usando ActiveSupport-2.3.14
Jeff Maass
depois de encontrar o método, tente o método do ownermétodo
Juguang 3/13/13
1
Qual é o número dois 2.method(:crime)?
stack1
1
uma instância da classeFixnum
kittehh
1
Nota importante: Isso não abrirá nenhum método definido dinamicamente method_missing. Portanto, se você tiver um módulo ou classe ancestral com um class_evalou define_methoddentro dele method_missing, esse método não funcionará.
Cdpalmer # 6/15
83

Você pode realmente ir um pouco além da solução acima. Para o Ruby 1.8 Enterprise Edition, existem os métodos __file__e __line__nas Methodinstâncias:

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 e além, existe source_location(obrigado 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
fonte
2
Eu recebo "NoMethodError: undefined method" para ambos __file__e __line__em qualquer Methodinstância de classe, ex: method(:method).__file__.
Vikrant Chaudhary
Qual versão do ruby ​​você possui?
James Adam
ruby 1.8.7 (23-06-2010, nível de patch 299) [x86_64-linux]
Vikrant Chaudhary
19
No Ruby 1.9, m.__file__e m.__line__foram substituídos por m.source_location.
Jonathan Tran
E o Ruby 2.1?
precisa
38

Estou atrasado para esta discussão e estou surpreso que ninguém tenha 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
fonte
2
Estou surpreso que você seja o primeiro a referenciar explicitamente a classe Method . Outro tesouro menos conhecido introduzido em 1.9: Method#parameters.
FNY
13

Copiando minha resposta de uma pergunta semelhante mais recente que adiciona novas informações a esse problema.

O Ruby 1.9 possui um método chamado source_location :

Retorna o nome do arquivo de origem Ruby e o número da linha que contém esse método ou nulo se esse método não foi definido no Ruby (ou seja, nativo)

Isso foi suportado para 1.8.7 por esta jóia:

Então você pode solicitar o método:

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

E, em seguida, pergunte para source_locationesse método:

m.source_location

Isso retornará uma matriz com o nome do arquivo e o número da linha. Por exemplo, para ActiveRecord::Base#validatesisso retorna:

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 classes e módulos, o Ruby não oferece suporte integrado, mas existe um excelente Gist por aí que se baseia em source_locationretornar arquivos para um determinado método ou primeiro arquivo para uma classe, se nenhum método foi especificado:

Em ação:

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]

Nos Macs com o TextMate instalado, isso também abre o editor no local especificado.

Laas
fonte
7

Isso pode ajudar, mas você teria que codificá-lo. Colado no blog:

Ruby fornece um retorno de chamada method_added () que é chamado toda vez que um método é adicionado ou redefinido em uma classe. Faz parte da classe Module, e toda classe é um módulo. Também existem dois retornos de chamada relacionados, method_removed () e method_undefined ().

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

Ken
fonte
6

Se você pode travar o método, receberá um backtrace que informará exatamente onde ele está.

Infelizmente, se você não pode travá-lo, não pode descobrir onde ele foi definido. Se você tentar usar o método substituindo-o ou substituindo-o, qualquer falha ocorrerá com o seu método substituído ou substituído, e isso não será útil.

Maneiras úteis de travar métodos:

  1. Passe nilonde ele o proíbe - muitas vezes o método gera uma ArgumentErrorou a sempre presente NoMethodErrorem uma classe nula.
  2. Se você tem conhecimento interno do método e sabe que o método, por sua vez, chama outro método, você pode sobrescrever o outro método e aumentar dentro dele.
Orion Edwards
fonte
Se você tiver acesso ao código, poderá inseri-lo com a mesma facilidade require 'ruby-debug'; debugger em que deseja entrar.
the Tin Man
"Infelizmente, se você não pode travá-lo, não pode descobrir onde foi definido." Não é verdade. Veja outras respostas.
Martin T.
6

Talvez o #source_locationpossa ajudar a descobrir de onde vem o método.

ex:

ModelName.method(:has_one).source_location

Retorna

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

OU

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

Retorna

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

Resposta muito tardia :) Mas as respostas anteriores não me ajudaram

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
fonte
Por que você está passando nil?
Arup Rakshit
@ArupRakshit da documentação: «Estabelece proc como manipulador para rastreamento ou desativa o rastreamento se o parâmetro for nil
tig
3

Você pode fazer algo assim:

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

Em seguida, verifique se o foo_finder é carregado primeiro com algo como

ruby -r foo_finder.rb railsapp

(Eu só brinquei com trilhos, então não sei exatamente, mas imagino que haja uma maneira de começar dessa maneira.)

Isso mostrará todas as redefinições da String # foo. Com um pouco de metaprogramação, você pode generalizá-lo para qualquer função que desejar. Mas ele precisa ser carregado ANTES do arquivo que realmente faz a redefinição.

AShelly
fonte
3

Você sempre pode obter um rastreamento de onde está usando caller().

o homem de lata
fonte
1
Isso é útil para encontrar o que chamou você, mas não é bom quando você está tentando descobrir onde algo foi definido.
the Tin Man