# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # This cop checks for non-nil checks, which are usually redundant.
      #
      # With `IncludeSemanticChanges` set to `false` by default, this cop
      # does not report offenses for `!x.nil?` and does no changes that might
      # change behavior.
      #
      # With `IncludeSemanticChanges` set to `true`, this cop reports offenses
      # for `!x.nil?` and autocorrects that and `x != nil` to solely `x`, which
      # is *usually* OK, but might change behavior.
      #
      # @example
      #   # bad
      #   if x != nil
      #   end
      #
      #   # good
      #   if x
      #   end
      #
      #   # Non-nil checks are allowed if they are the final nodes of predicate.
      #   # good
      #   def signed_in?
      #     !current_user.nil?
      #   end
      #
      # @example IncludeSemanticChanges: false (default)
      #   # good
      #   if !x.nil?
      #   end
      #
      # @example IncludeSemanticChanges: true
      #   # bad
      #   if !x.nil?
      #   end
      #
      class NonNilCheck < Base
        extend AutoCorrector

        RESTRICT_ON_SEND = %i[!= nil? !].freeze

        def_node_matcher :not_equal_to_nil?, '(send _ :!= nil)'
        def_node_matcher :unless_check?, '(if (send _ :nil?) ...)'
        def_node_matcher :nil_check?, '(send _ :nil?)'
        def_node_matcher :not_and_nil_check?, '(send (send _ :nil?) :!)'

        def on_send(node)
          return if ignored_node?(node)
          return unless (offense_node = find_offense_node(node))

          message = message(node)
          add_offense(offense_node, message: message) do |corrector|
            autocorrect(corrector, node)
          end
        end

        def on_def(node)
          body = node.body

          return unless node.predicate_method? && body

          if body.begin_type?
            ignore_node(body.children.last)
          else
            ignore_node(body)
          end
        end
        alias on_defs on_def

        private

        def find_offense_node(node)
          if not_equal_to_nil?(node)
            node.loc.selector
          elsif include_semantic_changes? &&
                (not_and_nil_check?(node) || unless_and_nil_check?(node))
            node
          end
        end

        def autocorrect(corrector, node)
          case node.method_name
          when :!=
            autocorrect_comparison(corrector, node)
          when :!
            autocorrect_non_nil(corrector, node, node.receiver)
          when :nil?
            autocorrect_unless_nil(corrector, node, node.receiver)
          end
        end

        def unless_and_nil_check?(send_node)
          parent = send_node.parent

          nil_check?(send_node) && unless_check?(parent) && !parent.ternary? &&
            parent.unless?
        end

        def message(node)
          if node.method?(:!=)
            'Prefer `!expression.nil?` over `expression != nil`.'
          else
            'Explicit non-nil checks are usually redundant.'
          end
        end

        def include_semantic_changes?
          cop_config['IncludeSemanticChanges']
        end

        def autocorrect_comparison(corrector, node)
          expr = node.source

          new_code = if include_semantic_changes?
                       expr.sub(/\s*!=\s*nil/, '')
                     else
                       expr.sub(/^(\S*)\s*!=\s*nil/, '!\1.nil?')
                     end

          return if expr == new_code

          corrector.replace(node, new_code)
        end

        def autocorrect_non_nil(corrector, node, inner_node)
          if inner_node.receiver
            corrector.replace(node, inner_node.receiver.source)
          else
            corrector.replace(node, 'self')
          end
        end

        def autocorrect_unless_nil(corrector, node, receiver)
          corrector.replace(node.parent.loc.keyword, 'if')
          corrector.replace(node, receiver.source)
        end
      end
    end
  end
end
