Skip to content

Commit e783bec

Browse files
authored
Merge pull request rails#52722 from Shopify/class-attribute-redefine-once-take-2
class_attribute: reduce reliance on define_method
2 parents 32e549a + 1962e8d commit e783bec

File tree

6 files changed

+79
-23
lines changed

6 files changed

+79
-23
lines changed

activerecord/test/cases/test_case.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ def with_has_many_inversing(model = ActiveRecord::Base)
141141
yield
142142
ensure
143143
model.has_many_inversing = old
144-
if model != ActiveRecord::Base && !old
145-
model.singleton_class.remove_method(:has_many_inversing) # reset the class_attribute
146-
end
147144
end
148145

149146
def with_automatic_scope_inversing(*reflections)

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ module ActiveSupport
6262
autoload :Cache
6363
autoload :Callbacks
6464
autoload :Configurable
65+
autoload :ClassAttribute
6566
autoload :Deprecation
6667
autoload :Delegation
6768
autoload :Digest

activesupport/lib/active_support/callbacks.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ module Callbacks
6666

6767
included do
6868
extend ActiveSupport::DescendantsTracker
69-
class_attribute :__callbacks, instance_writer: false, default: {}
69+
class_attribute :__callbacks, instance_writer: false, instance_predicate: false, default: {}
7070
end
7171

7272
CALLBACK_FILTER_TYPES = [:before, :after, :around].freeze
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module ClassAttribute # :nodoc:
5+
class << self
6+
def redefine(owner, name, value)
7+
if owner.singleton_class?
8+
owner.redefine_method(name) { value }
9+
owner.send(:public, name)
10+
end
11+
12+
owner.redefine_singleton_method(name) { value }
13+
owner.singleton_class.send(:public, name)
14+
15+
owner.redefine_singleton_method("#{name}=") do |new_value|
16+
if owner.equal?(self)
17+
value = new_value
18+
else
19+
::ActiveSupport::ClassAttribute.redefine(self, name, new_value)
20+
end
21+
end
22+
owner.singleton_class.send(:public, "#{name}=")
23+
end
24+
end
25+
end
26+
end

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

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,16 @@ def class_attribute(*attrs, instance_accessor: true,
9191
raise TypeError, "#{name.inspect} is not a symbol nor a string"
9292
end
9393

94-
class_methods << <<~RUBY # In case the method exists and is not public
95-
silence_redefinition_of_method def #{name}
96-
end
97-
RUBY
98-
99-
methods << <<~RUBY if instance_reader
100-
silence_redefinition_of_method def #{name}
101-
defined?(@#{name}) ? @#{name} : self.class.#{name}
102-
end
103-
RUBY
94+
name = name.to_sym
95+
::ActiveSupport::ClassAttribute.redefine(self, name, default)
10496

105-
class_methods << <<~RUBY
106-
silence_redefinition_of_method def #{name}=(value)
107-
redefine_method(:#{name}) { value } if singleton_class?
108-
redefine_singleton_method(:#{name}) { value }
109-
value
110-
end
111-
RUBY
97+
unless singleton_class?
98+
methods << <<~RUBY if instance_reader
99+
silence_redefinition_of_method def #{name}
100+
defined?(@#{name}) ? @#{name} : self.class.#{name}
101+
end
102+
RUBY
103+
end
112104

113105
methods << <<~RUBY if instance_writer
114106
silence_redefinition_of_method(:#{name}=)
@@ -125,7 +117,5 @@ def class_attribute(*attrs, instance_accessor: true,
125117

126118
location = caller_locations(1, 1).first
127119
class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
128-
129-
attrs.each { |name| public_send("#{name}=", default) }
130120
end
131121
end

activesupport/test/core_ext/class/attribute_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def setup
88
@klass = Class.new do
99
class_attribute :setting
1010
class_attribute :timeout, default: 5
11+
class_attribute :system # private kernel method
1112
end
1213

1314
@sub = Class.new(@klass)
@@ -30,11 +31,14 @@ def setup
3031
test "overridable" do
3132
@sub.setting = 1
3233
assert_nil @klass.setting
34+
assert_equal 1, @sub.setting
3335

3436
@klass.setting = 2
37+
assert_equal 2, @klass.setting
3538
assert_equal 1, @sub.setting
3639

3740
assert_equal 1, Class.new(@sub).setting
41+
assert_equal 2, Class.new(@klass).setting
3842
end
3943

4044
test "predicate method" do
@@ -97,8 +101,34 @@ def setup
97101

98102
test "works well with singleton classes" do
99103
object = @klass.new
104+
100105
object.singleton_class.setting = "foo"
101106
assert_equal "foo", object.setting
107+
assert_nil @klass.setting
108+
109+
object.singleton_class.setting = "bar"
110+
assert_equal "bar", object.setting
111+
assert_nil @klass.setting
112+
113+
@klass.setting = "plop"
114+
assert_equal "bar", object.setting
115+
assert_equal "plop", @klass.setting
116+
end
117+
118+
test "when defined in a class's singleton" do
119+
@klass = Class.new do
120+
class << self
121+
class_attribute :__callbacks, default: 1
122+
end
123+
end
124+
125+
assert_equal 1, @klass.__callbacks
126+
assert_equal 1, @klass.singleton_class.__callbacks
127+
128+
# I honestly think this is a bug, but that's how it used to behave
129+
@klass.__callbacks = 4
130+
assert_equal 1, @klass.__callbacks
131+
assert_equal 1, @klass.singleton_class.__callbacks
102132
end
103133

104134
test "works well with module singleton classes" do
@@ -115,4 +145,16 @@ class << self
115145
val = @klass.public_send(:setting=, 1)
116146
assert_equal 1, val
117147
end
148+
149+
test "works when overriding private methods from an ancestor" do
150+
assert_nil @klass.system
151+
@klass.system = 1
152+
assert_equal 1, @klass.system
153+
154+
instance = @klass.new
155+
assert_equal 1, instance.system
156+
assert_predicate @klass.new, :system?
157+
instance.system = 2
158+
assert_equal 2, instance.system
159+
end
118160
end

0 commit comments

Comments
 (0)