123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Performance::CollectionLiteralInLoop

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Base
Instance Chain:
self, Base
Inherits: Base
  • Object
Defined in: lib/rubocop/cop/performance/collection_literal_in_loop.rb

Overview

Identifies places where Array and Hash literals are used within loops. It is better to extract them into a local variable or constant to avoid unnecessary allocations on each iteration.

You can set the minimum number of elements to consider an offense with MinSize.

Note
Since Ruby 3.4, certain simple arguments to Array#include? are optimized directly in Ruby. This avoids allocations without changing the code, as such no offense will be registered in those cases. Currently that includes: strings, self, local variables, instance variables, and method calls without arguments. Additionally, any number of methods can be chained: [1, 2, 3].include?(@foo) and [1, 2, 3].include?(@foo.bar.baz) both avoid the array allocation.

Examples:

# bad
users.select do |user|
  %i[superadmin admin].include?(user.role)
end

# good
admin_roles = %i[superadmin admin]
users.select do |user|
  admin_roles.include?(user.role)
end

# good
ADMIN_ROLES = %i[superadmin admin]
...
users.select do |user|
  ADMIN_ROLES.include?(user.role)
end

Constant Summary

Instance Method Summary

Instance Method Details

#check_literal?(node, method, arguments) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 102

def check_literal?(node, method, arguments)
  !node.nil? &&
    nonmutable_method_of_array_or_hash?(node, method) &&
    node.children.size >= min_size &&
    node.recursive_basic_literal? &&
    !optimized_array_include?(node, method, arguments)
end

#enumerable_method?(method_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 161

def enumerable_method?(method_name)
  ENUMERABLE_METHOD_NAMES.include?(method_name)
end

#keyword_loop?(type) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 143

def keyword_loop?(type)
  LOOP_TYPES.include?(type)
end

#literal_class(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 153

def literal_class(node)
  if node.array_type?
    'Array'
  elsif node.hash_type?
    'Hash'
  end
end

#loop?(ancestor, node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 139

def loop?(ancestor, node)
  keyword_loop?(ancestor.type) || kernel_loop?(ancestor) || node_within_enumerable_loop?(node, ancestor)
end

#min_size (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 165

def min_size
  Integer(cop_config['MinSize'] || 1)
end

#node_within_enumerable_loop?(node, ancestor) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 147

def node_within_enumerable_loop?(node, ancestor)
  enumerable_loop?(ancestor) do |receiver|
    receiver != node && !receiver&.descendants&.include?(node)
  end
end

#nonmutable_method_of_array_or_hash?(node, method) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 130

def nonmutable_method_of_array_or_hash?(node, method)
  (node.array_type? && ARRAY_METHODS.include?(method)) ||
    (node.hash_type? && HASH_METHODS.include?(method))
end

#on_send(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 92

def on_send(node)
  receiver, method, *arguments = *node.children
  return unless check_literal?(receiver, method, arguments) && parent_is_loop?(receiver)

  message = format(MSG, literal_class: literal_class(receiver))
  add_offense(receiver, message: message)
end

#optimized_array_include?(node, method, arguments) ⇒ Boolean (private)

Since Ruby 3.4, simple arguments to Array#include? are optimized. See https://github.com/ruby/ruby/pull/12123 for more details.

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 113

def optimized_array_include?(node, method, arguments)
  return false unless target_ruby_version >= 3.4 && node.array_type? && method == :include?
  # Disallow include?(1, 2)
  return false if arguments.count != 1

  arg = arguments.first
  # Allow `include?(foo.bar.baz.bat)`
  while arg.send_type?
    return false if arg.arguments.any? # Disallow include?(foo(bar))
    break unless arg.receiver

    arg = arg.receiver
  end
  ARRAY_INCLUDE_OPTIMIZED_TYPES.include?(arg.type)
end

#parent_is_loop?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/performance/collection_literal_in_loop.rb', line 135

def parent_is_loop?(node)
  node.each_ancestor.any? { |ancestor| loop?(ancestor, node) }
end