Skip to content

Commit ed0ce4b

Browse files
authored
Merge pull request rails#43548 from Shopify/native-descendants-tracker
Refactor DescendantsTracker to leverage native Class#descendants on Ruby 3.1
2 parents 62542cc + ffae3bd commit ed0ce4b

File tree

6 files changed

+170
-87
lines changed

6 files changed

+170
-87
lines changed

activesupport/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
* `ActiveSupport::DescendantsTracker` now mostly delegate to `Class#descendants` on Ruby 3.1
2+
3+
Ruby now provides a fast `Class#descendants` making `ActiveSupport::DescendantsTracker` mostly useless.
4+
5+
As a result the following methods are deprecated:
6+
7+
- `ActiveSupport::DescendantsTracker.direct_descendants`
8+
- `ActiveSupport::DescendantsTracker#direct_descendants`
9+
10+
*Jean Boussier*
11+
112
* Fix the `Digest::UUID.uuid_from_hash` behavior for namespace IDs that are different from the ones defined on `Digest::UUID`.
213

314
The new behavior will be enabled by setting the

activesupport/lib/active_support.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ def self.utc_to_local_returns_utc_offset_times=(value)
116116
def self.current_attributes_use_thread_variables=(value)
117117
CurrentAttributes._use_thread_variables = value
118118
end
119+
120+
@has_native_class_descendants = Class.method_defined?(:descendants) # RUBY_VERSION >= "3.1"
119121
end
120122

121123
autoload :I18n, "active_support/i18n"

activesupport/lib/active_support/callbacks.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ def normalize_callback_params(filters, block) # :nodoc:
608608
# This is used internally to append, prepend and skip callbacks to the
609609
# CallbackChain.
610610
def __update_callbacks(name) # :nodoc:
611-
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
611+
([self] + self.descendants).reverse_each do |target|
612612
chain = target.get_callbacks name
613613
yield target, chain.dup
614614
end
@@ -732,7 +732,7 @@ def skip_callback(name, *filter_list, &block)
732732
def reset_callbacks(name)
733733
callbacks = get_callbacks name
734734

735-
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
735+
self.descendants.each do |target|
736736
chain = target.get_callbacks(name).dup
737737
callbacks.each { |c| chain.delete(c) }
738738
target.set_callbacks name, chain
@@ -825,7 +825,7 @@ def define_callbacks(*names)
825825
names.each do |name|
826826
name = name.to_sym
827827

