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
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
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
# 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
# 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
# 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
rubocop:enable Metrics/AbcSize, Metrics/MethodLength
# File lib/rubocop/cop/naming/memoized_instance_variable_name.rb, line 223 def style_parameter_name 'EnforcedStyleForLeadingUnderscores' end
# 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
# 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