結論
「再代入する」
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というファイルをいつの間にか読んでいたとします。
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);
}
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;
きっかけ
ここからはじまる一連のツイートがきっかけです。
rubyのconstのsource_locationがほしい……。
— ノーマルトリガー (@_ksss_) January 27, 2017
@_ksss_ まずは Module#source_location(const_name) でしょうか
— _ko1 (@_ko1) January 27, 2017
@_ko1 @_ksss_ pryのshow-sourceコマンドで現状も取得できるので、今のままでも何とかなるのでは。
— joker1007 (@joker1007) January 27, 2017
@_ksss_ @joker1007 @_ko1 再定義時に警告出すために場所は保存してあるので名前が決まればすぐできる
— †ꁐ𐨥ᐠ†-̱̏ (@n0kada) January 27, 2017
@_ko1 @n0kada @joker1007 メソッド作らなくてもREPLで再代入してみれば調べられるならそれで十分な気もしてきました。
— ノーマルトリガー (@_ksss_) January 27, 2017
@_ksss_ @_ko1 @joker1007 class Foo; Bar = Bar; end
— †ꁐ𐨥ᐠ†-̱̏ (@n0kada) January 27, 2017
皆様ありがとうございました。