class RuboCop::Cop::Lint::UnmodifiedReduceAccumulator

Looks for ‘reduce` or `inject` blocks where the value returned (implicitly or explicitly) does not include the accumulator. A block is considered valid as long as at least one return value includes the accumulator.

If the accumulator is not included in the return value, then the entire block will just return a transformation of the last element value, and could be rewritten as such without a loop.

Also catches instances where an index of the accumulator is returned, as this may change the type of object being retained.

NOTE: For the purpose of reducing false positives, this cop only flags returns in ‘reduce` blocks where the element is the only variable in the expression (since we will not be able to tell what other variables relate to via static analysis).

@example

# bad
(1..4).reduce(0) do |acc, el|
  el * 2
end

# bad, may raise a NoMethodError after the first iteration
%w(a b c).reduce({}) do |acc, letter|
  acc[letter] = true
end

# good
(1..4).reduce(0) do |acc, el|
  acc + el * 2
end

# good, element is returned but modified using the accumulator
values.reduce do |acc, el|
  el << acc
  el
end

# good, returns the accumulator instead of the index
%w(a b c).reduce({}) do |acc, letter|
  acc[letter] = true
  acc
end

# good, at least one branch returns the accumulator
values.reduce(nil) do |result, value|
  break result if something?
  value
end

# good, recursive
keys.reduce(self) { |result, key| result[key] }

# ignored as the return value cannot be determined
enum.reduce do |acc, el|
  x = foo(acc, el)
  bar(x)
end

Constants

MSG
MSG_INDEX

Public Instance Methods

on_block(node) click to toggle source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 115
def on_block(node)
  return unless node.body
  return unless reduce_with_block?(node)
  return unless node.argument_list.length >= 2

  check_return_values(node)
end
Also aliased as: on_numblock
on_numblock(node)
Alias for: on_block

Private Instance Methods

acceptable_return?(return_val, element_name) click to toggle source

Determine if a return value is acceptable for the purposes of this cop If it is an expression containing the accumulator, it is acceptable Otherwise, it is only unacceptable if it contains the iterated element, since we otherwise do not have enough information to prevent false positives.

# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 191
def acceptable_return?(return_val, element_name)
  vars = expression_values(return_val).uniq
  return true if vars.none? || (vars - [element_name]).any?

  false
end
allowed_type?(parent_node) click to toggle source

Exclude ‘begin` nodes inside a `dstr` from being collected by `return_values`

# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 199
def allowed_type?(parent_node)
  !parent_node.dstr_type?
end
block_arg_name(node, index) click to toggle source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 159
def block_arg_name(node, index)
  node.argument_list[index].name
end
check_return_values(block_node) click to toggle source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 142
def check_return_values(block_node)
  return_values = return_values(block_node.body)
  accumulator_name = block_arg_name(block_node, 0)
  element_name = block_arg_name(block_node, 1)
  message_opts = { method: block_node.method_name, accum: accumulator_name }

  if (node = returned_accumulator_index(return_values, accumulator_name, element_name))
    add_offense(node, message: format(MSG_INDEX, message_opts))
  elsif potential_offense?(return_values, block_node.body, element_name, accumulator_name)
    return_values.each do |return_val|
      unless acceptable_return?(return_val, element_name)
        add_offense(return_val, message: format(MSG, message_opts))
      end
    end
  end
end
potential_offense?(return_values, block_body, element_name, accumulator_name) click to toggle source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 176
def potential_offense?(return_values, block_body, element_name, accumulator_name)
  !(element_modified?(block_body, element_name) ||
    returns_accumulator_anywhere?(return_values, accumulator_name))
end
return_values(block_body_node) click to toggle source

Return values in a block are either the value given to next, the last line of a multiline block, or the only line of the block

# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 128
def return_values(block_body_node)
  nodes = [block_body_node.begin_type? ? block_body_node.child_nodes.last : block_body_node]

  block_body_node.each_descendant(:next, :break) do |n|
    # Ignore `next`/`break` inside an inner block
    next if n.each_ancestor(:block).first != block_body_node.parent
    next unless n.first_argument

    nodes << n.first_argument
  end

  nodes
end
returned_accumulator_index(return_values, accumulator_name, element_name) click to toggle source

Look for an index of the accumulator being returned, except where the index is the element. This is always an offense, in order to try to catch potential exceptions due to type mismatches

# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 167
def returned_accumulator_index(return_values, accumulator_name, element_name)
  return_values.detect do |val|
    next unless accumulator_index?(val, accumulator_name)
    next true if val.method?(:[]=)

    val.arguments.none? { |arg| lvar_used?(arg, element_name) }
  end
end
returns_accumulator_anywhere?(return_values, accumulator_name) click to toggle source

If the accumulator is used in any return value, the node is acceptable since the accumulator has a chance to change each iteration

# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 183
def returns_accumulator_anywhere?(return_values, accumulator_name)
  return_values.any? { |node| lvar_used?(node, accumulator_name) }
end