lib/rubocop/cop/performance/casecmp.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module Performance
      # Identifies places where a case-insensitive string comparison
      # can better be implemented using `casecmp`.
      #
      # This cop is disabled by default because `String#casecmp` only works with
      # ASCII characters. See https://github.com/rubocop/rubocop/issues/9753.
      #
      # If you are working only with ASCII characters, then this cop can be
      # safely enabled.
      #
      # @safety
      #   This cop is unsafe because `String#casecmp` and `String#casecmp?` behave
      #   differently when using Non-ASCII characters.
      #
      # @example
      #   # bad
      #   str.downcase == 'abc'
      #   str.upcase.eql? 'ABC'
      #   'abc' == str.downcase
      #   'ABC'.eql? str.upcase
      #   str.downcase == str.downcase
      #
      #   # good
      #   str.casecmp('ABC').zero?
      #   'abc'.casecmp(str).zero?
      class Casecmp < Base
        extend AutoCorrector

        MSG = 'Use `%<good>s` instead of `%<bad>s`.'
        RESTRICT_ON_SEND = %i[== eql? !=].freeze
        CASE_METHODS = %i[downcase upcase].freeze

        def_node_matcher :downcase_eq, <<~PATTERN
          (send
            $(send _ ${:downcase :upcase})
            ${:== :eql? :!=}
            ${str (send _ {:downcase :upcase} ...) (begin str)})
        PATTERN

        def_node_matcher :eq_downcase, <<~PATTERN
          (send
            {str (send _ {:downcase :upcase} ...) (begin str)}
            ${:== :eql? :!=}
            $(send _ ${:downcase :upcase}))
        PATTERN

        def_node_matcher :downcase_downcase, <<~PATTERN
          (send
            $(send _ ${:downcase :upcase})
            ${:== :eql? :!=}
            $(send _ ${:downcase :upcase}))
        PATTERN

        def on_send(node)
          return unless downcase_eq(node) || eq_downcase(node)
          return unless (parts = take_method_apart(node))

          _receiver, method, arg, variable = parts
          good_method = build_good_method(method, arg, variable)

          message = format(MSG, good: good_method, bad: node.source)
          add_offense(node, message: message) do |corrector|
            autocorrect(corrector, node, good_method)
          end
        end

        private

        def take_method_apart(node)
          if downcase_downcase(node)
            receiver, method, rhs = *node
            arg, = *rhs
          elsif downcase_eq(node)
            receiver, method, arg = *node
          elsif eq_downcase(node)
            arg, method, receiver = *node
          else
            return
          end

          variable, = *receiver

          [receiver, method, arg, variable]
        end

        def autocorrect(corrector, node, replacement)
          corrector.replace(node, replacement)
        end

        def build_good_method(method, arg, variable)
          bang = method == :!= ? '!' : ''

          # We want resulting call to be parenthesized
          # if arg already includes one or more sets of parens, don't add more
          # or if method call already used parens, again, don't add more
          if arg.send_type? || !parentheses?(arg)
            "#{bang}#{variable.source}.casecmp(#{arg.source}).zero?"
          else
            "#{bang}#{variable.source}.casecmp#{arg.source}.zero?"
          end
        end
      end
    end
  end
end