828-
([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
828+
([self] + self.descendants).each do |target|
829829
target.set_callbacks name, CallbackChain.new(name, options)
830830
end
831831

activesupport/lib/active_support/core_ext/class/subclasses.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def descendants
1818
ObjectSpace.each_object(singleton_class).reject do |k|
1919
k.singleton_class? || k == self
2020
end
21-
end unless method_defined?(:descendants) # RUBY_VERSION >= "3.1"
21+
end unless ActiveSupport.instance_variable_get(:@has_native_class_descendants) # RUBY_VERSION >= "3.1"
2222

2323
# Returns an array with the direct children of +self+.
2424
#

activesupport/lib/active_support/descendants_tracker.rb

Lines changed: 124 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,108 +6,160 @@ module ActiveSupport
66
# This module provides an internal implementation to track descendants
77
# which is faster than iterating through ObjectSpace.
88
module DescendantsTracker
9-
@@direct_descendants = {}
10-
119
class << self
1210
def direct_descendants(klass)
13-
descendants = @@direct_descendants[klass]
14-
descendants ? descendants.to_a : []
11+
ActiveSupport::Deprecation.warn(<<~MSG)
12+
ActiveSupport::DescendantsTracker.direct_descendants is deprecated and will be removed in Rails 7.1.
13+
Use ActiveSupport::DescendantsTracker.subclasses instead.
14+
MSG
15+
subclasses(klass)
1516
end
16-
alias_method :subclasses, :direct_descendants
17+
end
1718

18-
def descendants(klass)
19-
arr = []
20-
accumulate_descendants(klass, arr)
21-
arr
22-
end
19+
if ActiveSupport.instance_variable_get(:@has_native_class_descendants) # RUBY_VERSION >= "3.1"
20+
class << self
21+
def subclasses(klass)
22+
klass.subclasses
23+
end
2324

24-
def clear(only: nil)
25-
if only.nil?
26-
@@direct_descendants.clear
27-
return
25+
def descendants(klass)
26+
klass.descendants
2827
end
2928

30-
@@direct_descendants.each do |klass, direct_descendants_of_klass|
31-
if only.member?(klass)
32-
@@direct_descendants.delete(klass)
33-
else
34-
direct_descendants_of_klass.reject! do |direct_descendant_of_class|
35-
only.member?(direct_descendant_of_class)
36-
end
37-
end
29+
def clear(only: nil) # :nodoc:
30+
# noop
31+
end
32+
33+
def native? # :nodoc:
34+
true
3835
end
3936
end
4037

41-
# This is the only method that is not thread safe, but is only ever called
42-
# during the eager loading phase.
43-
def store_inherited(klass, descendant)
44-
(@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
38+
def subclasses
39+
descendants.select { |descendant| descendant.superclass == self }
4540
end
4641

47-
private
48-
def accumulate_descendants(klass, acc)
49-
if direct_descendants = @@direct_descendants[klass]
50-
direct_descendants.each do |direct_descendant|
51-
acc << direct_descendant
52-
accumulate_descendants(direct_descendant, acc)
42+
def direct_descendants
43+
ActiveSupport::Deprecation.warn(<<~MSG)
44+
ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1.
45+
Use #subclasses instead.
46+
MSG
47+
subclasses
48+
end
49+
else
50+
@@direct_descendants = {}
51+
52+
class << self
53+
def subclasses(klass)
54+
descendants = @@direct_descendants[klass]
55+
descendants ? descendants.to_a : []
56+
end
57+
58+
def descendants(klass)
59+
arr = []
60+
accumulate_descendants(klass, arr)
61+
arr
62+
end
63+
64+
def clear(only: nil) # :nodoc:
65+
if only.nil?
66+
@@direct_descendants.clear
67+
return
68+
end
69+
70+
@@direct_descendants.each do |klass, direct_descendants_of_klass|
71+
if only.member?(klass)
72+
@@direct_descendants.delete(klass)
73+
else
74+
direct_descendants_of_klass.reject! do |direct_descendant_of_class|
75+
only.member?(direct_descendant_of_class)
76+
end
5377
end
5478
end
5579
end
56-
end
5780

58-
def inherited(base)
59-
DescendantsTracker.store_inherited(self, base)
60-
super
61-
end
81+
def native? # :nodoc:
82+
false
83+
end
6284

63-
def direct_descendants
64-
DescendantsTracker.direct_descendants(self)
65-
end
66-
alias_method :subclasses, :direct_descendants
85+
# This is the only method that is not thread safe, but is only ever called
86+
# during the eager loading phase.
87+
def store_inherited(klass, descendant)
88+
(@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
89+
end
6790

68-
def descendants
69-
DescendantsTracker.descendants(self)
70-
end
91+
private
92+
def accumulate_descendants(klass, acc)
93+
if direct_descendants = @@direct_descendants[klass]
94+
direct_descendants.each do |direct_descendant|
95+
acc << direct_descendant
96+
accumulate_descendants(direct_descendant, acc)
97+
end
98+
end
99+
end
100+
end
71101

72-
# DescendantsArray is an array that contains weak references to classes.
73-
class DescendantsArray # :nodoc:
74-
include Enumerable
102+
def inherited(base)
103+
DescendantsTracker.store_inherited(self, base)
104+
super
105+
end
75106

76-
def initialize
77-
@refs = []
107+
def direct_descendants
108+
ActiveSupport::Deprecation.warn(<<~MSG)
109+
ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1.
110+
Use #subclasses instead.
111+
MSG
112+
DescendantsTracker.subclasses(self)
78113
end
79114

80-
def initialize_copy(orig)
81-
@refs = @refs.dup
115+
def subclasses
116+
DescendantsTracker.subclasses(self)
82117
end
83118

84-
def <<(klass)
85-
@refs << WeakRef.new(klass)
119+
def descendants
120+
DescendantsTracker.descendants(self)
86121
end
87122

88-
def each
89-
@refs.reject! do |ref|
90-
yield ref.__getobj__
91-
false
92-
rescue WeakRef::RefError
93-
true
123+
# DescendantsArray is an array that contains weak references to classes.
124+
class DescendantsArray # :nodoc:
125+
include Enumerable
126+
127+
def initialize
128+
@refs = []
94129
end
95-
self
96-
end
97130

98-
def refs_size
99-
@refs.size
100-
end
131+
def initialize_copy(orig)
132+
@refs = @refs.dup
133+
end
101134

102-
def cleanup!
103-
@refs.delete_if { |ref| !ref.weakref_alive? }
104-
end
135+
def <<(klass)
136+
@refs << WeakRef.new(klass)
137+
end
105138

106-
def reject!
107-
@refs.reject! do |ref|
108-
yield ref.__getobj__
109-
rescue WeakRef::RefError
110-
true
139+
def each
140+
@refs.reject! do |ref|
141+
yield ref.__getobj__
142+
false
143+
rescue WeakRef::RefError
144+
true
145+
end
146+
self
147+
end
148+
149+
def refs_size
150+
@refs.size
151+
end
152+
153+
def cleanup!
154+
@refs.delete_if { |ref| !ref.weakref_alive? }
155+
end
156+
157+
def reject!
158+
@refs.reject! do |ref|
159+
yield ref.__getobj__
160+
rescue WeakRef::RefError
161+
true
162+
end
111163
end
112164
end
113165
end

activesupport/test/descendants_tracker_test.rb

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
class DescendantsTrackerTest < ActiveSupport::TestCase
88
setup do
9-
@original_state = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
10-
@original_state.each { |k, v| @original_state[k] = v.dup }
9+
if ActiveSupport::DescendantsTracker.class_variable_defined?(:@@direct_descendants)
10+
@original_state = ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).dup
11+
@original_state.each { |k, v| @original_state[k] = v.dup }
12+
end
1113

1214
ActiveSupport::DescendantsTracker.clear
1315
eval <<~RUBY
@@ -30,10 +32,14 @@ class Grandchild2 < Child1
3032
end
3133

3234
teardown do
33-
ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(@original_state)
35+
if ActiveSupport::DescendantsTracker.class_variable_defined?(:@@direct_descendants)
36+
ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).replace(@original_state)
37+
end
3438

3539
%i(Parent Child1 Child2 Grandchild1 Grandchild2).each do |name|
36-
DescendantsTrackerTest.send(:remove_const, name)
40+
if DescendantsTrackerTest.const_defined?(name)
41+
DescendantsTrackerTest.send(:remove_const, name)
42+
end
3743
end
3844
end
3945

@@ -65,30 +71,42 @@ class Grandchild2 < Child1
6571
end
6672

6773
test ".direct_descendants" do
68-
assert_equal_sets [Child1, Child2], Parent.direct_descendants
69-
assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
70-
assert_equal_sets [], Child2.direct_descendants
74+
assert_deprecated do
75+
assert_equal_sets [Child1, Child2], Parent.direct_descendants
76+
end
77+
78+
assert_deprecated do
79+
assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
80+
end
81+
82+
assert_deprecated do
83+
assert_equal_sets [], Child2.direct_descendants
84+
end
7185
end
7286

7387
test ".subclasses" do
7488
[Parent, Child1, Child2].each do |klass|
75-
assert_equal klass.direct_descendants, klass.subclasses
89+
assert_equal assert_deprecated { klass.direct_descendants }, klass.subclasses
7690
end
7791
end
7892

7993
test ".clear deletes all state" do
8094
ActiveSupport::DescendantsTracker.clear
81-
assert_empty ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants")
95+
if ActiveSupport::DescendantsTracker.class_variable_defined?(:@@direct_descendants)
96+
assert_empty ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants)
97+
end
8298
end
8399

84100
test ".clear(only) deletes the given classes only" do
101+
skip "Irrelevant for native Class#descendants" if ActiveSupport::DescendantsTracker.native?
102+
85103
ActiveSupport::DescendantsTracker.clear(only: Set[Child2, Grandchild1])
86104

87105
assert_equal_sets [Child1, Grandchild2], Parent.descendants
88106
assert_equal_sets [Grandchild2], Child1.descendants
89107

90-
assert_equal_sets [Child1], Parent.direct_descendants
91-
assert_equal_sets [Grandchild2], Child1.direct_descendants
108+
assert_equal_sets [Child1], assert_deprecated { Parent.direct_descendants }
109+
assert_equal_sets [Grandchild2], assert_deprecated { Child1.direct_descendants }
92110
end
93111

94112
private

0 commit comments

Comments
 (0)