diff --git a/NEWS.md b/NEWS.md index ff0bf012992..fb926d4c747 100644 --- a/NEWS.md +++ b/NEWS.md @@ -66,6 +66,8 @@ Note: We're only listing outstanding class updates. * Module + * `Module#descendants` is added. It returns an array of classes and + modules that have the receiver in their ancestors. [[Feature #9779]] * `Module#ruby2_keywords` and top-level `ruby2_keywords` are deprecated and will be removed in Ruby 4.4. [[Feature #22205]] @@ -298,6 +300,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 +[Feature #9779]: https://bugs.ruby-lang.org/issues/9779 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #20163]: https://bugs.ruby-lang.org/issues/20163 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 diff --git a/class.c b/class.c index f591eb9a2e6..79817af97d5 100644 --- a/class.c +++ b/class.c @@ -1005,9 +1005,7 @@ rb_mod_init_copy(VALUE clone, VALUE orig) rb_class_set_super(prev_clone_p, clone_p); prev_clone_p = clone_p; RCLASS_SET_CONST_TBL(clone_p, RCLASS_CONST_TBL(p), false); - if (RB_TYPE_P(clone, T_CLASS)) { - RCLASS_SET_INCLUDER(clone_p, clone); - } + RCLASS_SET_INCLUDER(clone_p, clone); add_subclass = TRUE; if (p != RCLASS_ORIGIN(p)) { origin[0] = clone_p; @@ -2118,6 +2116,115 @@ rb_class_subclasses(VALUE klass) return class_descendants(klass, true); } +struct descendants_traverse_data +{ + VALUE buffer; + long count; + long maxcount; + st_table *visited; +}; + +static void module_descendants_recursive(VALUE entry, VALUE v); + +static void +module_descendants_add(VALUE klass, struct descendants_traverse_data *data) +{ + // skip entries beyond the estimation to keep the enumeration pass allocation-free + if (data->buffer && data->count >= data->maxcount) return; + + if (st_insert(data->visited, (st_data_t)klass, 1)) return; // already visited + + if (data->buffer) { + // assumes that this does not cause GC as long as the length does not exceed the capacity + rb_ary_push(data->buffer, klass); + } + data->count++; + rb_class_foreach_subclass(klass, module_descendants_recursive, (VALUE)data); +} + +static void +module_descendants_recursive(VALUE entry, VALUE v) +{ + struct descendants_traverse_data *data = (struct descendants_traverse_data *)v; + + if (rb_objspace_garbage_object_p(entry)) return; + + if (RB_TYPE_P(entry, T_ICLASS)) { + // resolve the ICLASS to the including class or module; + // refinement ICLASSes have no includer + VALUE includer = RCLASS_INCLUDER(entry); + while (includer && RB_TYPE_P(includer, T_ICLASS)) { + includer = RCLASS_INCLUDER(includer); + } + if (!includer || UNDEF_P(includer)) return; + if (rb_objspace_garbage_object_p(includer)) return; + if (RCLASS_SINGLETON_P(includer)) return; // e.g. Object#extend + module_descendants_add(includer, data); + } + else { + if (RCLASS_SINGLETON_P(entry)) return; + module_descendants_add(entry, data); + } +} + +/* + * call-seq: + * descendants -> array + * + * Returns an array of classes and modules that have the receiver in + * their ancestors. This is the inverse of Module#ancestors: + * +x.descendants.include?(y)+ holds if and only if + * +y.ancestors.include?(x)+ holds, except that the receiver itself, + * singleton classes, and refinements are never included. + * The order of the returned array is not defined. + * + * module A; end + * module B; include A; end + * class C; include B; end + * class D < C; end + * + * A.descendants #=> [B, C, D] + * B.descendants #=> [C, D] + * C.descendants #=> [D] + * + * Note that the receiver does not hold references to its descendants + * and doesn't prevent them from being garbage collected. This means + * that a descendant might disappear from the result when all + * references to it are dropped, depending on whether garbage + * collector was run. + */ + +VALUE +rb_mod_descendants(VALUE mod) +{ + struct descendants_traverse_data data = { Qfalse, 0, -1, NULL }; + + // estimate the count of descendants + data.visited = st_init_numtable(); + st_insert(data.visited, (st_data_t)mod, 1); // exclude the receiver + rb_class_foreach_subclass(mod, module_descendants_recursive, (VALUE)&data); + st_free_table(data.visited); + + // the following allocation may cause GC which may change the number of descendants + data.buffer = rb_ary_new_capa(data.count); + data.maxcount = data.count; + data.count = 0; + // pre-sized so that st_insert() does not cause GC during the enumeration + data.visited = st_init_numtable_with_size(data.maxcount + 1); + st_insert(data.visited, (st_data_t)mod, 1); // exclude the receiver + + size_t gc_count = rb_gc_count(); + + rb_class_foreach_subclass(mod, module_descendants_recursive, (VALUE)&data); + + if (gc_count != rb_gc_count()) { + rb_bug("GC must not occur during the subclass iteration of Module#descendants"); + } + st_free_table(data.visited); + + return data.buffer; +} + /* * call-seq: * attached_object -> object diff --git a/include/ruby/internal/intern/class.h b/include/ruby/internal/intern/class.h index 63a2bc09017..1e6822c3eaf 100644 --- a/include/ruby/internal/intern/class.h +++ b/include/ruby/internal/intern/class.h @@ -174,6 +174,18 @@ VALUE rb_mod_include_p(VALUE child, VALUE parent); */ VALUE rb_mod_ancestors(VALUE mod); +/** + * Queries the module's descendants. This routine gathers classes and modules + * that have the given module in their ancestors. The returned array includes + * neither the given module itself nor singleton classes. + * + * @param[in] mod A module or a class. + * @return An array of classes and modules whose `ancestors` include `mod`. + * + * @internal + */ +VALUE rb_mod_descendants(VALUE mod); + /** * Queries the class's direct descendants. This routine gathers classes that are * direct subclasses of the given class, diff --git a/object.c b/object.c index 2eb958fb151..bd53644a1c3 100644 --- a/object.c +++ b/object.c @@ -4632,6 +4632,7 @@ InitVM_Object(void) rb_define_method(rb_cModule, "name", rb_mod_name, 0); /* in variable.c */ rb_define_method(rb_cModule, "set_temporary_name", rb_mod_set_temporary_name, 1); /* in variable.c */ rb_define_method(rb_cModule, "ancestors", rb_mod_ancestors, 0); /* in class.c */ + rb_define_method(rb_cModule, "descendants", rb_mod_descendants, 0); /* in class.c */ rb_define_method(rb_cModule, "attr", rb_mod_attr, -1); rb_define_method(rb_cModule, "attr_reader", rb_mod_attr_reader, -1); diff --git a/spec/ruby/core/module/descendants_spec.rb b/spec/ruby/core/module/descendants_spec.rb new file mode 100644 index 00000000000..779660b933b --- /dev/null +++ b/spec/ruby/core/module/descendants_spec.rb @@ -0,0 +1,137 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "4.1" do + describe "Module#descendants" do + it "returns a list of classes inheriting from self" do + parent = Class.new + child = Class.new(parent) + grandchild = Class.new(child) + + assert_descendants(parent, [child, grandchild]) + assert_descendants(child, [grandchild]) + assert_descendants(grandchild, []) + end + + it "returns a list of modules and classes including self" do + mod = Module.new + including_mod = Module.new { include mod } + klass = Class.new { include including_mod } + subclass = Class.new(klass) + + assert_descendants(mod, [including_mod, klass, subclass]) + assert_descendants(including_mod, [klass, subclass]) + end + + it "returns modules and classes prepending self" do + mod = Module.new + prepending_mod = Module.new { prepend mod } + klass = Class.new { prepend mod } + + assert_descendants(mod, [prepending_mod, klass]) + end + + it "returns modules and classes including self after prepending another module" do + prepended = Module.new + mod = Module.new + klass = Class.new { prepend prepended; include mod } + including_mod = Module.new { prepend prepended; include mod } + + assert_descendants(mod, [klass, including_mod]) + end + + it "does not return the internal nodes created by prepend" do + parent = Class.new { prepend Module.new } + child = Class.new(parent) { prepend Module.new } + + assert_descendants(parent, [child]) + end + + it "does not return modules and classes including a clone of self" do + mod = Module.new + clone = mod.clone + klass = Class.new { include clone } + + mod.descendants.should_not.include?(klass) + clone.descendants.should.include?(klass) + end + + it "returns modules and classes into which self is included later" do + mod = Module.new + including_mod = Module.new + klass = Class.new { include including_mod } + including_mod.include(mod) + + assert_descendants(mod, [including_mod, klass]) + end + + it "does not return self" do + mod = Module.new + Class.new { include mod } + + mod.descendants.should_not.include?(mod) + ModuleSpecs::Parent.descendants.should_not.include?(ModuleSpecs::Parent) + end + + it "returns an empty Array if there is no descendant" do + Module.new.descendants.should == [] + Class.new.descendants.should == [] + end + + it "does not return singleton classes" do + mod = Module.new + obj = Object.new + obj.extend(mod) + klass = Class.new { extend mod } + + mod.descendants.should_not.include?(obj.singleton_class) + mod.descendants.should_not.include?(klass.singleton_class) + ModuleSpecs::Internal.descendants.should_not.include?(ModuleSpecs::Child.singleton_class) + end + + it "does not return refinements" do + klass = Class.new + refinement = nil + Module.new do + refinement = refine(klass) do + def refined_method; end + end + end + + klass.descendants.should_not.include?(refinement) + refinement.descendants.should_not.include?(klass) + end + + it "has 1 entry per module or class" do + mod = Module.new + including_mod = Module.new { include mod } + Class.new do + include mod + include including_mod + end + + descendants = mod.descendants + descendants.should == descendants.uniq + + descendants = ModuleSpecs::Parent.descendants + descendants.should == descendants.uniq + end + + it "returns the modules and classes whose ancestors include self" do + mods = [ModuleSpecs::Basic, ModuleSpecs::Super, ModuleSpecs::Parent, + ModuleSpecs::Child, ModuleSpecs::Child2, ModuleSpecs::Grandchild] + + mods.each do |mod| + descendants = mod.descendants + mods.each do |other| + next if mod.equal?(other) + descendants.include?(other).should == other.ancestors.include?(mod) + end + end + end + + def assert_descendants(mod, descendants) + mod.descendants.sort_by(&:object_id).should == descendants.sort_by(&:object_id) + end + end +end diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index ad83d098236..c011da4a5c5 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -221,6 +221,83 @@ class TestModule < Test::Unit::TestCase assert_equal([String, Comparable, Object, Kernel, BasicObject], String.ancestors - mixins) end + def test_descendants + a = Module.new + b = Module.new { include a } + c = Class.new { include b } + d = Class.new(c) + + order = [a, b, c, d] + assert_equal([b, c, d], a.descendants.sort_by {|m| order.index(m)}) + assert_equal([c, d], b.descendants.sort_by {|m| order.index(m)}) + assert_equal([d], c.descendants) + assert_equal([], d.descendants) + + # prepend + pre = Module.new + e = Class.new { prepend pre } + assert_include(pre.descendants, e) + + # diamond inclusion does not produce duplicates + f = Class.new { include a; include b } + descendants = a.descendants + assert_equal(descendants.uniq, descendants) + assert_include(descendants, f) + + # include into a module propagates to existing includers + g = Module.new + h = Module.new + i = Class.new { include h } + h.include(g) + assert_include(g.descendants, h) + assert_include(g.descendants, i) + + # singleton classes are excluded, even via Object#extend + obj = Object.new + obj.extend(a) + assert_not_include(a.descendants, obj.singleton_class) + + # duality with Module#ancestors, except that the receiver is excluded + all = [a, b, c, d, pre, e, f, g, h, i] + all.each do |x| + dx = x.descendants + assert_not_include(dx, x) + all.each do |y| + next if x == y + assert_equal(y.ancestors.include?(x), dx.include?(y), + "#{x.inspect} vs #{y.inspect}") + end + end + end + + def test_descendants_gc + a = Module.new + Class.new { include a } + Module.new { include a } + GC.start + descendants = a.descendants + assert_not_include(descendants, a) + # the anonymous descendants above may or may not have been collected + # already; what must hold is that no dead object is returned + descendants.each {|m| assert_kind_of(Module, m)} + end + + def test_descendants_refinement + assert_separately([], <<-"end;") + class Target; def t; end; end + module M; end + refinement = nil + Module.new do + refinement = refine(Target) do + def t2; end + end + end + assert_equal([], Target.descendants) + assert_not_include(M.descendants, Target) + assert_not_include(Target.descendants, refinement) + end; + end + CLASS_EVAL = 2 @@class_eval = 'b'