mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
[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:
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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in new issue
Block a user