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
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
Private Instance Methods
Source
# 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
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.
Source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 199 def allowed_type?(parent_node) !parent_node.dstr_type? end
Exclude ‘begin` nodes inside a `dstr` from being collected by `return_values`
Source
# File lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb, line 159 def block_arg_name(node, index) node.argument_list[index].name end
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
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
Source
# 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(:any_block).first != block_body_node.parent next unless n.first_argument nodes << n.first_argument end nodes end
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
Source
# 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
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
Source
# 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
If the accumulator is used in any return value, the node is acceptable since the accumulator has a chance to change each iteration