Remove autoload for constant if the autoload fails

Previously, if an autoload failed (the file was loaded, but the
constant was not defined by the autoloaded file). Ruby will try
to autoload again if you delete the autoloaded file from
$LOADED_FEATURES.  With this change, the autoload and the
constant itself are removed as soon as it fails.

To handle cases where multiple threads are autoloading, when
deleting an autoload, handle the case where another thread
already deleted it.

Fixes [Bug #15790]
This commit is contained in:
Jeremy Evans 2021-10-08 12:54:26 -09:00 committed by GitHub
parent ded5a66cb9
commit 08759edea8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
Notes: git 2021-10-09 06:54:50 +09:00
Merged: https://github.com/ruby/ruby/pull/4715

Merged-By: jeremyevans <code@jeremyevans.net>
4 changed files with 121 additions and 47 deletions

View File

@ -441,21 +441,42 @@ describe "Module#autoload" do
ScratchPad.recorded.should == [:raise, :raise] ScratchPad.recorded.should == [:raise, :raise]
end end
it "does not remove the constant from Module#constants if the loaded file does not define it, but leaves it as 'undefined'" do ruby_version_is "3.1" do
path = fixture(__FILE__, "autoload_o.rb") it "removes the constant from Module#constants if the loaded file does not define it" do
ScratchPad.record [] path = fixture(__FILE__, "autoload_o.rb")
ModuleSpecs::Autoload.autoload :O, path ScratchPad.record []
ModuleSpecs::Autoload.autoload :O, path
ModuleSpecs::Autoload.const_defined?(:O).should == true ModuleSpecs::Autoload.const_defined?(:O).should == true
ModuleSpecs::Autoload.should have_constant(:O) ModuleSpecs::Autoload.should have_constant(:O)
ModuleSpecs::Autoload.autoload?(:O).should == path ModuleSpecs::Autoload.autoload?(:O).should == path
-> { ModuleSpecs::Autoload::O }.should raise_error(NameError) -> { ModuleSpecs::Autoload::O }.should raise_error(NameError)
ModuleSpecs::Autoload.should have_constant(:O) ModuleSpecs::Autoload.const_defined?(:O).should == false
ModuleSpecs::Autoload.const_defined?(:O).should == false ModuleSpecs::Autoload.should_not have_constant(:O)
ModuleSpecs::Autoload.autoload?(:O).should == nil ModuleSpecs::Autoload.autoload?(:O).should == nil
-> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError) -> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError)
end
end
ruby_version_is ""..."3.1" do
it "does not remove the constant from Module#constants if the loaded file does not define it, but leaves it as 'undefined'" do
path = fixture(__FILE__, "autoload_o.rb")
ScratchPad.record []
ModuleSpecs::Autoload.autoload :O, path
ModuleSpecs::Autoload.const_defined?(:O).should == true
ModuleSpecs::Autoload.should have_constant(:O)
ModuleSpecs::Autoload.autoload?(:O).should == path
-> { ModuleSpecs::Autoload::O }.should raise_error(NameError)
ModuleSpecs::Autoload.const_defined?(:O).should == false
ModuleSpecs::Autoload.should have_constant(:O)
ModuleSpecs::Autoload.autoload?(:O).should == nil
-> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError)
end
end end
it "does not try to load the file again if the loaded file did not define the constant" do it "does not try to load the file again if the loaded file did not define the constant" do
@ -554,31 +575,54 @@ describe "Module#autoload" do
# Basically, the parent autoload constant remains in a "undefined" state # Basically, the parent autoload constant remains in a "undefined" state
self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil
const_defined?(:DeclaredInParentDefinedInCurrent).should == false const_defined?(:DeclaredInParentDefinedInCurrent).should == false
self.should have_constant(:DeclaredInParentDefinedInCurrent)
-> { DeclaredInParentDefinedInCurrent }.should raise_error(NameError) -> { DeclaredInParentDefinedInCurrent }.should raise_error(NameError)
ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent) ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent)
end end
end end
it "and fails when finding the undefined autoload constant in the current scope when declared in current and defined in parent" do ruby_version_is "3.1" do
@remove << :DeclaredInCurrentDefinedInParent it "looks up in parent scope after failed autoload" do
module ModuleSpecs::Autoload @remove << :DeclaredInCurrentDefinedInParent
ScratchPad.record -> { module ModuleSpecs::Autoload
DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent ScratchPad.record -> {
} DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent
}
class LexicalScope class LexicalScope
autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb") autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
-> { DeclaredInCurrentDefinedInParent }.should raise_error(NameError) -> { DeclaredInCurrentDefinedInParent }.should_not raise_error(NameError)
# Basically, the autoload constant remains in a "undefined" state # Basically, the autoload constant remains in a "undefined" state
self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
const_defined?(:DeclaredInCurrentDefinedInParent).should == false const_defined?(:DeclaredInCurrentDefinedInParent).should == false
self.should have_constant(:DeclaredInCurrentDefinedInParent) -> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError)
-> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError) end
DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent
end end
end
end
DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent ruby_version_is ""..."3.1" do
it "and fails when finding the undefined autoload constant in the current scope when declared in current and defined in parent" do
@remove << :DeclaredInCurrentDefinedInParent
module ModuleSpecs::Autoload
ScratchPad.record -> {
DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent
}
class LexicalScope
autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
-> { DeclaredInCurrentDefinedInParent }.should raise_error(NameError)
# Basically, the autoload constant remains in a "undefined" state
self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
const_defined?(:DeclaredInCurrentDefinedInParent).should == false
self.should have_constant(:DeclaredInCurrentDefinedInParent)
-> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError)
end
DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent
end
end end
end end

