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

Inherits:
Base
  • Object
show all
Includes:
ConfigurableEnforcedStyle
Defined in:
lib/rubocop/cop/naming/memoized_instance_variable_name.rb

Overview

This cop 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

Constant Summary collapse

MSG =
'Memoized variable `%<var>s` does not match ' \
'method name `%<method>s`. Use `@%<suggested_var>s` instead.'
UNDERSCORE_REQUIRED =
'Memoized variable `%<var>s` does not start ' \
'with `_`. Use `@%<suggested_var>s` instead.'
DYNAMIC_DEFINE_METHODS =
%i[define_method define_singleton_method].to_set.freeze

Constants inherited from Base

Base::RESTRICT_ON_SEND

Instance Attribute Summary

Attributes inherited from Base

#config, #processed_source

Instance Method Summary collapse

Methods included from ConfigurableEnforcedStyle

#alternative_style, #alternative_styles, #ambiguous_style_detected, #correct_style_detected, #detected_style, #detected_style=, #no_acceptable_style!, #no_acceptable_style?, #opposite_style_detected, #style, #style_configured?, #style_detected, #supported_styles, #unexpected_style_detected

Methods inherited from Base

#add_global_offense, #add_offense, autocorrect_incompatible_with, badge, #callbacks_needed, callbacks_needed, #config_to_allow_offenses, #config_to_allow_offenses=, #cop_config, #cop_name, cop_name, department, documentation_url, exclude_from_registry, #excluded_file?, #external_dependency_checksum, inherited, #initialize, joining_forces, lint?, match?, #offenses, #on_investigation_end, #on_new_investigation, #on_other_file, #parse, #ready, #relevant_file?, support_autocorrect?, support_multiple_source?, #target_rails_version, #target_ruby_version

Methods included from ExcludeLimit

#exclude_limit

Methods included from AutocorrectLogic

#autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #correctable?, #disable_uncorrectable?, #safe_autocorrect?

Methods included from IgnoredNode

#ignore_node, #ignored_node?, #part_of_ignored_node?

Methods included from Util

silence_warnings

Constructor Details

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

Instance Method Details

#defined_memoized?(node, ivar) ⇒ Object


189
190
191
192
193
194
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 189

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

#method_definition?(node) ⇒ Object


157
158
159
160
161
162
163
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 157

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

#on_defined?(node) ⇒ Boolean

rubocop:disable Metrics/AbcSize, Metrics/MethodLength

Returns:

  • (Boolean)

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# 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) ⇒ Object

rubocop:disable Metrics/AbcSize


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# 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