lib/rubocop/cop/style/swap_values.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Enforces the use of shorthand-style swapping of 2 variables.
#
# @safety
# Autocorrection is unsafe, because the temporary variable used to
# swap variables will be removed, but may be referred to elsewhere.
#
# @example
# # bad
# tmp = x
# x = y
# y = tmp
#
# # good
# x, y = y, x
#
class SwapValues < Base
include RangeHelp
extend AutoCorrector
MSG = 'Replace this and assignments at lines %<x_line>d ' \
'and %<y_line>d with `%<replacement>s`.'
SIMPLE_ASSIGNMENT_TYPES = %i[lvasgn ivasgn cvasgn gvasgn casgn].to_set.freeze
def on_asgn(node)
return if allowed_assignment?(node)
tmp_assign = node
x_assign, y_assign = *node.right_siblings.take(2)
return unless x_assign && y_assign && swapping_values?(tmp_assign, x_assign, y_assign)
add_offense(node, message: message(x_assign, y_assign)) do |corrector|
range = correction_range(tmp_assign, y_assign)
corrector.replace(range, replacement(x_assign))
end
end
SIMPLE_ASSIGNMENT_TYPES.each { |asgn_type| alias_method :"on_#{asgn_type}", :on_asgn }
private
def allowed_assignment?(node)
node.parent&.mlhs_type? || node.parent&.shorthand_asgn?
end
def swapping_values?(tmp_assign, x_assign, y_assign)
simple_assignment?(tmp_assign) &&
simple_assignment?(x_assign) &&
simple_assignment?(y_assign) &&
lhs(x_assign) == rhs(tmp_assign) &&
lhs(y_assign) == rhs(x_assign) &&
rhs(y_assign) == lhs(tmp_assign)
end
def simple_assignment?(node)
return false unless node.respond_to?(:type)
SIMPLE_ASSIGNMENT_TYPES.include?(node.type)
end
def message(x_assign, y_assign)
format(
MSG,
x_line: x_assign.first_line,
y_line: y_assign.first_line,
replacement: replacement(x_assign)
)
end
def replacement(x_assign)
x = lhs(x_assign)
y = rhs(x_assign)
"#{x}, #{y} = #{y}, #{x}"
end
def lhs(node)
case node.type
when :casgn
namespace, name, = *node
if namespace
"#{namespace.const_name}::#{name}"
else
name.to_s
end
else
node.children[0].to_s
end
end
def rhs(node)
case node.type
when :casgn
node.children[2].source
else
node.children[1].source
end
end
def correction_range(tmp_assign, y_assign)
range_by_whole_lines(
range_between(tmp_assign.source_range.begin_pos, y_assign.source_range.end_pos)
)
end
end
end
end
end