lib/rubocop/cop/performance/string_replacement.rb



# encoding: utf-8
# frozen_string_literal: true

module RuboCop
  module Cop
    module Performance
      # This cop identifies places where `gsub` can be replaced by
      # `tr` or `delete`.
      #
      # @example
      #   @bad
      #   'abc'.gsub('b', 'd')
      #   'abc'.gsub('a', '')
      #   'abc'.gsub(/a/, 'd')
      #   'abc'.gsub!('a', 'd')
      #
      #   @good
      #   'abc'.gsub(/.*/, 'a')
      #   'abc'.gsub(/a+/, 'd')
      #   'abc'.tr('b', 'd')
      #   'a b c'.delete(' ')
      class StringReplacement < Cop
        MSG = 'Use `%s` instead of `%s`.'.freeze
        DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/
        REGEXP_CONSTRUCTOR_METHODS = [:new, :compile].freeze
        GSUB_METHODS = [:gsub, :gsub!].freeze
        DETERMINISTIC_TYPES = [:regexp, :str, :send].freeze
        DELETE = 'delete'.freeze
        TR = 'tr'.freeze
        BANG = '!'.freeze
        SINGLE_QUOTE = "'".freeze

        def on_send(node)
          _string, method, first_param, second_param = *node
          return unless GSUB_METHODS.include?(method)
          return unless string?(second_param)
          return unless DETERMINISTIC_TYPES.include?(first_param.type)

          first_source, options = first_source(first_param)
          second_source, = *second_param

          return if first_source.nil?

          if regex?(first_param)
            return unless first_source =~ DETERMINISTIC_REGEX
            return if options
            # This must be done after checking DETERMINISTIC_REGEX
            # Otherwise things like \s will trip us up
            first_source = interpret_string_escapes(first_source)
          end

          return if first_source.length != 1
          return unless second_source.length <= 1

          message = message(method, first_source, second_source)
          add_offense(node, range(node), message)
        end

        def autocorrect(node)
          _string, method, first_param, second_param = *node
          first_source, = first_source(first_param)
          second_source, = *second_param

          if regex?(first_param)
            first_source = interpret_string_escapes(first_source)
          end

          replacement_method = replacement_method(method,
                                                  first_source,
                                                  second_source)

          lambda do |corrector|
            corrector.replace(node.loc.selector, replacement_method)
            unless first_param.str_type?
              corrector.replace(first_param.source_range,
                                to_string_literal(first_source))
            end

            if second_source.empty? && first_source.length == 1
              remove_second_param(corrector, node, first_param)
            end
          end
        end

        private

        def string?(node)
          node && node.str_type?
        end

        def first_source(first_param)
          case first_param.type
          when :regexp, :send
            return nil unless regex?(first_param)
            source, options = extract_source(first_param)
          when :str
            source, = *first_param
          end

          [source, options]
        end

        def extract_source(node)
          case node.type
          when :regexp
            source_from_regex_literal(node)
          when :send
            source_from_regex_constructor(node)
          end
        end

        def source_from_regex_literal(node)
          regex, options = *node
          source, = *regex
          options, = *options
          [source, options]
        end

        def source_from_regex_constructor(node)
          _const, _init, regex = *node
          case regex.type
          when :regexp
            source_from_regex_literal(regex)
          when :str
            source, = *regex
            source
          end
        end

        def regex?(node)
          return true if node.regexp_type?

          const, init, = *node
          _, klass = *const

          klass == :Regexp && REGEXP_CONSTRUCTOR_METHODS.include?(init)
        end

        def range(node)
          Parser::Source::Range.new(node.source_range.source_buffer,
                                    node.loc.selector.begin_pos,
                                    node.source_range.end_pos)
        end

        def replacement_method(method, first_source, second_source)
          replacement = if second_source.empty? && first_source.length == 1
                          DELETE
                        else
                          TR
                        end

          "#{replacement}#{BANG if bang_method?(method)}"
        end

        def message(method, first_source, second_source)
          replacement_method = replacement_method(method,
                                                  first_source,
                                                  second_source)

          format(MSG, replacement_method, method)
        end

        def bang_method?(method)
          method.to_s.end_with?(BANG)
        end

        def method_suffix(node)
          node.loc.end ? node.loc.end.source : ''
        end

        def remove_second_param(corrector, node, first_param)
          end_range =
            Parser::Source::Range.new(node.source_range.source_buffer,
                                      first_param.source_range.end_pos,
                                      node.source_range.end_pos)

          corrector.replace(end_range, method_suffix(node))
        end
      end
    end
  end
end