summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean Boussier <jean.boussier@gmail.com>2021-10-28 14:07:11 +0200
committerJean Boussier <jean.boussier@gmail.com>2021-11-23 10:50:44 +0100
commitc0c2b31a35e19a47b499b57807bc0a0f9325f6d3 (patch)
treeab7ee0710597903b43a8d086a3805059693de928
parenta88b19d3d08447eeb7045621f02a844173d64203 (diff)
downloadruby-c0c2b31a35e19a47b499b57807bc0a0f9325f6d3.tar.gz
Add Class#subclasses
Implements [Feature #18273] Returns an array containing the receiver's direct subclasses without singleton classes.
-rw-r--r--NEWS.md16
-rw-r--r--class.c73
-rw-r--r--gems/bundled_gems2
-rw-r--r--include/ruby/internal/intern/class.h15
-rw-r--r--object.c1
-rw-r--r--spec/ruby/core/class/subclasses_spec.rb38
-rw-r--r--test/ruby/test_class.rb38
7 files changed, 163 insertions, 20 deletions
diff --git a/NEWS.md b/NEWS.md
index 3e8035714b..df58f5a457 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -121,6 +121,21 @@ Outstanding ones only.
C.descendants #=> []
```
+ * Class#subclasses, which returns an array of classes
+ directly inheriting from the receiver, not
+ including singleton classes.
+ [[Feature #18273]]
+
+ ```ruby
+ class A; end
+ class B < A; end
+ class C < B; end
+ class D < A; end
+ A.subclasses #=> [D, B]
+ B.subclasses #=> [C]
+ C.subclasses #=> []
+ ```
+
* Enumerable
* Enumerable#compact is added. [[Feature #17312]]
@@ -457,6 +472,7 @@ See [the repository](https://github.com/ruby/error_highlight) in detail.
[Feature #18172]: https://bugs.ruby-lang.org/issues/18172
[Feature #18229]: https://bugs.ruby-lang.org/issues/18229
[Feature #18290]: https://bugs.ruby-lang.org/issues/18290
+[Feature #18273]: https://bugs.ruby-lang.org/issues/18273
[GH-1509]: https://github.com/ruby/ruby/pull/1509
[GH-4815]: https://github.com/ruby/ruby/pull/4815
diff --git a/class.c b/class.c
index 52750aad79..6ad64a6efe 100644
--- a/class.c
+++ b/class.c
@@ -1377,6 +1377,7 @@ struct subclass_traverse_data
VALUE buffer;
long count;
long maxcount;
+ bool immediate_only;
};
static void
@@ -1390,8 +1391,38 @@ class_descendants_recursive(VALUE klass, VALUE v)
rb_ary_push(data->buffer, klass);
}
data->count++;
+ if (!data->immediate_only) {
+ rb_class_foreach_subclass(klass, class_descendants_recursive, v);
+ }
+ }
+ else {
+ rb_class_foreach_subclass(klass, class_descendants_recursive, v);
+ }
+}
+
+static VALUE
+class_descendants(VALUE klass, bool immediate_only)
+{
+ struct subclass_traverse_data data = { Qfalse, 0, -1, immediate_only };
+
+ // estimate the count of subclasses
+ rb_class_foreach_subclass(klass, class_descendants_recursive, (VALUE) &data);
+
+ // the following allocation may cause GC which may change the number of subclasses
+ data.buffer = rb_ary_new_capa(data.count);
+ data.maxcount = data.count;
+ data.count = 0;
+
+ size_t gc_count = rb_gc_count();
+
+ // enumerate subclasses
+ rb_class_foreach_subclass(klass, class_descendants_recursive, (VALUE) &data);
+
+ if (gc_count != rb_gc_count()) {
+ rb_bug("GC must not occur during the subclass iteration of Class#descendants");
}
- rb_class_foreach_subclass(klass, class_descendants_recursive, v);
+
+ return data.buffer;
}
/*
@@ -1415,26 +1446,32 @@ class_descendants_recursive(VALUE klass, VALUE v)
VALUE
rb_class_descendants(VALUE klass)
{
- struct subclass_traverse_data data = { Qfalse, 0, -1 };
-
- // estimate the count of subclasses
- rb_class_foreach_subclass(klass, class_descendants_recursive, (VALUE) &data);
-
- // the following allocation may cause GC which may change the number of subclasses
- data.buffer = rb_ary_new_capa(data.count);
- data.maxcount = data.count;
- data.count = 0;
-
- size_t gc_count = rb_gc_count();
+ return class_descendants(klass, false);
+}
- // enumerate subclasses
- rb_class_foreach_subclass(klass, class_descendants_recursive, (VALUE) &data);
- if (gc_count != rb_gc_count()) {
- rb_bug("GC must not occur during the subclass iteration of Class#descendants");
- }
+/*
+ * call-seq:
+ * subclasses -> array
+ *
+ * Returns an array of classes where the receiver is the
+ * direct superclass of the class, excluding singleton classes.
+ * The order of the returned array is not defined.
+ *
+ * class A; end
+ * class B < A; end
+ * class C < B; end
+ * class D < A; end
+ *
+ * A.subclasses #=> [D, B]
+ * B.subclasses #=> [C]
+ * C.subclasses #=> []
+ */
- return data.buffer;
+VALUE
+rb_class_subclasses(VALUE klass)
+{
+ return class_descendants(klass, true);
}
static void
diff --git a/gems/bundled_gems b/gems/bundled_gems
index a947b14ca0..53138037d8 100644
--- a/gems/bundled_gems
+++ b/gems/bundled_gems
@@ -13,4 +13,4 @@ matrix 0.4.2 https://github.com/ruby/matrix
prime 0.1.2 https://github.com/ruby/prime
rbs 1.7.1 https://github.com/ruby/rbs
typeprof 0.20.4 https://github.com/ruby/typeprof
-debug 1.3.4 https://github.com/ruby/debug
+debug 1.3.4 https://github.com/ruby/debug 1e1bc8262fcbd33199114b7573151c7754a3e505
diff --git a/include/ruby/internal/intern/class.h b/include/ruby/internal/intern/class.h
index 835e85c26d..2181ab93c7 100644
--- a/include/ruby/internal/intern/class.h
+++ b/include/ruby/internal/intern/class.h
@@ -158,7 +158,7 @@ VALUE rb_mod_included_modules(VALUE mod);
VALUE rb_mod_include_p(VALUE child, VALUE parent);
/**
- * Queries the module's ancestors. This routine gathers classes and modules
+ * Queries the module's ancestors. This routine gathers classes and modules
* that the passed module either inherits, includes, or prepends, then
* recursively applies that routine again and again to the collected entries
* until the list doesn't grow up.
@@ -188,6 +188,19 @@ VALUE rb_mod_ancestors(VALUE mod);
VALUE rb_class_descendants(VALUE klass);
/**
+ * Queries the class's direct descendants. This routine gathers classes that are
+ * direct subclasses of the given class,
+ * returning an array of classes that have the given class as a superclass.
+ * The returned array does not include singleton classes.
+ *
+ * @param[in] klass A class.
+ * @return An array of classes where `klass` is the `superclass`.
+ *
+ * @internal
+ */
+VALUE rb_class_subclasses(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 0f67b27667..705ba7ff1c 100644
--- a/object.c
+++ b/object.c
@@ -4660,6 +4660,7 @@ InitVM_Object(void)
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_method(rb_cClass, "subclasses", rb_class_subclasses, 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/subclasses_spec.rb b/spec/ruby/core/class/subclasses_spec.rb
new file mode 100644
index 0000000000..ddbcfb02c0
--- /dev/null
+++ b/spec/ruby/core/class/subclasses_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative '../module/fixtures/classes'
+
+ruby_version_is '3.1' do
+ describe "Class#subclasses" do
+ it "returns a list of classes directly inheriting from self" do
+ assert_subclasses(ModuleSpecs::Parent, [ModuleSpecs::Child, ModuleSpecs::Child2])
+ end
+
+ it "does not return included modules" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ parent.include(mod)
+
+ assert_subclasses(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.subclasses.should_not include(a_obj.singleton_class)
+ end
+
+ it "has 1 entry per module or class" do
+ ModuleSpecs::Parent.subclasses.should == ModuleSpecs::Parent.subclasses.uniq
+ end
+
+ def assert_subclasses(mod,subclasses)
+ mod.subclasses.sort_by(&:inspect).should == subclasses.sort_by(&:inspect)
+ end
+ end
+end
diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb
index eb448e952a..0ceb36bb1d 100644
--- a/test/ruby/test_class.rb
+++ b/test/ruby/test_class.rb
@@ -770,6 +770,44 @@ class TestClass < Test::Unit::TestCase
end
end
+ def test_subclasses
+ 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], c.subclasses)
+ assert_equal([ssc], sc.subclasses)
+ assert_equal([], ssc.subclasses)
+
+ object_subclasses = Object.subclasses
+ assert_include(object_subclasses, c)
+ assert_not_include(object_subclasses, sc)
+ assert_not_include(object_subclasses, ssc)
+ object_subclasses.each do |subclass|
+ assert_equal Object, subclass.superclass, "Expected #{subclass}.superclass to be Object"
+ end
+ end
+
+ def test_subclass_gc
+ c = Class.new
+ 100000.times do
+ cc = Class.new(c)
+ 100.times { Class.new(cc) }
+ end
+ assert(c.subclasses.size <= 100000)
+ end
+
+ def test_subclass_gc_stress
+ 10000.times do
+ c = Class.new
+ 100.times { Class.new(c) }
+ assert(c.subclasses.size <= 100)
+ end
+ end
+
def test_classext_memory_leak
assert_no_memory_leak([], <<-PREP, <<-CODE, rss: true)
code = proc { Class.new }