概要
foo.rb
foo/
bar/
baz.rb
という構造があったときに、"foo.rb内で"クラス Bar を定義しようとした瞬間、Bar is not a class
と怒られるという話です。
以下、具体的な再現コードです。
require 'bundler/inline'
require 'tmpdir'
gemfile(true) do
source 'https://rubygems.org'
gem 'zeitwerk', '= 2.6.13'
end
Dir.mktmpdir { |dir|
loadpath_1 = "#{dir}/loadpath_1"
FileUtils.mkpath "#{loadpath_1}"
IO.write("#{loadpath_1}/foo.rb", <<~RUBY)
module Foo
# 次の行に到達した瞬間エラーが発生
class Bar
end
end
RUBY
FileUtils.mkpath "#{loadpath_1}/foo/bar"
IO.write("#{loadpath_1}/foo/bar/baz.rb", <<~RUBY)
module Foo
class Bar
class Baz
end
end
end
RUBY
Zeitwerk::Loader.new.then { |loader|
loader.push_dir loadpath_1
loader.setup
}
# causes
# `loadpath_1/foo.rb:2:in `<module:Foo>': Bar is not a class (TypeError)`
p(Foo::Bar)
}
# 次の行に到達した瞬間エラーが発生
の箇所でエラーとなります。
関連情報
にも関連するセクションがあります。
Zeitwerkは事前定義されてない名前空間は全てmoduleであるとして決め打ちします。
つまり、こういうものを書いていると・・・
app/controllers/admins/items_controller.rbclass Admins # 外側をクラスにする class ItemsController # ここはclass・moduleどちらでも構わない end end
> Admins::ItemsController => TypeError: Admins is not a class
怒られてしまいます。
これと多少似ている状況です。本記事では、このAdminsに相当する名前空間である Foo::Bar
を、きちんと foo.rb の中でクラスとして定義しようとしています。
ところが、定義しようとするとその瞬間、 Foo::Bar
は module として autovivify により定義されてしまうのです。何のこっちゃと驚いてしまう現象です。
解決方法
とはいえ、そもそもZeitwerkで名前空間として扱われるようなクラスなら、ファイルとして独立させ、 foo/bar.rb
で定義するべきといえます。
Foo::Bar
は Foo
下のクラスなんだからと、 foo.rb
の中へ押し込んでしまうのはそもそも分かりづらいです。
Foo::Bar
の定義を foo/bar.rb
に分離してやれば解決します:
IO.write("#{loadpath_1}/foo.rb", <<~RUBY)
module Foo
# Bar の定義を除去
end
RUBY
# Bar の定義は foo/bar.rb に分離
FileUtils.mkpath "#{loadpath_1}/foo"
IO.write("#{loadpath_1}/foo/bar.rb", <<~RUBY)
module Foo
class Bar
end
end
RUBY