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

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
ConfigurableEnforcedStyle
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 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 AutoCorrector

support_autocorrect?

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

#active_support_extensions_enabled?, #add_global_offense, #add_offense, autocorrect_incompatible_with, badge, #begin_investigation, 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, #inspect, 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?, #autocorrect_with_disable_uncorrectable?, #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



197
198
199
200
201
202
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 197

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

#method_definition?(node) ⇒ Object



160
161
162
163
164
165
166
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 160

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)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rubocop/cop/naming/memoized_instance_variable_name.rb', line 205

def on_defined?(node)
  arg = node.arguments.first
  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) ⇒ Object

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



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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