Skip to content
Permalink
Browse files

Improve model attribute accessor method names for backtraces

Ruby uses the original method name, so will show the __temp__ method
name in the backtrace. However, in the common case the method name
is compatible with the `def` keyword, so we can avoid the __temp__
method name in that case to improve the name shown in backtraces
or TracePoint#method_id.
  • Loading branch information...
dylanahsmith authored and jeremy committed Jun 22, 2018
1 parent ee95bed commit 99c87ad2474d5c5b6e52ceac34c3cf9f9cb57f9f
@@ -474,5 +474,43 @@ def missing_attribute(attr_name, stack)
def _read_attribute(attr)
__send__(attr)
end

module AttrNames # :nodoc:
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/

# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# 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)
method_name = "#{attr_name}#{'=' if writer}"
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
yield method_name, "'#{attr_name}'.freeze"
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.send(:alias_method, method_name, temp_method_name)
mod.send(:undef_method, temp_method_name)
end
end
end
end
end
@@ -29,17 +29,16 @@ def attribute(name, type = Type::Value.new, **options)
private

def define_method_attribute=(name)
safe_name = name.unpack1("h*")
ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name

generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
write_attribute(name, value)
end
RUBY
end
end

NO_DEFAULT_PROVIDED = Object.new # :nodoc:
@@ -97,15 +96,4 @@ def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
end

module AttributeMethods #:nodoc:
AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}
end
end
@@ -22,15 +22,6 @@ module AttributeMethods
delegate :column_for_attribute, to: :class
end

AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}

RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)

class GeneratedAttributeMethods < Module #:nodoc:
@@ -8,42 +8,19 @@ module Read
module ClassMethods # :nodoc:
private

# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# 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 define_method_attribute(name)
safe_name = name.unpack1("h*")
temp_method = "__temp__#{safe_name}"

ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key

generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def #{temp_method}
#{sync_with_transaction_state}
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
STR

generated_attribute_methods.module_eval do
alias_method name, temp_method
undef_method temp_method
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}
#{sync_with_transaction_state}
name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
RUBY
end
end
end
@@ -13,19 +13,19 @@ module ClassMethods # :nodoc:
private

def define_method_attribute=(name)
safe_name = name.unpack1("h*")
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key

generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
RUBY
end
end
end

0 comments on commit 99c87ad

Please sign in to comment.
You can’t perform that action at this time.