diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 5415bfdc82a8c..d85562bd38aaa 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -117,6 +117,9 @@ def attribute(*names, default: NOT_SET) raise ArgumentError, "Restricted attribute names: #{invalid_attribute_names.join(", ")}" end + Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "") + Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value") + ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner| names.each do |name| owner.define_cached_method(name, namespace: :current_attributes) do |batch| @@ -134,9 +137,6 @@ def attribute(*names, default: NOT_SET) end end - Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "") - Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value") - self.defaults = defaults.merge(names.index_with { default }) end @@ -185,9 +185,16 @@ def respond_to_missing?(name, _) def method_added(name) super + + # We try to generate instance delegators early to not rely on method_missing. return if name == :initialize + + # If the added method isn't public, we don't delegate it. return unless public_method_defined?(name) + + # If we already have a class method by that name, we don't override it. return if singleton_class.method_defined?(name) || singleton_class.private_method_defined?(name) + Delegation.generate(singleton_class, [name], to: :instance, as: self, nilable: false) end end diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index 56d1c225e37a0..23bc26cc16795 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -276,4 +276,35 @@ def foo; end # Sets the cache because of a `method_added` hook assert_instance_of(Hash, current.bar) end + + test "instance delegators are eagerly defined" do + current = Class.new(ActiveSupport::CurrentAttributes) do + def self.name + "MyCurrent" + end + + def regular + :regular + end + + attribute :attr, default: :att + end + + assert current.singleton_class.method_defined?(:attr) + assert current.singleton_class.method_defined?(:attr=) + assert current.singleton_class.method_defined?(:regular) + end + + test "attribute delegators have precise signature" do + current = Class.new(ActiveSupport::CurrentAttributes) do + def self.name + "MyCurrent" + end + + attribute :attr, default: :att + end + + assert_equal [], current.method(:attr).parameters + assert_equal [[:req, :value]], current.method(:attr=).parameters + end end