rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/lint/literal_assignment_in_condition.rb

Summary

Maintainability
A
45 mins
Test Coverage
A
100%
# frozen_string_literal: true

module RuboCop
  module Cop
    module Lint
      # Checks for literal assignments in the conditions of `if`, `while`, and `until`.
      # It emulates the following Ruby warning:
      #
      # [source,console]
      # ----
      # $ ruby -we 'if x = true; end'
      # -e:1: warning: found `= literal' in conditional, should be ==
      # ----
      #
      # As a lint cop, it cannot be determined if `==` is appropriate as intended,
      # therefore this cop does not provide autocorrection.
      #
      # @example
      #
      #   # bad
      #   if x = 42
      #     do_something
      #   end
      #
      #   # good
      #   if x == 42
      #     do_something
      #   end
      #
      #   # good
      #   if x = y
      #     do_something
      #   end
      #
      class LiteralAssignmentInCondition < Base
        MSG = "Don't use literal assignment `= %<literal>s` in conditional, " \
              'should be `==` or non-literal operand.'

        def on_if(node)
          traverse_node(node.condition) do |asgn_node|
            next unless asgn_node.loc.operator

            rhs = asgn_node.to_a.last
            next if !all_literals?(rhs) || parallel_assignment_with_splat_operator?(rhs)

            range = offense_range(asgn_node, rhs)

            add_offense(range, message: format(MSG, literal: rhs.source))
          end
        end
        alias on_while on_if
        alias on_until on_if

        private

        def traverse_node(node, &block)
          yield node if AST::Node::EQUALS_ASSIGNMENTS.include?(node.type)

          node.each_child_node { |child| traverse_node(child, &block) }
        end

        def all_literals?(node)
          case node.type
          when :dstr, :xstr
            false
          when :array
            node.values.all? { |value| all_literals?(value) }
          when :hash
            (node.values + node.keys).all? { |item| all_literals?(item) }
          else
            node.respond_to?(:literal?) && node.literal?
          end
        end

        def parallel_assignment_with_splat_operator?(node)
          node.array_type? && node.values.first&.splat_type?
        end

        def offense_range(asgn_node, rhs)
          asgn_node.loc.operator.join(rhs.source_range.end)
        end
      end
    end
  end
end