環境
対象 | バージョン |
---|---|
Rubocop | 1.56.1 |
TargetVersion |
3.2 |
Rails | 7.0.7 |
TL;DR
Style/ClassAndModuleChildren cop のオートコレクションは非安全である。特に既存のモデル名をスコープ名として使用している場合には、オートコレクションによってZeitwerkの定数解決に関するTypeErrorが発生する。
既存のモデル名をスコープ名としていた場合
以下の記事のように、既存のモデル名をスコープ名とするモデル設計を行っていたとする。
図1のようなディレクトリ構造で与えられている Post::Revision
モデルに対して、Style/ClassAndModuleChildren cop (EnforcedStyle: nested) のオートコレクションを適用する 1 。
app/models
├── post.rb
└── post
└── revision.rb
class Post; end
class Post::Revision; end
すると、post/revision.rb
は次の変更を受ける。
- class Post::Revision; end
+ module Post
+ class Revision; end
+ end
この状態で bin/rails server
などのコマンドを実行すると、「Post
はモジュールではない」というメッセージを持つTypeErrorが発生する。
app/models/post/revision.rb:3:in `<main>': Post is not a module (TypeError)
app/models/project.rb:1: previous definition of Post was here
原因と解決
Zeitwerkは、まず post.rb
を参照し post
をクラスの定数であると解決する。その次に post/revision.rb
の module Post
を見て、「post
はモジュールではないよ!」と怒る2。
したがって、次のように Post
をモジュール定数からクラス定数として書き換えればよい。
- module Post
+ class Post
class Revision; end
end
そもそも……
そもそもRubocopドキュメントの『Style/ClassAndModuleChildren』節に、このcopのオートコレクションが非安全であり、手動による監督が必要であると明記されている。Rubocop はコンパクト記法 (Post::Revision
) からネスト記法に移行する知識を (少なくとも現時点では) 持たないのだ。
Safety
Autocorrection is unsafe.
Moving from compact to nested children requires knowledge of whether the outer parent is a module or a class. Moving from nested to compact requires verification that the outer parent is defined elsewhere. RuboCop does not have the knowledge to perform either operation safely and thus requires manual oversight.
そもそもモデル設計に関して、admin などのクラス名とならないスコープ名は正しいものだが、「名前空間としてクラスを使」3ったモデル名は避けるべきだろう。たとえば、Railsドキュメントの『モデルを生成』には、「階層を指定」してモデルを生成するコマンドとして
rails generate model admin/account
が例示されている。つまり、階層を表す接頭辞 (スコープ名) に既存のモデル名を充てることは推奨されないということだ。
-
コマンド
rubocop -A <file>
で Rubocop の自動訂正を適用できる。 ↩ -
Zeitwerkの名前解決アルゴリズムについては、『Zeitwerkの壊し方』を参照してほしい。 ↩
-
https://qiita.com/fursich/items/717a720d9f4465e4cbbb#5-%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%A8%E3%81%97%E3%81%A6%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86 ↩