# frozen_string_literal: true module RuboCop module Cop module RSpec # Checks if an example group does not include any tests. # # @example usage # # # bad # describe Bacon do # let(:bacon) { Bacon.new(chunkiness) } # let(:chunkiness) { false } # # context 'extra chunky' do # flagged by rubocop # let(:chunkiness) { true } # end # # it 'is chunky' do # expect(bacon.chunky?).to be_truthy # end # end # # # good # describe Bacon do # let(:bacon) { Bacon.new(chunkiness) } # let(:chunkiness) { false } # # it 'is chunky' do # expect(bacon.chunky?).to be_truthy # end # end # # # good # describe Bacon do # pending 'will add tests later' # end class EmptyExampleGroup < Base MSG = 'Empty example group detected.' # @!method example_group_body(node) # Match example group blocks and yield their body # # @example source that matches # describe 'example group' do # it { is_expected.to be } # end # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] example group body def_node_matcher :example_group_body, <<~PATTERN (block #{send_pattern('#ExampleGroups.all')} args $_) PATTERN # @!method example_or_group_or_include?(node) # Match examples, example groups and includes # # @example source that matches # it { is_expected.to fly } # describe('non-empty example groups too') { } # it_behaves_like 'an animal' # it_behaves_like('a cat') { let(:food) { 'milk' } } # it_has_root_access # skip # it 'will be implemented later' # # @param node [RuboCop::AST::Node] # @return [Array] matching nodes def_node_matcher :example_or_group_or_include?, <<~PATTERN { #{block_pattern( '{#Examples.all #ExampleGroups.all #Includes.all}' )} #{send_pattern('{#Examples.all #Includes.all}')} } PATTERN # @!method examples_inside_block?(node) # Match examples defined inside a block which is not a hook # # @example source that matches # %w(r g b).each do |color| # it { is_expected.to have_color(color) } # end # # @example source that does not match # before do # it { is_expected.to fall_into_oblivion } # end # # @param node [RuboCop::AST::Node] # @return [Array] matching nodes def_node_matcher :examples_inside_block?, <<~PATTERN (block !#{send_pattern('#Hooks.all')} _ #examples?) PATTERN # @!method examples_directly_or_in_block?(node) # Match examples or examples inside blocks # # @example source that matches # it { expect(drink).to be_cold } # context('when winter') { it { expect(drink).to be_hot } } # (1..5).each { |divisor| it { is_expected.to divide_by(divisor) } } # # @param node [RuboCop::AST::Node] # @return [Array] matching nodes def_node_matcher :examples_directly_or_in_block?, <<~PATTERN { #example_or_group_or_include? #examples_inside_block? } PATTERN # @!method examples?(node) # Matches examples defined in scopes where they could run # # @example source that matches # it { expect(myself).to be_run } # describe { it { i_run_as_well } } # # @example source that does not match # before { it { whatever here wont run anyway } } # # @param node [RuboCop::AST::Node] # @return [Array] matching nodes def_node_matcher :examples?, <<~PATTERN { #examples_directly_or_in_block? (begin <#examples_directly_or_in_block? ...>) } PATTERN def on_block(node) return if node.each_ancestor(:def, :defs).any? return if node.each_ancestor(:block).any? { |block| example?(block) } example_group_body(node) do |body| add_offense(node.send_node) if offensive?(body) end end private def offensive?(body) return true unless body return false if conditionals_with_examples?(body) if body.if_type? || body.case_type? !examples_in_branches?(body) else !examples?(body) end end def conditionals_with_examples?(body) return unless body.begin_type? || body.case_type? body.each_descendant(:if, :case).any? do |condition_node| examples_in_branches?(condition_node) end end def examples_in_branches?(condition_node) condition_node.branches.any? { |branch| examples?(branch) } end end end end end