diff options
| -rw-r--r-- | NEWS.md | 10 | ||||
| -rw-r--r-- | load.c | 77 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/autoload_relative_spec.rb | 114 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/fixtures/autoload_relative_b.rb | 7 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/fixtures/autoload_relative_d.rb | 5 | ||||
| -rw-r--r-- | spec/ruby/core/module/autoload_relative_spec.rb | 128 | ||||
| -rw-r--r-- | spec/ruby/core/module/fixtures/autoload_relative_a.rb | 9 | ||||
| -rw-r--r-- | test/ruby/test_autoload.rb | 89 |
8 files changed, 439 insertions, 0 deletions
@@ -11,6 +11,15 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Kernel + + * `Kernel#autoload_relative` and `Module#autoload_relative` are added. + These methods work like `autoload`, but resolve the file path relative + to the file where the method is called, similar to `require_relative`. + This makes it easier to autoload constants from files in the same + directory without hardcoding absolute paths or manipulating `$LOAD_PATH`. + [[Feature #15330]] + * Method * `Method#source_location`, `Proc#source_location`, and @@ -92,5 +101,6 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT [Feature #6012]: https://bugs.ruby-lang.org/issues/6012 +[Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21785]: https://bugs.ruby-lang.org/issues/21785 @@ -1538,6 +1538,49 @@ rb_mod_autoload(VALUE mod, VALUE sym, VALUE file) /* * call-seq: + * mod.autoload_relative(const, filename) -> nil + * + * Registers _filename_ to be loaded (using Kernel::require) + * the first time that _const_ (which may be a String or + * a symbol) is accessed in the namespace of _mod_. The _filename_ + * is interpreted as relative to the directory of the file where + * autoload_relative is called. + * + * module A + * end + * A.autoload_relative(:B, "b.rb") + * + * If _const_ in _mod_ is defined as autoload, the file name to be + * loaded is replaced with _filename_. If _const_ is defined but not + * as autoload, does nothing. + * + * The relative path is converted to an absolute path, which is what + * will be returned by Module#autoload? for the constant. + * + * Raises LoadError if called without file context (e.g., from eval). + */ + +static VALUE +rb_mod_autoload_relative(VALUE mod, VALUE sym, VALUE file) +{ + ID id = rb_to_id(sym); + VALUE base, absolute_path; + + FilePathValue(file); + + base = rb_current_realfilepath(); + if (NIL_P(base)) { + rb_loaderror("cannot infer basepath (autoload_relative called without file context)"); + } + base = rb_file_dirname(base); + absolute_path = rb_file_absolute_path(file, base); + + rb_autoload_str(mod, id, absolute_path); + return Qnil; +} + +/* + * call-seq: * mod.autoload?(name, inherit=true) -> String or nil * * Returns _filename_ to be loaded if _name_ is registered as @@ -1605,6 +1648,38 @@ rb_f_autoload(VALUE obj, VALUE sym, VALUE file) /* * call-seq: + * autoload_relative(const, filename) -> nil + * + * Registers _filename_ to be loaded (using Kernel::require) + * the first time that _const_ (which may be a String or + * a symbol) is accessed. The _filename_ is interpreted as + * relative to the directory of the file where autoload_relative + * is called. + * + * autoload_relative(:MyModule, "my_module.rb") + * + * If _const_ is defined as autoload, the file name to be loaded is + * replaced with _filename_. If _const_ is defined but not as + * autoload, does nothing. + * + * The relative path is converted to an absolute path, which is what + * will be returned by Kernel#autoload? for the constant. + * + * Raises LoadError if called without file context (e.g., from eval). + */ + +static VALUE +rb_f_autoload_relative(VALUE obj, VALUE sym, VALUE file) +{ + VALUE klass = rb_class_real(rb_vm_cbase()); + if (!klass) { + rb_raise(rb_eTypeError, "Can not set autoload on singleton class"); + } + return rb_mod_autoload_relative(klass, sym, file); +} + +/* + * call-seq: * autoload?(name, inherit=true) -> String or nil * * Returns _filename_ to be loaded if _name_ is registered as @@ -1689,7 +1764,9 @@ Init_load(void) rb_define_global_function("require", rb_f_require, 1); rb_define_global_function("require_relative", rb_f_require_relative, 1); rb_define_method(rb_cModule, "autoload", rb_mod_autoload, 2); + rb_define_method(rb_cModule, "autoload_relative", rb_mod_autoload_relative, 2); rb_define_method(rb_cModule, "autoload?", rb_mod_autoload_p, -1); rb_define_global_function("autoload", rb_f_autoload, 2); + rb_define_global_function("autoload_relative", rb_f_autoload_relative, 2); rb_define_global_function("autoload?", rb_f_autoload_p, -1); } diff --git a/spec/ruby/core/kernel/autoload_relative_spec.rb b/spec/ruby/core/kernel/autoload_relative_spec.rb new file mode 100644 index 0000000000..c21e7803a4 --- /dev/null +++ b/spec/ruby/core/kernel/autoload_relative_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Specs for Kernel#autoload_relative + +ruby_version_is "4.1" do + describe "Kernel#autoload_relative" do + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + # Clean up constants defined by these tests + [:KSAutoloadRelativeA, :KSAutoloadRelativeB, :KSAutoloadRelativeC, + :KSAutoloadRelativeE, :KSAutoloadRelativeF, :KSAutoloadRelativeG, + :KSAutoloadRelativeH, :KSAutoloadRelativeI].each do |const| + KernelSpecs.send(:remove_const, const) if KernelSpecs.const_defined?(const, false) + end + [:KSAutoloadRelativeD, :NestedTest].each do |const| + Object.send(:remove_const, const) if Object.const_defined?(const, false) + end + end + + it "is a private method" do + Kernel.should have_private_instance_method(:autoload_relative) + end + + it "registers a file to load relative to the current file" do + KernelSpecs.autoload_relative :KSAutoloadRelativeA, "fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeA) + path.should_not be_nil + path.should.end_with?("autoload_relative_b.rb") + File.exist?(path).should be_true + end + + it "loads the file when the constant is accessed" do + KernelSpecs.autoload_relative :KSAutoloadRelativeB, "fixtures/autoload_relative_b.rb" + KernelSpecs::KSAutoloadRelativeB.loaded.should == :ksautoload_b + end + + it "sets the autoload constant in the constant table" do + KernelSpecs.autoload_relative :KSAutoloadRelativeC, "fixtures/autoload_relative_b.rb" + KernelSpecs.should have_constant(:KSAutoloadRelativeC) + end + + it "can autoload in instance_eval with a file context" do + result = Object.new.instance_eval(<<-CODE, __FILE__, __LINE__) + autoload_relative :KSAutoloadRelativeD, "fixtures/autoload_relative_d.rb" + KSAutoloadRelativeD.loaded + CODE + result.should == :ksautoload_d + end + + it "raises LoadError if called from eval without file context" do + -> { + eval('autoload_relative :Foo, "foo.rb"') + }.should raise_error(LoadError, /autoload_relative called without file context/) + end + + it "accepts both string and symbol for constant name" do + KernelSpecs.autoload_relative :KSAutoloadRelativeE, "fixtures/autoload_relative_b.rb" + KernelSpecs.autoload_relative "KSAutoloadRelativeF", "fixtures/autoload_relative_b.rb" + + KernelSpecs.should have_constant(:KSAutoloadRelativeE) + KernelSpecs.should have_constant(:KSAutoloadRelativeF) + end + + it "returns nil" do + KernelSpecs.autoload_relative(:KSAutoloadRelativeG, "fixtures/autoload_relative_b.rb").should be_nil + end + + it "resolves nested directory paths correctly" do + -> { + autoload_relative :NestedTest, "../kernel/fixtures/autoload_relative_b.rb" + autoload?(:NestedTest) + }.should_not raise_error + end + + it "resolves paths starting with ./" do + KernelSpecs.autoload_relative :KSAutoloadRelativeH, "./fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeH) + path.should_not be_nil + path.should.end_with?("autoload_relative_b.rb") + end + + it "ignores $LOAD_PATH and uses only relative path resolution" do + original_load_path = $LOAD_PATH.dup + $LOAD_PATH.clear + begin + KernelSpecs.autoload_relative :KSAutoloadRelativeI, "fixtures/autoload_relative_b.rb" + path = KernelSpecs.autoload?(:KSAutoloadRelativeI) + path.should_not be_nil + # Should still resolve even with empty $LOAD_PATH + File.exist?(path).should be_true + ensure + $LOAD_PATH.replace(original_load_path) + end + end + + describe "when Object is frozen" do + it "raises a FrozenError before defining the constant" do + ruby_exe(<<-RUBY).should include("FrozenError") + Object.freeze + begin + autoload_relative :Foo, "autoload_b.rb" + rescue => e + puts e.class + end + RUBY + end + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb new file mode 100644 index 0000000000..6de6f5091d --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb @@ -0,0 +1,7 @@ +module KernelSpecs + module KSAutoloadRelativeB + def self.loaded + :ksautoload_b + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb new file mode 100644 index 0000000000..5b6b5e1fa2 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb @@ -0,0 +1,5 @@ +module KSAutoloadRelativeD + def self.loaded + :ksautoload_d + end +end diff --git a/spec/ruby/core/module/autoload_relative_spec.rb b/spec/ruby/core/module/autoload_relative_spec.rb new file mode 100644 index 0000000000..ecbab9b4d5 --- /dev/null +++ b/spec/ruby/core/module/autoload_relative_spec.rb @@ -0,0 +1,128 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Specs for Module#autoload_relative +module ModuleSpecs + module AutoloadRelative + # Will be used for testing + end +end + +ruby_version_is "4.1" do + describe "Module#autoload_relative" do + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + end + + it "is a public method" do + Module.should have_public_instance_method(:autoload_relative, false) + end + + it "registers a file to load relative to the current file the first time the named constant is accessed" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeA, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:AutoloadRelativeA) + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + File.exist?(path).should be_true + end + + it "loads the registered file when the constant is accessed" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeB, "fixtures/autoload_relative_a.rb" + ModuleSpecs::Autoload::AutoloadRelativeB.should be_kind_of(Module) + end + + it "returns nil" do + ModuleSpecs::Autoload.autoload_relative(:AutoloadRelativeC, "fixtures/autoload_relative_a.rb").should be_nil + end + + it "registers a file to load the first time the named constant is accessed" do + module ModuleSpecs::Autoload::AutoloadRelativeTest + autoload_relative :D, "fixtures/autoload_relative_a.rb" + end + path = ModuleSpecs::Autoload::AutoloadRelativeTest.autoload?(:D) + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + end + + it "sets the autoload constant in the constants table" do + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTableTest, "fixtures/autoload_relative_a.rb" + ModuleSpecs::Autoload.should have_constant(:AutoloadRelativeTableTest) + end + + it "calls #to_path on non-String filenames" do + name = mock("autoload_relative mock") + name.should_receive(:to_path).and_return("fixtures/autoload_relative_a.rb") + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToPath, name + ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToPath).should_not be_nil + end + + it "calls #to_str on non-String filenames" do + name = mock("autoload_relative mock") + name.should_receive(:to_str).and_return("fixtures/autoload_relative_a.rb") + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToStr, name + ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToStr).should_not be_nil + end + + it "raises a TypeError if the filename argument is not a String or pathname" do + -> { + ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTypError, nil + }.should raise_error(TypeError) + end + + it "raises a NameError if the constant name is not valid" do + -> { + ModuleSpecs::Autoload.autoload_relative :invalid_name, "fixtures/autoload_relative_a.rb" + }.should raise_error(NameError) + end + + it "raises an ArgumentError if the constant name starts with a lowercase letter" do + -> { + ModuleSpecs::Autoload.autoload_relative :autoload, "fixtures/autoload_relative_a.rb" + }.should raise_error(NameError) + end + + it "raises LoadError if called from eval without file context" do + -> { + ModuleSpecs::Autoload.module_eval('autoload_relative :EvalTest, "fixtures/autoload_relative_a.rb"') + }.should raise_error(LoadError, /autoload_relative called without file context/) + end + + it "can autoload in instance_eval with a file context" do + path = nil + ModuleSpecs::Autoload.instance_eval(<<-CODE, __FILE__, __LINE__) + autoload_relative :InstanceEvalTest, "fixtures/autoload_relative_a.rb" + path = autoload?(:InstanceEvalTest) + CODE + path.should_not be_nil + path.should.end_with?("autoload_relative_a.rb") + end + + it "resolves paths relative to the file where it's called" do + # Using fixtures/autoload_relative_a.rb which exists + ModuleSpecs::Autoload.autoload_relative :RelativePathTest, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:RelativePathTest) + path.should.include?("fixtures") + path.should.end_with?("autoload_relative_a.rb") + end + + it "can load nested directory paths" do + ModuleSpecs::Autoload.autoload_relative :NestedPath, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:NestedPath) + path.should_not be_nil + File.exist?(path).should be_true + end + + describe "interoperability with autoload?" do + it "returns the absolute path with autoload?" do + ModuleSpecs::Autoload.autoload_relative :QueryTest, "fixtures/autoload_relative_a.rb" + path = ModuleSpecs::Autoload.autoload?(:QueryTest) + # Should be an absolute path + Pathname.new(path).absolute?.should be_true + end + end +end +end diff --git a/spec/ruby/core/module/fixtures/autoload_relative_a.rb b/spec/ruby/core/module/fixtures/autoload_relative_a.rb new file mode 100644 index 0000000000..494181adc2 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_relative_a.rb @@ -0,0 +1,9 @@ +module ModuleSpecs + module Autoload + class AutoloadRelativeA + end + + class AutoloadRelativeB + end + end +end diff --git a/test/ruby/test_autoload.rb b/test/ruby/test_autoload.rb index 82bf2d9d2c..de08be96e4 100644 --- a/test/ruby/test_autoload.rb +++ b/test/ruby/test_autoload.rb @@ -614,6 +614,95 @@ p Foo::Bar end end + def test_autoload_relative_toplevel + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main.rb') + module_file = File.join(tmpdir, 'test_module.rb') + + File.write(module_file, <<-RUBY) + module AutoloadRelativeTest + VERSION = '1.0' + end + RUBY + + File.write(main_file, <<-RUBY) + autoload_relative :AutoloadRelativeTest, 'test_module.rb' + puts AutoloadRelativeTest::VERSION + RUBY + + assert_in_out_err([main_file], '', ['1.0'], []) + end + end + + def test_autoload_relative_module_level + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'main_mod.rb') + module_file = File.join(tmpdir, 'nested_module.rb') + + File.write(module_file, <<-RUBY) + module Container + module NestedModule + MSG = 'loaded' + end + end + RUBY + + File.write(main_file, <<-RUBY) + module Container + autoload_relative :NestedModule, 'nested_module.rb' + end + puts Container::NestedModule::MSG + RUBY + + assert_in_out_err([main_file], '', ['loaded'], []) + end + end + + def test_autoload_relative_query + Dir.mktmpdir('autoload_relative') do |tmpdir| + main_file = File.join(tmpdir, 'query_test.rb') + module_file = File.join(tmpdir, 'query_module.rb') + + File.write(module_file, 'module QueryModule; end') + + File.write(main_file, <<-RUBY) + autoload_relative :QueryModule, 'query_module.rb' + path = autoload?(:QueryModule) + # Use realpath for comparison to handle symlinks (e.g., /var -> /private/var on macOS) + real_tmpdir = File.realpath('#{tmpdir}') + puts path.start_with?(real_tmpdir) && path.end_with?('query_module.rb') + RUBY + + assert_in_out_err([main_file], '', ['true'], []) + end + end + + def test_autoload_relative_nested_directory + Dir.mktmpdir('autoload_relative') do |tmpdir| + nested_dir = File.join(tmpdir, 'nested') + Dir.mkdir(nested_dir) + + main_file = File.join(tmpdir, 'nested_test.rb') + module_file = File.join(nested_dir, 'deep_module.rb') + + File.write(module_file, 'module DeepModule; VALUE = 42; end') + + File.write(main_file, <<-RUBY) + autoload_relative :DeepModule, 'nested/deep_module.rb' + puts DeepModule::VALUE + RUBY + + assert_in_out_err([main_file], '', ['42'], []) + end + end + + def test_autoload_relative_no_basepath + # Test that autoload_relative raises an error when called from eval without file context + assert_raise(LoadError) do + eval('autoload_relative :TestConst, "test.rb"') + end + end + private def assert_separately(*args, **kwargs) |
