class RuboCop::Cop::Naming::MemoizedInstanceVariableName

Checks for memoized methods whose instance variable name does not match the method name. Applies to both regular methods (defined with ‘def`) and dynamic methods (defined with `define_method` or `define_singleton_method`).

This cop can be configured with the EnforcedStyleForLeadingUnderscores directive. It can be configured to allow for memoized instance variables prefixed with an underscore. Prefixing ivars with an underscore is a convention that is used to implicitly indicate that an ivar should not be set or referenced outside of the memoization method.

@safety

This cop relies on the pattern `@instance_var ||= ...`,
but this is sometimes used for other purposes than memoization
so this cop is considered unsafe.

@example EnforcedStyleForLeadingUnderscores: disallowed (default)

# bad
# Method foo is memoized using an instance variable that is
# not `@foo`. This can cause confusion and bugs.
def foo
  @something ||= calculate_expensive_thing
end

def foo
  return @something if defined?(@something)
  @something = calculate_expensive_thing
end

# good
def _foo
  @foo ||= calculate_expensive_thing
end

# good
def foo
  @foo ||= calculate_expensive_thing
end

# good
def foo
  @foo ||= begin
    calculate_expensive_thing
  end
end

# good
def foo
  helper_variable = something_we_need_to_calculate_foo
  @foo ||= calculate_expensive_thing(helper_variable)
end

# good
define_method(:foo) do
  @foo ||= calculate_expensive_thing
end

# good
define_method(:foo) do
  return @foo if defined?(@foo)
  @foo = calculate_expensive_thing
end

@example EnforcedStyleForLeadingUnderscores: required

# bad
def foo
  @something ||= calculate_expensive_thing
end

# bad
def foo
  @foo ||= calculate_expensive_thing
end

def foo
  return @foo if defined?(@foo)
  @foo = calculate_expensive_thing
end

# good
def foo
  @_foo ||= calculate_expensive_thing
end

# good
def _foo
  @_foo ||= calculate_expensive_thing
end

def foo
  return @_foo if defined?(@_foo)
  @_foo = calculate_expensive_thing
end

# good
define_method(:foo) do
  @_foo ||= calculate_expensive_thing
end

# good
define_method(:foo) do
  return @_foo if defined?(@_foo)
  @_foo = calculate_expensive_thing
end

@example EnforcedStyleForLeadingUnderscores :optional

# bad
def foo
  @something ||= calculate_expensive_thing
end

# good
def foo
  @foo ||= calculate_expensive_thing
end

# good
def foo
  @_foo ||= calculate_expensive_thing
end

# good
def _foo
  @_foo ||= calculate_expensive_thing
end

# good
def foo
  return @_foo if defined?(@_foo)
  @_foo = calculate_expensive_thing
end

# good
define_method(:foo) do
  @foo ||= calculate_expensive_thing
end

# good
define_method(:foo) do
  @_foo ||= calculate_expensive_thing
end

Constants

DYNAMIC_DEFINE_METHODS
MSG
UNDERSCORE_REQUIRED

Public Instance Methods

on_defined?(node) click to toggle source

rubocop:disable Metrics/AbcSize, Metrics/MethodLength

# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 197
def on_defined?(node)
  arg = node.arguments.first
  return unless arg.ivar_type?

  method_node, method_name = find_definition(node)
  return unless method_node

  var_name = arg.children.first
  defined_memoized?(method_node.body, var_name) do |defined_ivar, return_ivar, ivar_assign|
    return if matches?(method_name, ivar_assign)

    msg = format(
      message(var_name.to_s),
      var: var_name.to_s,
      suggested_var: suggested_var(method_name),
      method: method_name
    )
    add_offense(defined_ivar, message: msg)
    add_offense(return_ivar, message: msg)
    add_offense(ivar_assign.loc.name, message: msg)
  end
end
on_or_asgn(node) click to toggle source

rubocop:disable Metrics/AbcSize

# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 166
def on_or_asgn(node)
  lhs, _value = *node
  return unless lhs.ivasgn_type?

  method_node, method_name = find_definition(node)
  return unless method_node

  body = method_node.body
  return unless body == node || body.children.last == node

  return if matches?(method_name, lhs)

  msg = format(
    message(lhs.children.first.to_s),
    var: lhs.children.first.to_s,
    suggested_var: suggested_var(method_name),
    method: method_name
  )
  add_offense(lhs, message: msg)
end

Private Instance Methods

find_definition(node) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 227
def find_definition(node)
  # Methods can be defined in a `def` or `defs`,
  # or dynamically via a `block` node.
  node.each_ancestor(:def, :defs, :block).each do |ancestor|
    method_node, method_name = method_definition?(ancestor)
    return [method_node, method_name] if method_node
  end

  nil
end
matches?(method_name, ivar_assign) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 238
def matches?(method_name, ivar_assign)
  return true if ivar_assign.nil? || method_name == :initialize

  method_name = method_name.to_s.delete('!?')
  variable = ivar_assign.children.first
  variable_name = variable.to_s.sub('@', '')

  variable_name_candidates(method_name).include?(variable_name)
end
message(variable) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 248
def message(variable)
  variable_name = variable.to_s.sub('@', '')

  return UNDERSCORE_REQUIRED if style == :required && !variable_name.start_with?('_')

  MSG
end
style_parameter_name() click to toggle source

rubocop:enable Metrics/AbcSize, Metrics/MethodLength

# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 223
def style_parameter_name
  'EnforcedStyleForLeadingUnderscores'
end
suggested_var(method_name) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 256
def suggested_var(method_name)
  suggestion = method_name.to_s.delete('!?')

  style == :required ? "_#{suggestion}" : suggestion
end
variable_name_candidates(method_name) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 262
def variable_name_candidates(method_name)
  no_underscore = method_name.delete_prefix('_')
  with_underscore = "_#{method_name}"
  case style
  when :required
    [with_underscore,
     method_name.start_with?('_') ? method_name : nil].compact
  when :disallowed
    [method_name, no_underscore]
  when :optional
    [method_name, with_underscore, no_underscore]
  else
    raise 'Unreachable'
  end
end