lib/rubocop/cop/performance/detect.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Performance
      # Identifies usages of `first`, `last`, `[0]` or `[-1]`
      # chained to `select`, `find_all` or `filter` and change them to use
      # `detect` instead.
      #
      # @safety
      #   This cop is unsafe because it assumes that the receiver is an
      #   `Array` or equivalent, but can't reliably detect it. For example,
      #   if the receiver is a `Hash`, it may report a false positive.
      #
      # @example
      #   # bad
      #   [].select { |item| true }.first
      #   [].select { |item| true }.last
      #   [].find_all { |item| true }.first
      #   [].find_all { |item| true }.last
      #   [].filter { |item| true }.first
      #   [].filter { |item| true }.last
      #   [].filter { |item| true }[0]
      #   [].filter { |item| true }[-1]
      #
      #   # good
      #   [].detect { |item| true }
      #   [].reverse.detect { |item| true }
      #
      class Detect < Base
        extend AutoCorrector

        CANDIDATE_METHODS = Set[:select, :find_all, :filter].freeze

        MSG = 'Use `%<prefer>s` instead of `%<first_method>s.%<second_method>s`.'
        REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of `%<first_method>s.%<second_method>s`.'
        INDEX_MSG = 'Use `%<prefer>s` instead of `%<first_method>s[%<index>i]`.'
        INDEX_REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of `%<first_method>s[%<index>i]`.'
        RESTRICT_ON_SEND = %i[first last []].freeze

        def_node_matcher :detect_candidate?, <<~PATTERN
          {
            (send $(block (call _ %CANDIDATE_METHODS) ...) ${:first :last} $...)
            (send $(block (send _ %CANDIDATE_METHODS) ...) $:[] (int ${0 -1}))
            (send $(call _ %CANDIDATE_METHODS ...) ${:first :last} $...)
            (send $(send _ %CANDIDATE_METHODS ...) $:[] (int ${0 -1}))
          }
        PATTERN

        def on_send(node)
          detect_candidate?(node) do |receiver, second_method, args|
            if second_method == :[]
              index = args
              args = {}
            end

            return unless args.empty?
            return unless receiver

            receiver, _args, body = *receiver if receiver.block_type?
            return if accept_first_call?(receiver, body)

            register_offense(node, receiver, second_method, index)
          end
        end
        alias on_csend on_send

        private

        def accept_first_call?(receiver, body)
          caller, _first_method, args = *receiver

          # check that we have usual block or block pass
          return true if body.nil? && (args.nil? || !args.block_pass_type?)

          lazy?(caller)
        end

        def register_offense(node, receiver, second_method, index)
          _caller, first_method, _args = *receiver
          range = receiver.loc.selector.join(node.loc.selector)

          message = message_for_method(second_method, index)
          formatted_message = format(message, prefer: preferred_method,
                                              first_method: first_method,
                                              second_method: second_method,
                                              index: index)

          add_offense(range, message: formatted_message) do |corrector|
            autocorrect(corrector, node, replacement(second_method, index))
          end
        end

        def replacement(method, index)
          if method == :last || (method == :[] && index == -1)
            "reverse.#{preferred_method}"
          else
            preferred_method
          end
        end

        def autocorrect(corrector, node, replacement)
          receiver, _first_method = *node

          first_range = receiver.source_range.end.join(node.loc.selector)

          receiver, _args, _body = *receiver if receiver.block_type?

          corrector.remove(first_range)
          corrector.replace(receiver.loc.selector, replacement)
        end

        def message_for_method(method, index)
          case method
          when :[]
            index == -1 ? INDEX_REVERSE_MSG : INDEX_MSG
          when :last
            REVERSE_MSG
          else
            MSG
          end
        end

        def preferred_method
          config.for_cop('Style/CollectionMethods')['PreferredMethods']['detect'] || 'detect'
        end

        def lazy?(node)
          return false unless node

          receiver, method, _args = *node
          method == :lazy && !receiver.nil?
        end
      end
    end
  end
end