lib/active_model/attribute_methods.rb in activemodel-6.0.6.1 vs lib/active_model/attribute_methods.rb in activemodel-6.1.0.rc1

- old
+ new

@@ -205,14 +205,16 @@ # person.nickname # => "Bob" # person.name_short? # => true # person.nickname_short? # => true def alias_attribute(new_name, old_name) self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s) - attribute_method_matchers.each do |matcher| - matcher_new = matcher.method_name(new_name).to_s - matcher_old = matcher.method_name(old_name).to_s - define_proxy_call false, self, matcher_new, matcher_old + CodeGenerator.batch(self, __FILE__, __LINE__) do |owner| + attribute_method_matchers.each do |matcher| + matcher_new = matcher.method_name(new_name).to_s + matcher_old = matcher.method_name(old_name).to_s + define_proxy_call false, owner, matcher_new, matcher_old + end end end # Is +new_name+ an alias? def attribute_alias?(new_name) @@ -247,11 +249,13 @@ # def clear_attribute(attr) # send("#{attr}=", nil) # end # end def define_attribute_methods(*attr_names) - attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) } + CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner| + attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) } + end end # Declares an attribute that should be prefixed and suffixed by # <tt>ActiveModel::AttributeMethods</tt>. # @@ -279,25 +283,27 @@ # # person = Person.new # person.name = 'Bob' # person.name # => "Bob" # person.name_short? # => true - def define_attribute_method(attr_name) - attribute_method_matchers.each do |matcher| - method_name = matcher.method_name(attr_name) + def define_attribute_method(attr_name, _owner: generated_attribute_methods) + CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner| + attribute_method_matchers.each do |matcher| + method_name = matcher.method_name(attr_name) - unless instance_method_already_implemented?(method_name) - generate_method = "define_method_#{matcher.target}" + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{matcher.target}" - if respond_to?(generate_method, true) - send(generate_method, attr_name.to_s) - else - define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s + if respond_to?(generate_method, true) + send(generate_method, attr_name.to_s, owner: owner) + else + define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s + end end end + attribute_method_matchers_cache.clear end - attribute_method_matchers_cache.clear end # Removes all the previously dynamically defined methods from the class. # # class Person @@ -321,16 +327,56 @@ # Person.undefine_attribute_methods # # person.name_short? # => NoMethodError def undefine_attribute_methods generated_attribute_methods.module_eval do - instance_methods.each { |m| undef_method(m) } + undef_method(*instance_methods) end attribute_method_matchers_cache.clear end private + class CodeGenerator + class << self + def batch(owner, path, line) + if owner.is_a?(CodeGenerator) + yield owner + else + instance = new(owner, path, line) + result = yield instance + instance.execute + result + end + end + end + + def initialize(owner, path, line) + @owner = owner + @path = path + @line = line + @sources = ["# frozen_string_literal: true\n"] + @renames = {} + end + + def <<(source_line) + @sources << source_line + end + + def rename_method(old_name, new_name) + @renames[old_name] = new_name + end + + def execute + @owner.module_eval(@sources.join(";"), @path, @line - 1) + @renames.each do |old_name, new_name| + @owner.alias_method new_name, old_name + @owner.undef_method old_name + end + end + end + private_constant :CodeGenerator + def generated_attribute_methods @generated_attribute_methods ||= Module.new.tap { |mod| include mod } end def instance_method_already_implemented?(method_name) @@ -350,22 +396,18 @@ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4) end def attribute_method_matchers_matching(method_name) attribute_method_matchers_cache.compute_if_absent(method_name) do - # Bump plain matcher to last place so that only methods that do not - # match any other pattern match the actual attribute name. - # This is currently only needed to support legacy usage. - matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) - matchers.map { |matcher| matcher.match(method_name) }.compact + attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact end end # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. - def define_proxy_call(include_private, mod, name, target, *extra) + def define_proxy_call(include_private, code_generator, name, target, *extra) defn = if NAME_COMPILABLE_REGEXP.match?(name) "def #{name}(*args)" else "define_method(:'#{name}') do |*args|" end @@ -376,16 +418,15 @@ "#{"self." unless include_private}#{target}(#{extra})" else "send(:'#{target}', #{extra})" end - mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - #{defn} - #{body} - end - ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true) - RUBY + code_generator << + defn << + body << + "end" << + "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)" end class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :target @@ -405,14 +446,10 @@ end def method_name(attr_name) @method_name % attr_name end - - def plain? - prefix.empty? && suffix.empty? - end end end # Allows access to the object attributes, which are held in the hash # returned by <tt>attributes</tt>, as though they were first-class @@ -497,22 +534,21 @@ # We are also defining a constant to hold the frozen string of # the attribute name. Using a constant means that we do not have # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. - def self.define_attribute_accessor_method(mod, attr_name, writer: false) + def self.define_attribute_accessor_method(owner, attr_name, writer: false) method_name = "#{attr_name}#{'=' if writer}" if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name) - yield method_name, "'#{attr_name}'.freeze" + yield method_name, "'#{attr_name}'" else safe_name = attr_name.unpack1("h*") const_name = "ATTR_#{safe_name}" const_set(const_name, attr_name) unless const_defined?(const_name) temp_method_name = "__temp__#{safe_name}#{'=' if writer}" attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}" yield temp_method_name, attr_name_expr - mod.alias_method method_name, temp_method_name - mod.undef_method temp_method_name + owner.rename_method(temp_method_name, method_name) end end end end end