lib/active_support/descendants_tracker.rb
# frozen_string_literal: true require "weakref" require "active_support/ruby_features" module ActiveSupport # This module provides an internal implementation to track descendants # which is faster than iterating through ObjectSpace. module DescendantsTracker class << self def direct_descendants(klass) ActiveSupport::Deprecation.warn(<<~MSG) ActiveSupport::DescendantsTracker.direct_descendants is deprecated and will be removed in Rails 7.1. Use ActiveSupport::DescendantsTracker.subclasses instead. MSG subclasses(klass) end end @clear_disabled = false if RubyFeatures::CLASS_SUBCLASSES @@excluded_descendants = if RUBY_ENGINE == "ruby" # On MRI `ObjectSpace::WeakMap` keys are weak references. # So we can simply use WeakMap as a `Set`. ObjectSpace::WeakMap.new else # On TruffleRuby `ObjectSpace::WeakMap` keys are strong references. # So we use `object_id` as a key and the actual object as a value. # # JRuby for now doesn't have Class#descendant, but when it will, it will likely # have the same WeakMap semantic than Truffle so we future proof this as much as possible. class WeakSet # :nodoc: def initialize @map = ObjectSpace::WeakMap.new end def [](object) @map.key?(object.object_id) end def []=(object, _present) @map[object.object_id] = object end end WeakSet.new end class << self def disable_clear! # :nodoc: unless @clear_disabled @clear_disabled = true remove_method(:subclasses) @@excluded_descendants = nil end end def subclasses(klass) klass.subclasses end def descendants(klass) klass.descendants end def clear(classes) # :nodoc: raise "DescendantsTracker.clear was disabled because config.cache_classes = true" if @clear_disabled classes.each do |klass| @@excluded_descendants[klass] = true klass.descendants.each do |descendant| @@excluded_descendants[descendant] = true end end end def native? # :nodoc: true end end def subclasses subclasses = super subclasses.reject! { |d| @@excluded_descendants[d] } subclasses end def descendants subclasses.concat(subclasses.flat_map(&:descendants)) end def direct_descendants ActiveSupport::Deprecation.warn(<<~MSG) ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1. Use #subclasses instead. MSG subclasses end else @@direct_descendants = {} class << self def disable_clear! # :nodoc: @clear_disabled = true end def subclasses(klass) descendants = @@direct_descendants[klass] descendants ? descendants.to_a : [] end def descendants(klass) arr = [] accumulate_descendants(klass, arr) arr end def clear(classes) # :nodoc: raise "DescendantsTracker.clear was disabled because config.cache_classes = true" if @clear_disabled @@direct_descendants.each do |klass, direct_descendants_of_klass| if classes.member?(klass) @@direct_descendants.delete(klass) else direct_descendants_of_klass.reject! do |direct_descendant_of_class| classes.member?(direct_descendant_of_class) end end end end def native? # :nodoc: false end # This is the only method that is not thread safe, but is only ever called # during the eager loading phase. def store_inherited(klass, descendant) (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant end private def accumulate_descendants(klass, acc) if direct_descendants = @@direct_descendants[klass] direct_descendants.each do |direct_descendant| acc << direct_descendant accumulate_descendants(direct_descendant, acc) end end end end def inherited(base) DescendantsTracker.store_inherited(self, base) super end def direct_descendants ActiveSupport::Deprecation.warn(<<~MSG) ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1. Use #subclasses instead. MSG DescendantsTracker.subclasses(self) end def subclasses DescendantsTracker.subclasses(self) end def descendants DescendantsTracker.descendants(self) end # DescendantsArray is an array that contains weak references to classes. class DescendantsArray # :nodoc: include Enumerable def initialize @refs = [] end def initialize_copy(orig) @refs = @refs.dup end def <<(klass) @refs << WeakRef.new(klass) end def each @refs.reject! do |ref| yield ref.__getobj__ false rescue WeakRef::RefError true end self end def refs_size @refs.size end def cleanup! @refs.delete_if { |ref| !ref.weakref_alive? } end def reject! @refs.reject! do |ref| yield ref.__getobj__ rescue WeakRef::RefError true end end end end end end