Project

General

Profile

Actions

Bug #22190

closed

Class#subclasses does not include clones of classes that include modules

Bug #22190: Class#subclasses does not include clones of classes that include modules

Added by shugo (Shugo Maeda) 7 days ago. Updated 1 day ago.

Status:
Closed
Target version:
ruby -v:
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [x86_64-linux]
[ruby-core:126038]

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), 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).

Updated by shugo (Shugo Maeda) 7 days ago Actions #1 [ruby-core:126039]

  • 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

Updated by shugo (Shugo Maeda) 1 day ago Actions #2 [ruby-core:126103]

  • Status changed from Open to Closed
Actions

Also available in: PDF Atom