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 @@ -1334,6 +1334,41 @@ rb_mod_ancestors(VALUE mod) return ary; } +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) { 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 @@ -174,6 +174,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