Edited at

Rubyでclassやconstの定義位置を調べる方法

More than 1 year has passed since last update.


結論

「再代入する」

classやconstに、なんでもいいから値を再代入すると、warningで元々の定義位置を教えてくれるのでその場所を見に行けばOK。

> Some = 1 # class

(irb):1: warning: already initialized constant Some
./some.rb:123: warning: previous definition of Some was here

> Some::VALUE = 1 # const
(irb):2: warning: already initialized constant Some
./some.rb:125: warning: previous definition of Some was here


背景

Rubyでアプリケーションコードを読んでいると、「え、このclass or const(大文字英字で始まるいわゆる定数)の定義位置はどこだ?」という疑問が湧いてくることがあると思います。

アプリケーションコードの場合、沢山のgemを読み込んでいるため、検索するだけでも一苦労な場合があります。

メソッドの場合はMethod/UnbindMethod#source_locationを使えばファイル名と行数が手に入るのでいいのですが、classやconstの場合はRubyにAPIがありません。

classやconstは大抵のものは暗黙の命名規則(gem名 == class/module名とか、Foo::Bar::Baz classはfoo/bar/baz.rbにあるだろうとか)を読めば見つかるのですが、

稀にどこにあるのかわからないものもあるでしょう。

そんなときに知った解決方法が「再代入する」という方法です。


方法


irbの場合

foo.rbというファイルをいつの間にか読んでいたとします。


foo.rb

class Foo

class Bar
end
end

アプリケーションコードにFoo::Barという記述があり、どこで定義されたものなのか調べたいとき、自分自身を再代入します。

irb(main):001:0> Foo::Bar = Foo::Bar

(irb):1: warning: already initialized constant Foo::Bar
./foo.rb:2: warning: previous definition of Bar was here

すると、warningとして元々定義されていたファイルと行数が表示されます。

foo.rbの2行目で定義されていることがわかりました。

なぜ自分自身を再代入しているかというと、その後の副作用を減らすためです。

なのでREPLをすぐ終了させるのなら1を代入しても一応の目的は達成できます。


もうちょっと現実的な例を出すと、Rack::Task classはどこで定義されているか調べてみましょう。

$ irb -r rake

irb(main):001:0> Rake::Task = Rake::Task
(irb):1: warning: already initialized constant Rake::Task
/Users/ksss/.rbenv/versions/2.5.0-dev/lib/ruby/gems/2.5.0/gems/rake-12.0.0/lib/rake/task.rb:14: warning: previous definition of Task was here
=> Rake::Task

constでも同じく取得できます。

$ irb -r rake

irb(main):001:0> Rake::VERSION = Rake::VERSION
(irb):1: warning: already initialized constant Rake::VERSION
/Users/ksss/.rbenv/versions/2.5.0-dev/lib/ruby/gems/2.5.0/gems/rake-12.0.0/lib/rake/version.rb:2: warning: previous definition of VERSION was here
=> "12.0.0"

実験ではModule#const_setで定義したものでもeval系で定義したものでもファイル名や行数が設定されていれば取得できました。

ただ、一度再代入するともう一度調べたときには、再代入したときのファイル名と行数が表示されてしまうので、再度REPLを立ち上げ直さなければならない副作用があるのが欠点です。

適当なC拡張を書けばメソッド化できそうですが、そこまでしなくてもどうせ調べるときはREPLを使うので、再代入する方法で十分かなと思います。


pryの場合

pryにはshow-sourceという特別なコマンドがあるようです。

$ pry -r rake

[1] pry(main)> show-source Rake::Task

From: /Users/ksss/.rbenv/versions/2.5.0-dev/lib/ruby/gems/2.5.0/gems/rake-12.0.0/lib/rake/task.rb @ line 14:
Class name: Rake::Task
Number of lines: 376

class Task
# List of prerequisites for a task.
attr_reader :prerequisites

# List of actions attached to a task.
attr_reader :actions
...

pryの場合はclassのコードまで表示してくれるので便利ですが、期待していなかった挙動になることもあるようです。

実装まで終えていないのでどういう条件かはわからず。

$ pry -r rake

[1] pry(main)> show-source Rake::VERSION

From: /Users/ksss/.rbenv/versions/2.5.0-dev/lib/ruby/gems/2.5.0/gems/rake-12.0.0/lib/rake/ext/string.rb @ line 3:
Class name: String
Number of monkeypatches: 4. Use the `-a` option to display all available monkeypatches
Number of lines: 173

class String

rake_extension("ext") do

もちろんirbと同じく再代入する方法はpryでも使えます。


REPLじゃなくても

実はREPLじゃなくても確認はできるようです。

Rubyのコードを直接実行するようなruby -eだったりrails runnerを使うとさっと確認できます。

$ ruby -r rake -e 'Rake = 1'

-e:1: warning: already initialized constant Rake
/lib/rake.rb:23: warning: previous definition of Rake was here

$ bin/rails runner 'Rake = 1'
/lib/rails/commands/runner.rb:62: warning: already initialized constant Rake
/lib/rake.rb:23: warning: previous definition of Rake was here


CRubyでの実装

warningメッセージで検索すると、以下のコードが見つかりました。

rb_const_entry_t *ce;

...
VALUE name = QUOTE_ID(id);
visibility = ce->flag;
if (klass == rb_cObject)
rb_warn("already initialized constant %"PRIsVALUE"", name);
else
rb_warn("already initialized constant %"PRIsVALUE"::%"PRIsVALUE"",
rb_class_name(klass), name);
if (!NIL_P(ce->file) && ce->line) {
rb_compile_warn(RSTRING_PTR(ce->file), ce->line,
"previous definition of %"PRIsVALUE" was here", name);
}

https://github.com/ruby/ruby/blob/bef93a2ddf8e17bc636aef7719df56e4c722a722/variable.c#L2669-L2679

rb_const_entry_tという構造体がconstの値、ファイル名、行数などを持っているようです。

typedef struct rb_const_entry_struct {

rb_const_flag_t flag;
int line;
const VALUE value; /* should be mark */
const VALUE file; /* should be mark */
} rb_const_entry_t;

https://github.com/ruby/ruby/blob/bef93a2ddf8e17bc636aef7719df56e4c722a722/constant.h#L31-L36


きっかけ

ここからはじまる一連のツイートがきっかけです。

皆様ありがとうございました。