lib/rubocop/cop/performance/inefficient_hash_search.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Performance
      # Checks for inefficient searching of keys and values within
      # hashes.
      #
      # `Hash#keys.include?` is less efficient than `Hash#key?` because
      # the former allocates a new array and then performs an O(n) search
      # through that array, while `Hash#key?` does not allocate any array and
      # performs a faster O(1) search for the key.
      #
      # `Hash#values.include?` is less efficient than `Hash#value?`. While they
      # both perform an O(n) search through all of the values, calling `values`
      # allocates a new array while using `value?` does not.
      #
      # @safety
      #   This cop is unsafe because it can't tell whether the receiver is a hash object.
      #
      # @example
      #   # bad
      #   { a: 1, b: 2 }.keys.include?(:a)
      #   { a: 1, b: 2 }.keys.include?(:z)
      #   h = { a: 1, b: 2 }; h.keys.include?(100)
      #
      #   # good
      #   { a: 1, b: 2 }.key?(:a)
      #   { a: 1, b: 2 }.has_key?(:z)
      #   h = { a: 1, b: 2 }; h.key?(100)
      #
      #   # bad
      #   { a: 1, b: 2 }.values.include?(2)
      #   { a: 1, b: 2 }.values.include?('garbage')
      #   h = { a: 1, b: 2 }; h.values.include?(nil)
      #
      #   # good
      #   { a: 1, b: 2 }.value?(2)
      #   { a: 1, b: 2 }.has_value?('garbage')
      #   h = { a: 1, b: 2 }; h.value?(nil)
      #
      class InefficientHashSearch < Base
        extend AutoCorrector

        RESTRICT_ON_SEND = %i[include?].freeze

        def_node_matcher :inefficient_include?, <<~PATTERN
          (call (call $_ {:keys :values}) :include? _)
        PATTERN

        def on_send(node)
          inefficient_include?(node) do |receiver|
            return if receiver.nil?

            message = message(node)
            add_offense(node, message: message) do |corrector|
              # Replace `keys.include?` or `values.include?` with the appropriate
              # `key?`/`value?` method.
              corrector.replace(node, replacement(node))
            end
          end
        end
        alias on_csend on_send

        private

        def message(node)
          "Use `##{correct_method(node)}` instead of `##{current_method(node)}.include?`."
        end

        def replacement(node)
          "#{correct_hash_expression(node)}#{correct_dot(node)}#{correct_method(node)}(#{correct_argument(node)})"
        end

        def correct_method(node)
          case current_method(node)
          when :keys then use_long_method ? 'has_key?' : 'key?'
          when :values then use_long_method ? 'has_value?' : 'value?'
          end
        end

        def current_method(node)
          node.receiver.method_name
        end

        def use_long_method
          preferred_config = config.for_all_cops['Style/PreferredHashMethods']
          preferred_config && preferred_config['EnforcedStyle'] == 'long' && preferred_config['Enabled']
        end

        def correct_argument(node)
          node.first_argument.source
        end

        def correct_hash_expression(node)
          node.receiver.receiver.source
        end

        def correct_dot(node)
          node.receiver.loc.dot.source
        end
      end
    end
  end
end