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).
Updated by shugo (Shugo Maeda) 7 days ago
- Assignee set to jhawthorn (John Hawthorn)
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED
I've created a pull request: https://github.com/ruby/ruby/pull/17799/changes
Updated by shugo (Shugo Maeda) 1 day ago
- Status changed from Open to Closed