123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Naming::MemoizedInstanceVariableName

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/naming/memoized_instance_variable_name.rb

Overview

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.

Examples:

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

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

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

Cop Safety Information:

  • 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.

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

SYMBOL_TO_STRING_CACHE

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?, #target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#defined_memoized?(node, ivar)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 198

def_node_matcher :defined_memoized?, <<~PATTERN
  (begin
    (if (defined $(ivar %1)) (return $(ivar %1)) nil?)
    ...
    $(ivasgn %1 _))
PATTERN

#find_definition(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 243

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) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 254

def matches?(method_name, ivar_assign)
  return true if ivar_assign.nil? || INITIALIZE_METHODS.include?(method_name)

  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) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 264

def message(variable)
  variable_name = variable.to_s.sub('@', '')

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

  MSG
end

#method_definition?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 161

def_node_matcher :method_definition?, <<~PATTERN
  ${
    (block (send _ %DYNAMIC_DEFINE_METHODS ({sym str} $_)) ...)
    (def $_ ...)
    (defs _ $_ ...)
  }
PATTERN

#on_defined?(node) ⇒ Boolean

Metrics/MethodLength

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 206

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)

rubocop:disable Metrics/MethodLength

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 171

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

#style_parameter_name (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 239

def style_parameter_name
  'EnforcedStyleForLeadingUnderscores'
end

#suggested_var(method_name) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 272

def suggested_var(method_name)
  suggestion = method_name.to_s.delete('!?=')

  style == :required ? "_#{suggestion}" : suggestion
end

#variable_name_candidates(method_name) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 278

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