summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md17
-rw-r--r--class.c35
-rw-r--r--include/ruby/internal/intern/class.h13
-rw-r--r--object.c1
-rw-r--r--spec/ruby/core/class/descendants_spec.rb38
-rw-r--r--test/ruby/test_class.rb18
6 files changed, 122 insertions, 0 deletions
diff --git a/NEWS.md b/NEWS.md
index f2c13859fa..0e8a5fc163 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -96,6 +96,22 @@ Outstanding ones only.
* Array#intersect? is added. [[Feature #15198]]
+* Class
+
+ * Class#descendants, which returns an array of classes
+ directly or indirectly inheriting from the receiver, not
+ including the receiver or singleton classes.
+ [[Feature #14394]]
+
+ ```ruby
+ class A; end
+ class B < A; end
+ class C < B; end
+ A.descendants #=> [B, C]
+ B.descendants #=> [C]
+ C.descendants #=> []
+ ```
+
* Enumerable
* Enumerable#compact is added. [[Feature #17312]]
@@ -358,6 +374,7 @@ See [the repository](https://github.com/ruby/error_highlight) in detail.
[Bug #4443]: https://bugs.ruby-lang.org/issues/4443
[Feature #12194]: https://bugs.ruby-lang.org/issues/12194
[Feature #14256]: https://bugs.ruby-lang.org/issues/14256
+[Feature #14394]: https://bugs.ruby-lang.org/issues/14394
[Feature #14579]: https://bugs.ruby-lang.org/issues/14579
[Feature #15198]: https://bugs.ruby-lang.org/issues/15198
[Feature #15211]: https://bugs.ruby-lang.org/issues/15211
diff --git a/class.c b/class.c
index 8b0bfb8387..6bf17aaa47 100644
--- a/class.c
+++ b/class.c
@@ -1335,6 +1335,41 @@ rb_mod_ancestors(VALUE mod)
}
static void
+class_descendants_recursive(VALUE klass, VALUE ary)
+{
+ if (BUILTIN_TYPE(klass) == T_CLASS && !FL_TEST(klass, FL_SINGLETON)) {
+ rb_ary_push(ary, klass);
+ }
+ rb_class_foreach_subclass(klass, class_descendants_recursive, ary);
+}
+
+/*
+ * call-seq:
+ * descendants -> array
+ *
+ * Returns an array of classes where the receiver is one of
+ * the ancestors of the class, excluding the receiver and
+ * singleton classes. The order of the returned array is not
+ * defined.
+ *
+ * class A; end
+ * class B < A; end
+ * class C < B; end
+ *
+ * A.descendants #=> [B, C]
+ * B.descendants #=> [C]
+ * C.descendants #=> []
+ */
+
+VALUE
+rb_class_descendants(VALUE klass)
+{
+ VALUE ary = rb_ary_new();
+ rb_class_foreach_subclass(klass, class_descendants_recursive, ary);
+ return ary;
+}
+
+static void
ins_methods_push(st_data_t name, st_data_t ary)
{
rb_ary_push((VALUE)ary, ID2SYM((ID)name));
diff --git a/include/ruby/internal/intern/class.h b/include/ruby/internal/intern/class.h
index 60baf98472..835e85c26d 100644
--- a/include/ruby/internal/intern/class.h
+++ b/include/ruby/internal/intern/class.h
@@ -175,6 +175,19 @@ VALUE rb_mod_include_p(VALUE child, VALUE parent);
VALUE rb_mod_ancestors(VALUE mod);
/**
+ * Queries the class's descendants. This routine gathers classes that are
+ * subclasses of the given class (or subclasses of those subclasses, etc.),
+ * returning an array of classes that have the given class as an ancestor.
+ * The returned array does not include the given class or singleton classes.
+ *
+ * @param[in] klass A class.
+ * @return An array of classes where `klass` is an ancestor.
+ *
+ * @internal
+ */
+VALUE rb_class_descendants(VALUE klass);
+
+/**
* Generates an array of symbols, which are the list of method names defined in
* the passed class.
*
diff --git a/object.c b/object.c
index 5eca02a08c..f98fb83936 100644
--- a/object.c
+++ b/object.c
@@ -4653,6 +4653,7 @@ InitVM_Object(void)
rb_define_method(rb_cClass, "new", rb_class_new_instance_pass_kw, -1);
rb_define_method(rb_cClass, "initialize", rb_class_initialize, -1);
rb_define_method(rb_cClass, "superclass", rb_class_superclass, 0);
+ rb_define_method(rb_cClass, "descendants", rb_class_descendants, 0); /* in class.c */
rb_define_alloc_func(rb_cClass, rb_class_s_alloc);
rb_undef_method(rb_cClass, "extend_object");
rb_undef_method(rb_cClass, "append_features");
diff --git a/spec/ruby/core/class/descendants_spec.rb b/spec/ruby/core/class/descendants_spec.rb
new file mode 100644
index 0000000000..f87cd68be8
--- /dev/null
+++ b/spec/ruby/core/class/descendants_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative '../module/fixtures/classes'
+
+ruby_version_is '3.1' do
+ describe "Class#descendants" do
+ it "returns a list of classes descended from self (excluding self)" do
+ assert_descendants(ModuleSpecs::Parent, [ModuleSpecs::Child, ModuleSpecs::Child2, ModuleSpecs::Grandchild])
+ end
+
+ it "does not return included modules" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ parent.include(mod)
+
+ assert_descendants(parent, [child])
+ end
+
+ it "does not return singleton classes" do
+ a = Class.new
+
+ a_obj = a.new
+ def a_obj.force_singleton_class
+ 42
+ end
+
+ a.descendants.should_not include(a_obj.singleton_class)
+ end
+
+ it "has 1 entry per module or class" do
+ ModuleSpecs::Parent.descendants.should == ModuleSpecs::Parent.descendants.uniq
+ end
+
+ def assert_descendants(mod, descendants)
+ mod.descendants.sort_by(&:inspect).should == descendants.sort_by(&:inspect)
+ end
+ end
+end
diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb
index 368c046261..96bca08601 100644
--- a/test/ruby/test_class.rb
+++ b/test/ruby/test_class.rb
@@ -737,4 +737,22 @@ class TestClass < Test::Unit::TestCase
c = Class.new.freeze
assert_same(c, Module.new.const_set(:Foo, c))
end
+
+ def test_descendants
+ c = Class.new
+ sc = Class.new(c)
+ ssc = Class.new(sc)
+ [c, sc, ssc].each do |k|
+ k.include Module.new
+ k.new.define_singleton_method(:force_singleton_class){}
+ end
+ assert_equal([sc, ssc], c.descendants)
+ assert_equal([ssc], sc.descendants)
+ assert_equal([], ssc.descendants)
+
+ object_descendants = Object.descendants
+ assert_include(object_descendants, c)
+ assert_include(object_descendants, sc)
+ assert_include(object_descendants, ssc)
+ end
end