# frozen_string_literal: true module RuboCop module Cop module Performance # This cop is used to identify usages of `first`, `last`, `[0]` or `[-1]` # chained to `select`, `find_all` or `filter` and change them to use # `detect` instead. # # @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 } # # `ActiveRecord` compatibility: # `ActiveRecord` does not implement a `detect` method and `find` has its # own meaning. Correcting ActiveRecord methods with this cop should be # considered unsafe. class Detect < Base extend AutoCorrector CANDIDATE_METHODS = Set[:select, :find_all, :filter].freeze MSG = 'Use `%s` instead of `%s.%s`.' REVERSE_MSG = 'Use `reverse.%s` instead of `%s.%s`.' INDEX_MSG = 'Use `%s` instead of `%s[%i]`.' INDEX_REVERSE_MSG = 'Use `reverse.%s` instead of `%s[%i]`.' RESTRICT_ON_SEND = %i[first last []].freeze def_node_matcher :detect_candidate?, <<~PATTERN { (send $(block (send _ %CANDIDATE_METHODS) ...) ${:first :last} $...) (send $(block (send _ %CANDIDATE_METHODS) ...) $:[] (int ${0 -1})) (send $(send _ %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 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