diff --git a/README.md b/README.md index f7b5851..7073dc2 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,8 @@ class Hotel < ApplicationRecord end ``` +When autoloaded, Zeitwerk verifies the expected constant (`Hotel` in the example) stores a class or module object. If it doesn't, `Zeitwerk::Error` is raised. + An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup. diff --git a/lib/zeitwerk/core_ext/module.rb b/lib/zeitwerk/core_ext/module.rb index 2c98984..6f2350d 100644 --- a/lib/zeitwerk/core_ext/module.rb +++ b/lib/zeitwerk/core_ext/module.rb @@ -3,7 +3,14 @@ module Zeitwerk::ConstAdded def const_added(cname) if loader = Zeitwerk::ExplicitNamespace.__loader_for(self, cname) - loader.on_namespace_loaded(const_get(cname, false)) + namespace = const_get(cname, false) + + unless namespace.is_a?(Module) + cref = Zeitwerk::Cref.new(self, cname) + raise Zeitwerk::Error, "#{cref.path} is expected to be a namespace, should be a class or module (got #{namespace.class})" + end + + loader.on_namespace_loaded(namespace) end super end diff --git a/test/lib/zeitwerk/test_explicit_namespace.rb b/test/lib/zeitwerk/test_explicit_namespace.rb index 0a69d60..548b5b4 100644 --- a/test/lib/zeitwerk/test_explicit_namespace.rb +++ b/test/lib/zeitwerk/test_explicit_namespace.rb @@ -166,4 +166,17 @@ def self.hash(_) assert M::X end end + + test "if the expected constant does not define a class or module object, we raise a controlled error" do + on_teardown { remove_const :Hotel } + + files = [ + ["hotel.rb", "Hotel = 1"], + ["hotel/pricing.rb", "class Hotel::Pricing; end"] + ] + with_setup(files) do + error = assert_raises(Zeitwerk::Error) { Hotel } + assert_equal "Hotel is expected to be a namespace, should be a class or module (got Integer)", error.message + end + end end