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. Also, its autocorrection is unsafe
because it may conflict with instance variable names already in use.

@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 205
def on_defined?(node)
  arg = node.first_argument
  return false unless arg.ivar_type?

  method_node, method_name = find_definition(node)
  return false unless method_node

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

    suggested_var = suggested_var(method_name)
    msg = format(
      message(var_name.to_s),
      var: var_name.to_s,
      suggested_var: suggested_var,
      method: method_name
    )
    add_offense(defined_ivar, message: msg) do |corrector|
      corrector.replace(defined_ivar, "@#{suggested_var}")
    end
    add_offense(return_ivar, message: msg) do |corrector|
      corrector.replace(return_ivar, "@#{suggested_var}")
    end
    add_offense(ivar_assign.loc.name, message: msg) do |corrector|
      corrector.replace(ivar_assign.loc.name, "@#{suggested_var}")
    end
  end
end
on_or_asgn(node) click to toggle source

rubocop:disable Metrics/AbcSize rubocop:disable Metrics/MethodLength

# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 170
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)

  suggested_var = suggested_var(method_name)
  msg = format(
    message(lhs.children.first.to_s),
    var: lhs.children.first.to_s,
    suggested_var: suggested_var,
    method: method_name
  )
  add_offense(lhs, message: msg) do |corrector|
    corrector.replace(lhs.loc.name, "@#{suggested_var}")
  end
end

Private Instance Methods

find_definition(node) click to toggle source
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 242
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 253
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 263
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 238
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 271
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 277
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