Railsで定数を参照するときは完全修飾名を使った方が良い

発端

とある機能を開発していたところ、以下のエラーが出てwebsocketサーバーが起動しなくなりました。

NameError: uninitialized constant Concerns::XXXable

エラーの発生箇所は、とあるController内の include Concerns::XXXable でした。
Concerns::XXXable 自体は定義してあり、そのあたりの修正もしていなかったため困惑しましたが、どうやら調べていくとquerlyというgemをインストールしてから発生していることがわかりました。
なぜgemをインストールしただけでエラーが発生するようになったか、調査に結構時間がかかったので原因と対策をまとめました。

結論

原因: gem内でトップレベルにConcernsというnamespaceが切られていたこと

ざっくり説明すると、querly内のトップレベルのnamespaceにConcernsというnamespaceが切られていたため、その中で Concerns::XXXable を探そうとして、上記エラーが発生していました。

※すでに当該コードは下記PRで修正されています。

github.com

対策: 定数を参照するときは完全修飾名を使うようにする

標題の通り、 Site::Concerns::XXXable のように完全修飾名を使うことで予期せぬバグは発生しなくなります。
そもそも Concerns::Site::XXXable の方が良くないか?っていう話はいったん置いておきます。

詳しい原因

原因を詳しく説明すると、こんな感じです。

うちのアプリのwebsocketサーバーは、起動時に Rails.application.eager_load! を実行していました。
Rails.application.egaer_load!を実行すると

  1. autoload_pathsを先頭から探索
  2. const_missingが発生したmoduleからトップレベルの名前空間につくまで順に親を探索

の順に定数を探索しにいきます。

querlyをインストールしていない状態で include Concerns::XXXable をしているControllerを読み込むと

  1. Concernsが見つからない
  2. autload_pathsを探索してもConcerns::XXXableが見つからない
  3. 上記Controller内から順に親namespaceを探索し、Site::Concerns::XXXableが見つかる

という風にちゃんと読み込めます。

一方、querlyをインストールしている状態で include Concerns::XXXable をしているControllerを読み込むと Rails起動時にquerlyが読み込まれquerlyのConcernsが読み込まれてしまうため、 上記2の段階でquerlyのConcernsを参照してしまい、定数が見つからないというエラーが発生していました。

(全然わかりやすく説明できない...)

トップレベルにConcernsみたいなnamespaceが切られているgemをインストールしていなくても、Concerns::XXXable を読み込みにいく前に自分でConcerns(Rails標準のapp/controller/concernsなど)を読み込んでしまうと同様のエラーがおきました。

まとめ

定数を参照するときは完全修飾名を使うようにしよう!という話でした。
説明がぜんぜんうまくできませんでした...!