Issue #22190 has been updated by shugo (Shugo Maeda). 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 ---------------------------------------- Bug #22190: Class#subclasses does not include clones of classes that include modules https://bugs.ruby-lang.org/issues/22190#change-118029 * Author: shugo (Shugo Maeda) * Status: Open * Assignee: jhawthorn (John Hawthorn) * Target version: 4.0 * ruby -v: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux] * Backport: 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED ---------------------------------------- `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. ```ruby 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: ```c // 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), where `RCLASS_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), where `clone_origin` is 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). -- https://bugs.ruby-lang.org/