# 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.rhs
            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 node.equals_asgn?

          node.each_child_node do |child|
            next if scope_body?(node, child)

            traverse_node(child, &block)
          end
        end

        # An assignment inside a block or method body within the condition belongs to
        # that inner scope rather than the condition itself, so it is not inspected.
        def scope_body?(node, child)
          node.type?(:any_block, :any_def) && child == node.body
        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
