diff --git a/class.c b/class.c index da22fd3583006a..f3ea869573a6ca 100644 --- a/class.c +++ b/class.c @@ -1843,6 +1843,10 @@ ensure_origin(VALUE klass) { VALUE origin = RCLASS_ORIGIN(klass); if (origin == klass) { + /* Create the box-local classext before reading m_tbl, so that the + * origin shares the m_tbl with the box-local iclasses of klass, + * as rb_prepend_module relies on that identity. */ + rb_class_ensure_writable(klass); origin = class_alloc(T_ICLASS, klass); RCLASS_SET_M_TBL(origin, RCLASS_M_TBL(klass)); rb_class_set_super(origin, RCLASS_SUPER(klass)); diff --git a/lib/timeout.gemspec b/lib/timeout.gemspec index 5449494e071b2f..03e71ee430afef 100644 --- a/lib/timeout.gemspec +++ b/lib/timeout.gemspec @@ -26,7 +26,7 @@ Gem::Specification.new do |spec| spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f| - (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)}) + (f == File.basename(__FILE__)) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)}) end end spec.require_paths = ["lib"] diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 9ee932b442a632..43f13494de3782 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -203,6 +203,58 @@ def test_autoload_in_box assert_raise(NameError) { BOX_B } end + def test_autoload_dispatches_prepended_require + # Autoload must go through the `Kernel#require` decorations (Zeitwerk, RubyGems, etc.) + # of the box that registered the autoload, not `Ruby::Box#require` directly. + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + FEATURE = "/nonexistent/virtual_feature" + module Decor + def require(path) + if path == FEATURE + Object.const_set(:AutoloadedFromDecorator, Module.new) + return true + end + super + end + end + Kernel.prepend(Decor) + Object.autoload(:AutoloadedFromDecorator, FEATURE) + assert_kind_of Module, AutoloadedFromDecorator + end; + end + + def test_autoload_dispatches_prepended_require_of_the_registered_box + # Even when the autoload is triggered from outside, it must be dispatched to the + # (decorated) `Kernel#require` of the box that registered the autoload. + # --enable=gems because Kernel.prepend in a box without RubyGems has a separate + # ancestry ordering problem, and assert_separately runs with --disable=gems. + assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + box = Ruby::Box.new + box.eval(<<~RUBY) + FEATURE = "/nonexistent/box_virtual_feature" + module BoxDecor + def require(path) + if path == FEATURE + Holder.const_set(:Virtual, "decorated in \#{Ruby::Box.current.inspect}") + return true + end + super + end + end + Kernel.prepend(BoxDecor) + module Holder + autoload :Virtual, FEATURE + end + RUBY + puts box::Holder::Virtual + end; + assert_equal 1, output.size + assert_match(/\Adecorated in #autoload_const; @@ -3176,9 +3182,6 @@ autoload_feature_require(VALUE _arguments) // We save this for later use in autoload_apply_constants: arguments->autoload_data = rb_check_typeddata(autoload_const->autoload_data_value, &autoload_data_type); - if (rb_box_available() && BOX_OBJ_P(autoload_box_value)) - receiver = autoload_box_value; - /* * Clear the global cc cache table because the require method can be different from the current * box's one and it may cause inconsistent cc-cme states. @@ -3187,7 +3190,25 @@ autoload_feature_require(VALUE _arguments) */ rb_gccct_clear_table(); - VALUE result = rb_funcall(receiver, rb_intern("require"), 1, arguments->autoload_data->feature); + VALUE feature = arguments->autoload_data->feature; + rb_box_t *box = NULL; + if (rb_box_available() && BOX_OBJ_P(autoload_box_value)) { + box = rb_get_box_t(autoload_box_value); + } + + VALUE result; + if (box && box->top_self) { + /* + * Call `require` on the top self of the box that registered the autoload, in a frame + * running in that box, so that `Kernel#require` decorations in the box (RubyGems, + * Zeitwerk, etc.) are dispatched and the feature is loaded into that box. + */ + result = rb_vm_call_cfunc_in_box(box->top_self, autoload_feature_require_in_box, + box->top_self, feature, feature, box); + } + else { + result = rb_funcall(rb_vm_top_self(), rb_intern("require"), 1, feature); + } if (RTEST(result)) { return rb_mutex_synchronize(autoload_mutex, autoload_apply_constants, _arguments);