lib/rubocop/cop/performance/map_method_chain.rb
# frozen_string_literal: true module RuboCop module Cop module Performance # Checks if the map method is used in a chain. # # Autocorrection is not supported because an appropriate block variable name cannot be determined automatically. # # @safety # This cop is unsafe because false positives occur if the number of times the first method is executed # affects the return value of subsequent methods. # # [source,ruby] # ---- # class X # def initialize # @@num = 0 # end # # def foo # @@num += 1 # self # end # # def bar # @@num * 2 # end # end # # [X.new, X.new].map(&:foo).map(&:bar) # => [4, 4] # [X.new, X.new].map { |x| x.foo.bar } # => [2, 4] # ---- # # @example # # # bad # array.map(&:foo).map(&:bar) # # # good # array.map { |item| item.foo.bar } # class MapMethodChain < Base include IgnoredNode MSG = 'Use `%<method_name>s { |x| x.%<map_args>s }` instead of `%<method_name>s` method chain.' RESTRICT_ON_SEND = %i[map collect].freeze def_node_matcher :block_pass_with_symbol_arg?, <<~PATTERN (:block_pass (:sym $_)) PATTERN def on_send(node) return if part_of_ignored_node?(node) return unless (map_arg = block_pass_with_symbol_arg?(node.first_argument)) map_args = [map_arg] return unless (begin_of_chained_map_method = find_begin_of_chained_map_method(node, map_args)) range = begin_of_chained_map_method.loc.selector.begin.join(node.source_range.end) message = format(MSG, method_name: begin_of_chained_map_method.method_name, map_args: map_args.join('.')) add_offense(range, message: message) ignore_node(node) end private # rubocop:disable Metrics/CyclomaticComplexity def find_begin_of_chained_map_method(node, map_args) return unless (chained_map_method = node.receiver) return if !chained_map_method.call_type? || !RESTRICT_ON_SEND.include?(chained_map_method.method_name) return unless (map_arg = block_pass_with_symbol_arg?(chained_map_method.first_argument)) map_args.unshift(map_arg) receiver = chained_map_method.receiver return chained_map_method unless receiver&.call_type? && block_pass_with_symbol_arg?(receiver.first_argument) find_begin_of_chained_map_method(chained_map_method, map_args) end # rubocop:enable Metrics/CyclomaticComplexity end end end end