# encoding: utf-8
# frozen_string_literal: true
require 'tsort'
module RuboCop
module Cop
module Style
# Checks for simple usages of parallel assignment.
# This will only complain when the number of variables
# being assigned matched the number of assigning variables.
#
# @example
# # bad
# a, b, c = 1, 2, 3
# a, b, c = [1, 2, 3]
#
# # good
# one, two = *foo
# a, b = foo()
# a, b = b, a
#
# a = 1
# b = 2
# c = 3
class ParallelAssignment < Cop
include IfNode
MSG = 'Do not use parallel assignment.'.freeze
def on_masgn(node)
left, right = *node
left_elements = *left
right_elements = [*right].compact # edge case for one constant
# only complain when the number of variables matches
return if left_elements.size != right_elements.size
# account for edge cases using one variable with a comma
return if left_elements.size == 1
# account for edge case of Constant::CONSTANT
return unless right.array_type?
# allow mass assignment as the return of a method call
return if right.block_type? || right.send_type?
# allow mass assignment when using splat
return if (left_elements + right_elements).any?(&:splat_type?)
order = find_valid_order(left_elements, right_elements)
# For `a, b = b, a` or similar, there is no valid order
return if order.nil?
add_offense(node, :expression)
end
def autocorrect(node)
lambda do |corrector|
left, right = *node
left_elements = *left
right_elements = [*right].compact
order = find_valid_order(left_elements, right_elements)
assignment_corrector =
if modifier_statement?(node.parent)
ModifierCorrector.new(node, config, order)
elsif rescue_modifier?(node.parent)
RescueCorrector.new(node, config, order)
else
GenericCorrector.new(node, config, order)
end
corrector.replace(assignment_corrector.correction_range,
assignment_corrector.correction)
end
end
private
def find_valid_order(left_elements, right_elements)
# arrange left_elements in an order such that no corresponding right
# element refers to a left element earlier in the sequence
# this can be done using an algorithm called a "topological sort"
# fortunately for us, Ruby's stdlib contains an implementation
assignments = left_elements.zip(right_elements)
begin
AssignmentSorter.new(assignments).tsort
rescue TSort::Cyclic
nil
end
end
# Helper class necessitated by silly design of TSort prior to Ruby 2.1
# Newer versions have a better API, but that doesn't help us
class AssignmentSorter
include TSort
extend RuboCop::NodePattern::Macros
def_node_matcher :var_name, '{(casgn _ $_) (_ $_)}'
def_node_search :uses_var?, '{({lvar ivar cvar gvar} %) (const _ %)}'
def_node_search :matching_calls, '(send %1 %2 $...)'
def initialize(assignments)
@assignments = assignments
end
def tsort_each_node
@assignments.each { |a| yield a }
end
def tsort_each_child(assignment)
# yield all the assignments which must come after `assignment`
# (due to dependencies on the previous value of the assigned var)
my_lhs, _my_rhs = *assignment
@assignments.each do |other|
_other_lhs, other_rhs = *other
if ((var = var_name(my_lhs)) && uses_var?(other_rhs, var)) ||
(my_lhs.asgn_method_call? && accesses?(other_rhs, my_lhs))
yield other
end
end
end
# `lhs` is an assignment method call like `obj.attr=` or `ary[idx]=`.
# Does `rhs` access the same value which is assigned by `lhs`?
def accesses?(rhs, lhs)
if lhs.method_name == :[]=
matching_calls(rhs, lhs.receiver, :[]).any? do |args|
args == lhs.method_args
end
else
access_method = lhs.method_name.to_s.chop.to_sym
matching_calls(rhs, lhs.receiver, access_method).any?
end
end
end
def modifier_statement?(node)
node &&
((node.if_type? && modifier_if?(node)) ||
((node.while_type? || node.until_type?) && modifier_while?(node)))
end
def modifier_while?(node)
node.loc.respond_to?(:keyword) &&
%w(while until).include?(node.loc.keyword.source) &&
node.loc.respond_to?(:end) && node.loc.end.nil?
end
def rescue_modifier?(node)
node &&
node.rescue_type? &&
(node.parent.nil? || !node.parent.kwbegin_type?)
end
# An internal class for correcting parallel assignment
class GenericCorrector
include AutocorrectAlignment
attr_reader :config, :node, :correction, :correction_range
def initialize(node, config, new_elements)
@node = node
@config = config
@new_elements = new_elements
end
def correction
assignment.join("\n#{offset(node)}")
end
def correction_range
node.source_range
end
protected
def assignment
@new_elements.map do |lhs, rhs|
"#{lhs.source} = #{rhs.source}"
end
end
private
def extract_sources(node)
node.children.map(&:source)
end
def cop_config
@config.for_cop('Style/ParallelAssignment')
end
end
# An internal class for correcting parallel assignment
# protected by rescue
class RescueCorrector < GenericCorrector
def correction
_node, rescue_clause = *node.parent
_, _, rescue_result = *rescue_clause
"begin\n" \
"#{indentation(node)}" \
"#{assignment.join("\n#{indentation(node)}")}" \
"\n#{offset(node)}rescue\n" \
"#{indentation(node)}#{rescue_result.source}" \
"\n#{offset(node)}end"
end
def correction_range
node.parent.source_range
end
end
# An internal class for correcting parallel assignment
# guarded by if, unless, while, or until
class ModifierCorrector < GenericCorrector
def correction
parent = node.parent
modifier_range =
Parser::Source::Range.new(parent.source_range.source_buffer,
parent.loc.keyword.begin_pos,
parent.source_range.end_pos)
"#{modifier_range.source}\n" \
"#{indentation(node)}" \
"#{assignment.join("\n#{indentation(node)}")}" \
"\n#{offset(node)}end"
end
def correction_range
node.parent.source_range
end
end
end
end
end
end