lib/rubocop/cop/style/symbol_proc.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Use symbols as procs when possible.
      #
      # @example
      #   # bad
      #   something.map { |s| s.upcase }
      #
      #   # good
      #   something.map(&:upcase)
      class SymbolProc < Cop
        MSG = 'Pass `&:%s` as an argument to `%s` instead of a block.'.freeze
        SUPER_TYPES = %i[super zsuper].freeze

        def_node_matcher :proc_node?, '(send (const nil :Proc) :new)'
        def_node_matcher :symbol_proc?, <<-PATTERN
          (block
            ${(send ...) (super ...) zsuper}
            $(args (arg _))
            $(send lvar $_))
        PATTERN

        def self.autocorrect_incompatible_with
          [Layout::SpaceBeforeBlockBraces]
        end

        def on_block(node)
          symbol_proc?(node) do |send_or_super, block_args, block_body, method|
            block_method_name = resolve_block_method_name(send_or_super)

            # TODO: Rails-specific handling that we should probably make
            # configurable - https://github.com/bbatsov/rubocop/issues/1485
            # we should ignore lambdas & procs
            return if proc_node?(send_or_super)
            return if %i[lambda proc].include?(block_method_name)
            return if ignored_method?(block_method_name)
            return unless can_shorten?(block_args, block_body)

            offense(node, method, block_method_name)
          end
        end

        def autocorrect(node)
          lambda do |corrector|
            block_send_or_super, _block_args, block_body = *node
            _receiver, method_name, _args = *block_body

            if super?(block_send_or_super)
              args = *block_send_or_super
            else
              _breceiver, _bmethod_name, *args = *block_send_or_super
            end
            autocorrect_method(corrector, node, args, method_name)
          end
        end

        private

        def resolve_block_method_name(block_send_or_super)
          return :super if super?(block_send_or_super)

          _receiver, method_name, _args = *block_send_or_super
          method_name
        end

        def offense(node, method_name, block_method_name)
          block_start = node.loc.begin.begin_pos
          block_end = node.loc.end.end_pos
          range = range_between(block_start, block_end)

          add_offense(node,
                      range,
                      format(MSG,
                             method_name,
                             block_method_name))
        end

        def autocorrect_method(corrector, node, args, method_name)
          if args.empty?
            autocorrect_no_args(corrector, node, method_name)
          else
            autocorrect_with_args(corrector, node, args, method_name)
          end
        end

        def autocorrect_no_args(corrector, node, method_name)
          corrector.replace(block_range_with_space(node), "(&:#{method_name})")
        end

        def autocorrect_with_args(corrector, node, args, method_name)
          arg_range = args.last.source_range
          arg_range = range_with_surrounding_comma(arg_range, :right)
          replacement = " &:#{method_name}"
          replacement = ',' + replacement unless arg_range.source.end_with?(',')
          corrector.insert_after(arg_range, replacement)
          corrector.remove(block_range_with_space(node))
        end

        def block_range_with_space(node)
          block_range = range_between(begin_pos_for_replacement(node),
                                      node.loc.end.end_pos)
          range_with_surrounding_space(block_range, :left)
        end

        def begin_pos_for_replacement(node)
          block_send_or_super, _block_args, _block_body = *node
          expr = block_send_or_super.source_range

          if (paren_pos = (expr.source =~ /\(\s*\)$/))
            expr.begin_pos + paren_pos
          else
            node.loc.begin.begin_pos
          end
        end

        def ignored_methods
          cop_config['IgnoredMethods']
        end

        def ignored_method?(name)
          ignored_methods.include?(name.to_s)
        end

        def can_shorten?(block_args, block_body)
          argument_matches_receiver?(block_args, block_body)
        end

        # TODO: This might be clearer as a node matcher with unification
        def argument_matches_receiver?(block_args, block_body)
          receiver, = *block_body

          block_arg_name, = *block_args.children.first
          receiver_name, = *receiver

          block_arg_name == receiver_name
        end

        def super?(node)
          SUPER_TYPES.include?(node.type)
        end
      end
    end
  end
end