rubocop-hq/rubocop

View on GitHub
lib/rubocop/cop/internal_affairs/example_description.rb

Summary

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

module RuboCop
  module Cop
    module InternalAffairs
      # Checks that RSpec examples that use `expects_offense`
      # or `expects_no_offenses` do not have conflicting
      # descriptions.
      #
      # @example
      #   # bad
      #   it 'does not register an offense' do
      #     expect_offense('...')
      #   end
      #
      #   it 'registers an offense' do
      #     expect_no_offenses('...')
      #   end
      #
      #   # good
      #   it 'registers an offense' do
      #     expect_offense('...')
      #   end
      #
      #   it 'does not register an offense' do
      #     expect_no_offenses('...')
      #   end
      class ExampleDescription < Base
        extend AutoCorrector

        MSG = 'Description does not match use of `%<method_name>s`.'

        RESTRICT_ON_SEND = %i[
          expect_offense
          expect_no_offenses
          expect_correction
          expect_no_corrections
        ].to_set.freeze

        EXPECT_NO_OFFENSES_DESCRIPTION_MAPPING = {
          /\A(adds|registers|reports|finds) (an? )?offense/ => 'does not register an offense',
          /\A(flags|handles|works)\b/ => 'does not register'
        }.freeze

        EXPECT_OFFENSE_DESCRIPTION_MAPPING = {
          /\A(does not|doesn't) (register|find|flag|report)/ => 'registers',
          /\A(does not|doesn't) add (a|an|any )?offense/ => 'registers an offense',
          /\Aregisters no offense/ => 'registers an offense',
          /\A(accepts|register)\b/ => 'registers'
        }.freeze

        EXPECT_NO_CORRECTIONS_DESCRIPTION_MAPPING = {
          /\A(auto[- ]?)?correct/ => 'does not correct'
        }.freeze

        EXPECT_CORRECTION_DESCRIPTION_MAPPING = {
          /\b(does not|doesn't) (auto[- ]?)?correct/ => 'autocorrects'
        }.freeze

        EXAMPLE_DESCRIPTION_MAPPING = {
          expect_no_offenses: EXPECT_NO_OFFENSES_DESCRIPTION_MAPPING,
          expect_offense: EXPECT_OFFENSE_DESCRIPTION_MAPPING,
          expect_no_corrections: EXPECT_NO_CORRECTIONS_DESCRIPTION_MAPPING,
          expect_correction: EXPECT_CORRECTION_DESCRIPTION_MAPPING
        }.freeze

        # @!method offense_example(node)
        def_node_matcher :offense_example, <<~PATTERN
          (block
            (send _ {:it :specify} $...)
            _args
            `(send nil? %RESTRICT_ON_SEND ...)
          )
        PATTERN

        def on_send(node)
          parent = node.each_ancestor(:block).first
          return unless parent && (current_description = offense_example(parent)&.first)

          method_name = node.method_name
          message = format(MSG, method_name: method_name)

          description_map = EXAMPLE_DESCRIPTION_MAPPING[method_name]
          check_description(current_description, description_map, message)
        end

        private

        def check_description(current_description, description_map, message)
          description_text = string_contents(current_description)
          return unless (new_description = correct_description(description_text, description_map))

          add_offense(current_description, message: message) do |corrector|
            corrector.replace(current_description, "'#{new_description}'")
          end
        end

        def correct_description(current_description, description_map)
          description_map.each do |incorrect_description_pattern, preferred_description|
            if incorrect_description_pattern.match?(current_description)
              return current_description.gsub(incorrect_description_pattern, preferred_description)
            end
          end

          nil
        end

        def string_contents(node)
          node.str_type? ? node.value : node.source
        end
      end
    end
  end
end