better error message for constants autoloaded from anonymous modules [fixes #13204]

load_missing_constant is a private method that basically plays the role of const_missing.
This method has an error condition that is surprising: it raises if the class or module
already has the missing constant. How is it possible that if the class of module has
the constant Ruby has called const_missing in the first place?

The answer is that the from_mod argument is self except for anonymous modules, because
const_missing passes down Object in such case (see the comment in the source code of the
patch for the rationale).

But then, it is better to pass down Object *if Object is also missing the constant* and
otherwise err with an informative message right away.
This commit is contained in:
Xavier Noria 2013-12-06 19:13:51 +01:00
parent 8ef1ef1b82
commit 01c9782fa2
2 changed files with 22 additions and 24 deletions

@ -176,14 +176,22 @@ def self.exclude_from(base)
end end
def const_missing(const_name) def const_missing(const_name)
# The interpreter does not pass nesting information, and in the from_mod = anonymous? ? guess_for_anonymous(const_name) : self
# case of anonymous modules we cannot even make the trade-off of
# assuming their name reflects the nesting. Resort to Object as
# the only meaningful guess we can make.
from_mod = anonymous? ? ::Object : self
Dependencies.load_missing_constant(from_mod, const_name) Dependencies.load_missing_constant(from_mod, const_name)
end end
# Dependencies assumes the name of the module reflects the nesting (unless
# it can be proven that is not the case), and the path to the file that
# defines the constant. Anonymous modules cannot follow these conventions
# and we assume therefore the user wants to refer to a top-level constant.
def guess_for_anonymous(const_name)
if Object.const_defined?(const_name)
raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module"
else
Object
end
end
def unloadable(const_desc = self) def unloadable(const_desc = self)
super(const_desc) super(const_desc)
end end
@ -456,8 +464,6 @@ def load_missing_constant(from_mod, const_name)
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end end
raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false)
qualified_name = qualified_name_for from_mod, const_name qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore path_suffix = qualified_name.underscore

@ -530,29 +530,21 @@ module A
end end
end end
def test_const_missing_should_not_double_load def test_const_missing_in_anonymous_modules_loads_top_level_constants
$counting_loaded_times = 0
with_autoloading_fixtures do with_autoloading_fixtures do
require_dependency '././counting_loader' # class_eval STRING pushes the class to the nesting of the eval'ed code.
assert_equal 1, $counting_loaded_times klass = Class.new.class_eval "E"
assert_raise(NameError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader } assert_equal E, klass
assert_equal 1, $counting_loaded_times
end end
end end
def test_const_missing_within_anonymous_module def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object
$counting_loaded_times = 0
m = Module.new
m.module_eval "def a() CountingLoader; end"
extend m
with_autoloading_fixtures do with_autoloading_fixtures do
kls = nil require_dependency 'e'
assert_nothing_raised { kls = a }
assert_equal "CountingLoader", kls.name
assert_equal 1, $counting_loaded_times
assert_nothing_raised { kls = a } mod = Module.new
assert_equal 1, $counting_loaded_times msg = 'E cannot be autoloaded from an anonymous class or module'
assert_raise(NameError, msg) { mod::E }
end end
end end