View File

@ -101,7 +101,7 @@ describe "Module#const_set" do
mod.const_get(:Foo).should == 1 mod.const_get(:Foo).should == 1
end end
it "does not warn if the previous value was undefined" do it "does not warn after a failed autoload" do
path = fixture(__FILE__, "autoload_o.rb") path = fixture(__FILE__, "autoload_o.rb")
ScratchPad.record [] ScratchPad.record []
mod = Module.new mod = Module.new
@ -109,7 +109,6 @@ describe "Module#const_set" do
mod.autoload :Foo, path mod.autoload :Foo, path
-> { mod::Foo }.should raise_error(NameError) -> { mod::Foo }.should raise_error(NameError)
mod.should have_constant(:Foo)
mod.const_defined?(:Foo).should == false mod.const_defined?(:Foo).should == false
mod.autoload?(:Foo).should == nil mod.autoload?(:Foo).should == nil

View File

@ -456,6 +456,31 @@ p Foo::Bar
end; end;
end end
def test_autoload_after_failed_and_removed_from_loaded_features
Dir.mktmpdir('autoload') do |tmpdir|
autoload_path = File.join(tmpdir, "test-bug-15790.rb")
File.write(autoload_path, '')
assert_separately(%W[-I #{tmpdir}], <<-RUBY)
path = #{File.realpath(autoload_path).inspect}
autoload :X, path
assert_equal(path, Object.autoload?(:X))
assert_raise(NameError){X}
assert_nil(Object.autoload?(:X))
assert_equal(false, Object.const_defined?(:X))
$LOADED_FEATURES.delete(path)
assert_equal(false, Object.const_defined?(:X))
assert_nil(Object.autoload?(:X))
assert_raise(NameError){X}
assert_equal(false, Object.const_defined?(:X))
assert_nil(Object.autoload?(:X))
RUBY
end
end
def add_autoload(path) def add_autoload(path)
(@autoload_paths ||= []) << path (@autoload_paths ||= []) << path
::Object.class_eval {autoload(:AutoloadTest, path)} ::Object.class_eval {autoload(:AutoloadTest, path)}
@ -463,7 +488,7 @@ p Foo::Bar
def remove_autoload_constant def remove_autoload_constant
$".replace($" - @autoload_paths) $".replace($" - @autoload_paths)
::Object.class_eval {remove_const(:AutoloadTest)} ::Object.class_eval {remove_const(:AutoloadTest)} if defined? Object::AutoloadTest
TestAutoload.class_eval {remove_const(:AutoloadTest)} if defined? TestAutoload::AutoloadTest TestAutoload.class_eval {remove_const(:AutoloadTest)} if defined? TestAutoload::AutoloadTest
end end
end end

View File

@ -2237,23 +2237,26 @@ autoload_delete(VALUE mod, ID id)
struct autoload_const *ac; struct autoload_const *ac;
st_delete(tbl, &n, &load); st_delete(tbl, &n, &load);
ele = get_autoload_data((VALUE)load, &ac); /* Qfalse can indicate already deleted */
VM_ASSERT(ele); if (load != Qfalse) {
if (ele) { ele = get_autoload_data((VALUE)load, &ac);
VM_ASSERT(!list_empty(&ele->constants)); VM_ASSERT(ele);
} if (ele) {
VM_ASSERT(!list_empty(&ele->constants));
}
/* /*
* we must delete here to avoid "already initialized" warnings * we must delete here to avoid "already initialized" warnings
* with parallel autoload. Using list_del_init here so list_del * with parallel autoload. Using list_del_init here so list_del
* works in autoload_c_free * works in autoload_c_free
*/ */
list_del_init(&ac->cnode); list_del_init(&ac->cnode);
if (tbl->num_entries == 0) { if (tbl->num_entries == 0) {
n = autoload; n = autoload;
st_delete(RCLASS_IV_TBL(mod), &n, &val); st_delete(RCLASS_IV_TBL(mod), &n, &val);
} }
}
} }
} }
@ -2502,7 +2505,10 @@ rb_autoload_load(VALUE mod, ID id)
result = rb_ensure(autoload_require, (VALUE)&state, result = rb_ensure(autoload_require, (VALUE)&state,
autoload_reset, (VALUE)&state); autoload_reset, (VALUE)&state);
if (flag > 0 && (ce = rb_const_lookup(mod, id))) { if (!(ce = rb_const_lookup(mod, id)) || ce->value == Qundef) {
rb_const_remove(mod, id);
}
else if (flag > 0) {
ce->flag |= flag; ce->flag |= flag;
} }
RB_GC_GUARD(load); RB_GC_GUARD(load);