# frozen_string_literal: true module RuboCop module Cop module RSpec # Checks that message expectations do not have a configured response. # # @example # # bad # expect(foo).to receive(:bar).with(42).and_return("hello world") # # # good (without spies) # allow(foo).to receive(:bar).with(42).and_return("hello world") # expect(foo).to receive(:bar).with(42) # class StubbedMock < Base MSG = 'Prefer `%s` over `%s` when ' \ 'configuring a response.' # @!method message_expectation?(node) # Match message expectation matcher # # @example source that matches # receive(:foo) # # @example source that matches # receive_message_chain(:foo, :bar) # # @example source that matches # receive(:foo).with('bar') # # @param node [RuboCop::AST::Node] # @return [Array] matching nodes def_node_matcher :message_expectation?, <<-PATTERN { (send nil? { :receive :receive_message_chain } ...) # receive(:foo) (send (send nil? :receive ...) :with ...) # receive(:foo).with('bar') } PATTERN # @!method configured_response?(node) def_node_matcher :configured_response?, <<~PATTERN { :and_return :and_raise :and_throw :and_yield :and_call_original :and_wrap_original } PATTERN # @!method expectation(node) # Match expectation # # @example source that matches # is_expected.to be_in_the_bar # # @example source that matches # expect(cocktail).to contain_exactly(:fresh_orange_juice, :campari) # # @example source that matches # expect_any_instance_of(Officer).to be_alert # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] expectation, method name, matcher def_node_matcher :expectation, <<~PATTERN (send $(send nil? $#Expectations.all ...) :to $_) PATTERN # @!method matcher_with_configured_response(node) # Match matcher with a configured response # # @example source that matches # receive(:foo).and_return('bar') # # @example source that matches # receive(:lower).and_raise(SomeError) # # @example source that matches # receive(:redirect).and_call_original # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] matcher def_node_matcher :matcher_with_configured_response, <<~PATTERN (send #message_expectation? #configured_response? _) PATTERN # @!method matcher_with_return_block(node) # Match matcher with a return block # # @example source that matches # receive(:foo) { 'bar' } # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] matcher def_node_matcher :matcher_with_return_block, <<~PATTERN (block #message_expectation? (args) _) # receive(:foo) { 'bar' } PATTERN # @!method matcher_with_hash(node) # Match matcher with a configured response defined as a hash # # @example source that matches # receive_messages(foo: 'bar', baz: 'qux') # # @example source that matches # receive_message_chain(:foo, bar: 'baz') # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] matcher def_node_matcher :matcher_with_hash, <<~PATTERN { (send nil? :receive_messages hash) # receive_messages(foo: 'bar', baz: 'qux') (send nil? :receive_message_chain ... hash) # receive_message_chain(:foo, bar: 'baz') } PATTERN # @!method matcher_with_blockpass(node) # Match matcher with a configured response in block-pass # # @example source that matches # receive(:foo, &canned) # # @example source that matches # receive_message_chain(:foo, :bar, &canned) # # @example source that matches # receive(:foo).with('bar', &canned) # # @param node [RuboCop::AST::Node] # @yield [RuboCop::AST::Node] matcher def_node_matcher :matcher_with_blockpass, <<~PATTERN { (send nil? { :receive :receive_message_chain } ... block_pass) # receive(:foo, &canned) (send (send nil? :receive ...) :with ... block_pass) # receive(:foo).with('foo', &canned) } PATTERN RESTRICT_ON_SEND = %i[to].freeze def on_send(node) expectation(node, &method(:on_expectation)) end private def on_expectation(expectation, method_name, matcher) flag_expectation = lambda do add_offense(expectation, message: msg(method_name)) end matcher_with_configured_response(matcher, &flag_expectation) matcher_with_return_block(matcher, &flag_expectation) matcher_with_hash(matcher, &flag_expectation) matcher_with_blockpass(matcher, &flag_expectation) end def msg(method_name) format(MSG, method_name: method_name, replacement: replacement(method_name)) end def replacement(method_name) case method_name when :expect :allow when :is_expected 'allow(subject)' when :expect_any_instance_of :allow_any_instance_of end end end end end end