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

# 結論

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

```rb
> 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というファイルをいつの間にか読んでいたとします。

```rb:foo.rb
class Foo
  class Bar
  end
end
```

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

```sh
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はどこで定義されているか調べてみましょう。

```sh
$ 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でも同じく取得できます。

```sh
$ 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`という特別なコマンドがあるようです。

```sh
$ 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のコードまで表示してくれるので便利ですが、期待していなかった挙動になることもあるようです。
実装まで終えていないのでどういう条件かはわからず。

```sh
$ 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`を使うとさっと確認できます。

```sh
$ 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メッセージで検索すると、以下のコードが見つかりました。

```c
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の値、ファイル名、行数などを持っているようです。

```c
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

# きっかけ

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

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">rubyのconstのsource_locationがほしい……。</p>&mdash; ノーマルトリガー (@_ksss_) <a href="https://twitter.com/_ksss_/status/824884333940858881">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/_ksss_">@_ksss_</a> まずは Module#source_location(const_name) でしょうか</p>&mdash; _ko1 (@_ko1) <a href="https://twitter.com/_ko1/status/824887005662228480">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/_ko1">@_ko1</a> <a href="https://twitter.com/_ksss_">@_ksss_</a> pryのshow-sourceコマンドで現状も取得できるので、今のままでも何とかなるのでは。</p>&mdash; joker1007 (@joker1007) <a href="https://twitter.com/joker1007/status/824888125998592001">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/_ksss_">@_ksss_</a> <a href="https://twitter.com/joker1007">@joker1007</a> <a href="https://twitter.com/_ko1">@_ko1</a> 再定義時に警告出すために場所は保存してあるので名前が決まればすぐできる</p>&mdash; †ꁐ𐨥ᐠ†-̱̏ (@n0kada) <a href="https://twitter.com/n0kada/status/824963213603139584">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/_ko1">@_ko1</a> <a href="https://twitter.com/n0kada">@n0kada</a> <a href="https://twitter.com/joker1007">@joker1007</a> メソッド作らなくてもREPLで再代入してみれば調べられるならそれで十分な気もしてきました。</p>&mdash; ノーマルトリガー (@_ksss_) <a href="https://twitter.com/_ksss_/status/824964593260785673">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr"><a href="https://twitter.com/_ksss_">@_ksss_</a> <a href="https://twitter.com/_ko1">@_ko1</a> <a href="https://twitter.com/joker1007">@joker1007</a> class Foo; Bar = Bar; end</p>&mdash; †ꁐ𐨥ᐠ†-̱̏ (@n0kada) <a href="https://twitter.com/n0kada/status/824964844382072832">January 27, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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