Add Class#subclasses

Implements [Feature #18273]

Returns an array containing the receiver's direct subclasses without
singleton classes.
This commit is contained in:
Jean Boussier 2021-10-28 14:07:11 +02:00
parent a88b19d3d0
commit c0c2b31a35
Notes: git 2021-11-23 19:49:51 +09:00
7 changed files with 163 additions and 20 deletions

16
NEWS.md
View File

@ -121,6 +121,21 @@ Outstanding ones only.
C.descendants #=> [] 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
* Enumerable#compact is added. [[Feature #17312]] * 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 #18172]: https://bugs.ruby-lang.org/issues/18172
[Feature #18229]: https://bugs.ruby-lang.org/issues/18229 [Feature #18229]: https://bugs.ruby-lang.org/issues/18229
[Feature #18290]: https://bugs.ruby-lang.org/issues/18290 [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-1509]: https://github.com/ruby/ruby/pull/1509
[GH-4815]: https://github.com/ruby/ruby/pull/4815 [GH-4815]: https://github.com/ruby/ruby/pull/4815

73
class.c
View File

@ -1377,6 +1377,7 @@ struct subclass_traverse_data
VALUE buffer; VALUE buffer;
long count; long count;
long maxcount; long maxcount;
bool immediate_only;
}; };
static void static void
@ -1390,8 +1391,38 @@ class_descendants_recursive(VALUE klass, VALUE v)
rb_ary_push(data->buffer, klass); rb_ary_push(data->buffer, klass);
} }
data->count++; data->count++;
if (!data->immediate_only) {
rb_class_foreach_subclass(klass, class_descendants_recursive, v);
}
} }
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");
}
return data.buffer;
} }
/* /*
@ -1415,26 +1446,32 @@ class_descendants_recursive(VALUE klass, VALUE v)
VALUE VALUE
rb_class_descendants(VALUE klass) rb_class_descendants(VALUE klass)
{ {
struct subclass_traverse_data data = { Qfalse, 0, -1 }; return class_descendants(klass, false);
}
// 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); * call-seq:
data.maxcount = data.count; * subclasses -> array
data.count = 0; *
* 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 #=> []
*/
size_t gc_count = rb_gc_count(); VALUE
rb_class_subclasses(VALUE klass)
// enumerate subclasses {
rb_class_foreach_subclass(klass, class_descendants_recursive, (VALUE) &data); return class_descendants(klass, true);
if (gc_count != rb_gc_count()) {
rb_bug("GC must not occur during the subclass iteration of Class#descendants");
}
return data.buffer;
} }
static void static void

View File

@ -13,4 +13,4 @@ matrix 0.4.2 https://github.com/ruby/matrix
prime 0.1.2 https://github.com/ruby/prime prime 0.1.2 https://github.com/ruby/prime
rbs 1.7.1 https://github.com/ruby/rbs rbs 1.7.1 https://github.com/ruby/rbs
typeprof 0.20.4 https://github.com/ruby/typeprof 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

View File

@ -158,7 +158,7 @@ VALUE rb_mod_included_modules(VALUE mod);
VALUE rb_mod_include_p(VALUE child, VALUE parent); 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 * that the passed module either inherits, includes, or prepends, then
* recursively applies that routine again and again to the collected entries * recursively applies that routine again and again to the collected entries
* until the list doesn't grow up. * until the list doesn't grow up.
@ -187,6 +187,19 @@ VALUE rb_mod_ancestors(VALUE mod);
*/ */
VALUE rb_class_descendants(VALUE klass); 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 * Generates an array of symbols, which are the list of method names defined in
* the passed class. * the passed class.

View File

@ -4660,6 +4660,7 @@ InitVM_Object(void)
rb_define_method(rb_cClass, "initialize", rb_class_initialize, -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, "superclass", rb_class_superclass, 0);
rb_define_method(rb_cClass, "descendants", rb_class_descendants, 0); /* in class.c */ 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_define_alloc_func(rb_cClass, rb_class_s_alloc);
rb_undef_method(rb_cClass, "extend_object"); rb_undef_method(rb_cClass, "extend_object");
rb_undef_method(rb_cClass, "append_features"); rb_undef_method(rb_cClass, "append_features");

View File

@ -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

View File

@ -770,6 +770,44 @@ class TestClass < Test::Unit::TestCase
end end
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 def test_classext_memory_leak
assert_no_memory_leak([], <<-PREP, <<-CODE, rss: true) assert_no_memory_leak([], <<-PREP, <<-CODE, rss: true)
code = proc { Class.new } code = proc { Class.new }