Bug #22190
closedClass#subclasses does not include clones of classes that include modules
Description
Class#subclasses does not return clones of classes that include (or prepend) a module, while clones of classes without modules are returned. This is a regression first released in Ruby 4.0.4.
module M; end
class X; end
class C < X
include M
end
class D < X; end
clone_c = C.clone
clone_d = D.clone
p clone_c.superclass #=> X
p X.subclasses.include?(C) #=> true
p X.subclasses.include?(clone_d) #=> true (D includes no module)
p X.subclasses.include?(clone_c) #=> false (expected: true)
The same happens with prepend instead of include. Ruby 4.0.3 and earlier return true for all of the above.
Analysis¶
Since a2531ba293 ("Simplify subclasses list, remove from Box"), class_associate_super() maintains subclass lists only for direct T_CLASS -> T_CLASS links:
// class.c (master)
// Only maintain subclass lists for T_CLASS→T_CLASS relationships.
// Include/prepend inserts ICLASSes into the super chain, but T_CLASS
// subclass lists should track only the immutable T_CLASS→T_CLASS link.
if (RB_TYPE_P(klass, T_CLASS) && RB_TYPE_P(super, T_CLASS)) {
For normally created classes this is fine because the T_CLASS -> T_CLASS link is registered at class creation time, before any module is included. However, rb_mod_init_copy() builds the clone's superclass chain differently:
- When the original has no origin, the clone shares the original's ICLASS chain:
rb_class_set_super(clone, RCLASS_SUPER(orig))(class.c:983), whereRCLASS_SUPER(orig)is a T_ICLASS. The condition above is false, so the clone is never added to the real superclass's subclass list. - When the original has an origin (prepend), the tail is connected via
rb_class_set_super(clone_origin, RCLASS_SUPER(orig_origin))(class.c:1031), whereclone_originis a T_ICLASS, so this link is not registered either.
Before a2531ba293, subclass entries were reparented onto ICLASSes and the Class#subclasses walk descended through non-T_CLASS entries (see class_descendants_recursive in 4.0.3), so a clone registered under the shared ICLASS was still found from the superclass.
Method lookup and method cache invalidation do not seem to be affected (redefining a method on the superclass after cloning is observed correctly through the clone); the visible breakage is in the reflection API. Other users of the subclass lists may want checking, though (for example the allocator propagation added in 111215de27 also iterates subclass lists).