rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/style/redundant_percent_q.rb

Summary

Maintainability
A
35 mins
Test Coverage
A
100%
# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Checks for usage of the %q/%Q syntax when '' or "" would do.
      #
      # @example
      #
      #   # bad
      #   name = %q(Bruce Wayne)
      #   time = %q(8 o'clock)
      #   question = %q("What did you say?")
      #
      #   # good
      #   name = 'Bruce Wayne'
      #   time = "8 o'clock"
      #   question = '"What did you say?"'
      #
      class RedundantPercentQ < Base
        extend AutoCorrector

        MSG = 'Use `%<q_type>s` only for strings that contain both ' \
              'single quotes and double quotes%<extra>s.'
        DYNAMIC_MSG = ', or for dynamic strings that contain double quotes'
        SINGLE_QUOTE = "'"
        QUOTE = '"'
        EMPTY = ''
        PERCENT_Q = '%q'
        PERCENT_CAPITAL_Q = '%Q'
        STRING_INTERPOLATION_REGEXP = /#\{.+\}/.freeze
        ESCAPED_NON_BACKSLASH = /\\[^\\]/.freeze

        def on_dstr(node)
          return unless string_literal?(node)

          check(node)
        end

        def on_str(node)
          # Interpolated strings that contain more than just interpolation
          # will call `on_dstr` for the entire string and `on_str` for the
          # non interpolated portion of the string
          return unless string_literal?(node)

          check(node)
        end

        private

        def check(node)
          return unless start_with_percent_q_variant?(node)
          return if interpolated_quotes?(node) || allowed_percent_q?(node)

          add_offense(node) do |corrector|
            delimiter = /\A%Q[^"]+\z|'/.match?(node.source) ? QUOTE : SINGLE_QUOTE

            corrector.replace(node.loc.begin, delimiter)
            corrector.replace(node.loc.end, delimiter)
          end
        end

        def interpolated_quotes?(node)
          node.source.include?(SINGLE_QUOTE) && node.source.include?(QUOTE)
        end

        def allowed_percent_q?(node)
          (node.source.start_with?(PERCENT_Q) && acceptable_q?(node)) ||
            (node.source.start_with?(PERCENT_CAPITAL_Q) && acceptable_capital_q?(node))
        end

        def message(node)
          src = node.source
          extra = if src.start_with?(PERCENT_CAPITAL_Q)
                    DYNAMIC_MSG
                  else
                    EMPTY
                  end
          format(MSG, q_type: src[0, 2], extra: extra)
        end

        def string_literal?(node)
          node.loc.respond_to?(:begin) && node.loc.respond_to?(:end) &&
            node.loc.begin && node.loc.end
        end

        def start_with_percent_q_variant?(string)
          string.source.start_with?(PERCENT_Q, PERCENT_CAPITAL_Q)
        end

        def acceptable_q?(node)
          src = node.source

          return true if STRING_INTERPOLATION_REGEXP.match?(src)

          src.scan(/\\./).any?(ESCAPED_NON_BACKSLASH)
        end

        def acceptable_capital_q?(node)
          src = node.source
          src.include?(QUOTE) &&
            (STRING_INTERPOLATION_REGEXP.match?(src) ||
            (node.str_type? && double_quotes_required?(src)))
        end
      end
    end
  end
end