lib/rubocop/cop/rspec/repeated_include_example.rb



# frozen_string_literal: true

module RuboCop
  module Cop
    module RSpec
      # Check for repeated include of shared examples.
      #
      # @example
      #
      #    # bad
      #    describe 'foo' do
      #      include_examples 'cool stuff'
      #      include_examples 'cool stuff'
      #    end
      #
      #    # bad
      #    describe 'foo' do
      #      it_behaves_like 'a cool', 'thing'
      #      it_behaves_like 'a cool', 'thing'
      #    end
      #
      #    # bad
      #    context 'foo' do
      #      it_should_behave_like 'a duck'
      #      it_should_behave_like 'a duck'
      #    end
      #
      #    # good
      #    describe 'foo' do
      #      include_examples 'cool stuff'
      #    end
      #
      #    describe 'bar' do
      #      include_examples 'cool stuff'
      #    end
      #
      #    # good
      #    describe 'foo' do
      #      it_behaves_like 'a cool', 'thing'
      #      it_behaves_like 'a cool', 'person'
      #    end
      #
      #    # good
      #    context 'foo' do
      #      it_should_behave_like 'a duck'
      #      it_should_behave_like 'a goose'
      #    end
      #
      class RepeatedIncludeExample < Base
        MSG = 'Repeated include of shared_examples %<name>s ' \
          'on line(s) %<repeat>s'

        def_node_matcher :several_include_examples?, <<-PATTERN
          (begin <#include_examples? #include_examples? ...>)
        PATTERN

        def_node_matcher :include_examples?,
                         send_pattern('#Includes.examples')

        def_node_matcher :shared_examples_name, <<-PATTERN
          (send _ #Includes.examples $_ ...)
        PATTERN

        def on_begin(node)
          return unless several_include_examples?(node)

          repeated_include_examples(node).each do |item, repeats|
            add_offense(item, message: message(item, repeats))
          end
        end

        private

        def repeated_include_examples(node)
          node
            .children
            .select { |child| literal_include_examples?(child) }
            .group_by { |child| signature_keys(child) }
            .values
            .reject(&:one?)
            .flat_map { |items| add_repeated_lines(items) }
        end

        def literal_include_examples?(node)
          include_examples?(node) &&
            node.arguments.all?(&:recursive_literal_or_const?)
        end

        def add_repeated_lines(items)
          repeated_lines = items.map(&:first_line)
          items.map { |item| [item, repeated_lines - [item.first_line]] }
        end

        def signature_keys(item)
          item.arguments
        end

        def message(item, repeats)
          format(MSG, name: shared_examples_name(item).source, repeat: repeats)
        end
      end
    end
  end
end