From c0c2b31a35e19a47b499b57807bc0a0f9325f6d3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 28 Oct 2021 14:07:11 +0200 Subject: Add Class#subclasses Implements [Feature #18273] Returns an array containing the receiver's direct subclasses without singleton classes. --- NEWS.md | 16 ++++++++ class.c | 73 +++++++++++++++++++++++++-------- gems/bundled_gems | 2 +- include/ruby/internal/intern/class.h | 15 ++++++- object.c | 1 + spec/ruby/core/class/subclasses_spec.rb | 38 +++++++++++++++++ test/ruby/test_class.rb | 38 +++++++++++++++++ 7 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 spec/ruby/core/class/subclasses_spec.rb 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. @@ -187,6 +187,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 } -- cgit v1.2.1