# 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 %d ' \ 'and %d with `%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