summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md10
-rw-r--r--load.c77
-rw-r--r--spec/ruby/core/kernel/autoload_relative_spec.rb114
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_relative_b.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_relative_d.rb5
-rw-r--r--spec/ruby/core/module/autoload_relative_spec.rb128
-rw-r--r--spec/ruby/core/module/fixtures/autoload_relative_a.rb9
-rw-r--r--test/ruby/test_autoload.rb89
8 files changed, 439 insertions, 0 deletions
diff --git a/NEWS.md b/NEWS.md
index 47deb9c388..b09eadeb05 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -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
diff --git a/load.c b/load.c
index 391a6c1337..e33abeb0df 100644
--- a/load.c
+++ b/load.c
@@ -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)