[Feature #9779] Add Module#descendants

Module#descendants returns an array of classes and modules that have
the receiver in their ancestors; it is the inverse of Module#ancestors.
The receiver itself, singleton classes, and refinements are not included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shugo MaedaandClaude Fable 5 authored and Shugo Maeda committed 2026-08-06 23:05:57 +09:00
1 parent 5d7bb1076d
commit 9658933451
6 files changed
+340 -3
Notes: git 2026-08-06 14:06:31 +00:00

No files matched your search

+3
View File
@@ -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
+110 -3
View File
@@ -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
+12
View File
@@ -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,
+1
View File
@@ -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);
+137
View File
@@ -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
+77
View File
@@ -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'