[前][次][番号順一覧][スレッド一覧]

ruby-changes:69466

From: Jeremy <ko1@a...>
Date: Wed, 27 Oct 2021 04:35:35 +0900 (JST)
Subject: [ruby-changes:69466] 717ab0bb2e (master): Add Class#descendants

https://git.ruby-lang.org/ruby.git/commit/?id=717ab0bb2e

From 717ab0bb2ee63dfe76076e0c9f91fbac3a0de4fd Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@j...>
Date: Tue, 26 Oct 2021 10:35:21 -0900
Subject: Add Class#descendants

Doesn't include receiver or singleton classes.

Implements [Feature #14394]

Co-authored-by: fatkodima <fatkodima123@g...>
Co-authored-by: Benoit Daloze <eregontp@g...>
---
 NEWS.md                                  | 17 ++++++++++++++
 class.c                                  | 35 +++++++++++++++++++++++++++++
 include/ruby/internal/intern/class.h     | 13 +++++++++++
 object.c                                 |  1 +
 spec/ruby/core/class/descendants_spec.rb | 38 ++++++++++++++++++++++++++++++++
 test/ruby/test_class.rb                  | 18 +++++++++++++++
 6 files changed, 122 insertions(+)
 create mode 100644 spec/ruby/core/class/descendants_spec.rb

diff --git a/NEWS.md b/NEWS.md
index f2c13859fa0..0e8a5fc1635 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -96,6 +96,22 @@ Outstanding ones only. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L96
 
     * 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. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L374
 [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 8b0bfb83875..6bf17aaa477 100644
--- a/class.c
+++ b/class.c
@@ -1334,6 +1334,41 @@ rb_mod_ancestors(VALUE mod) https://github.com/ruby/ruby/blob/trunk/class.c#L1334
     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 60baf984727..835e85c26d2 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); https://github.com/ruby/ruby/blob/trunk/include/ruby/internal/intern/class.h#L174
  */
 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 5eca02a08c6..f98fb839366 100644
--- a/object.c
+++ b/object.c
@@ -4653,6 +4653,7 @@ InitVM_Object(void) https://github.com/ruby/ruby/blob/trunk/object.c#L4653
     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 00000000000..f87cd68be82
--- /dev/null
+++ b/spec/ruby/core/class/descendants_spec.rb
@@ -0,0 +1,38 @@ https://github.com/ruby/ruby/blob/trunk/spec/ruby/core/class/descendants_spec.rb#L1
+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 368c046261e..96bca08601c 100644
--- a/test/ruby/test_class.rb
+++ b/test/ruby/test_class.rb
@@ -737,4 +737,22 @@ class TestClass < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_class.rb#L737
     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
-- 
cgit v1.2.1


--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]