lib/rubocop/cop/rspec/predicate_matcher.rb



module RuboCop
  module Cop
    module RSpec
      # A helper for `inflected` style
      module InflectedHelper
        extend NodePattern::Macros

        MSG_INFLECTED = 'Prefer using `%<matcher_name>s` matcher over ' \
                        '`%<predicate_name>s`.'.freeze

        private

        def check_inflected(node)
          predicate_in_actual?(node) do |predicate|
            add_offense(
              node,
              location: :expression,
              message: message_inflected(predicate)
            )
          end
        end

        def_node_matcher :predicate_in_actual?, <<-PATTERN
          (send
            (send nil? :expect {
              (block $(send !nil? #predicate? ...) ...)
              $(send !nil? #predicate? ...)})
            ${:to :not_to :to_not}
            $#boolean_matcher?)
        PATTERN

        def_node_matcher :be_bool?, <<-PATTERN
          (send nil? {:be :eq :eql :equal} {true false})
        PATTERN

        def_node_matcher :be_boolthy?, <<-PATTERN
          (send nil? {:be_truthy :be_falsey :be_falsy :a_truthy_value :a_falsey_value :a_falsy_value})
        PATTERN

        def boolean_matcher?(node)
          if cop_config['Strict']
            be_boolthy?(node)
          else
            be_bool?(node) || be_boolthy?(node)
          end
        end

        def predicate?(sym)
          sym.to_s.end_with?('?')
        end

        def message_inflected(predicate)
          format(MSG_INFLECTED,
                 predicate_name: predicate.method_name,
                 matcher_name: to_predicate_matcher(predicate.method_name))
        end

        # rubocop:disable Metrics/MethodLength
        def to_predicate_matcher(name)
          case name = name.to_s
          when 'is_a?'
            'be_a'
          when 'instance_of?'
            'be_an_instance_of'
          when 'include?', 'respond_to?'
            name[0..-2]
          when /^has_/
            name.sub('has_', 'have_')[0..-2]
          else
            "be_#{name[0..-2]}"
          end
        end
        # rubocop:enable Metrics/MethodLength

        def autocorrect_inflected(node)
          predicate_in_actual?(node) do |predicate, to, matcher|
            lambda do |corrector|
              remove_predicate(corrector, predicate)
              corrector.replace(node.loc.selector,
                                true?(to, matcher) ? 'to' : 'not_to')
              rewrite_matcher(corrector, predicate, matcher)
            end
          end
        end

        def remove_predicate(corrector, predicate)
          range = range_between(
            predicate.loc.dot.begin_pos,
            predicate.loc.expression.end_pos
          )
          corrector.remove(range)

          block_range = block_loc(predicate)
          corrector.remove(block_range) if block_range
        end

        def rewrite_matcher(corrector, predicate, matcher)
          args = args_loc(predicate).source
          block_loc = block_loc(predicate)
          block = block_loc ? block_loc.source : ''

          corrector.replace(
            matcher.loc.expression,
            to_predicate_matcher(predicate.method_name) + args + block
          )
        end

        def true?(to_symbol, matcher)
          result = case matcher.method_name
                   when :be, :eq
                     matcher.first_argument.true_type?
                   when :be_truthy, :a_truthy_value
                     true
                   when :be_falsey, :be_falsy, :a_falsey_value, :a_falsy_value
                     false
                   end
          to_symbol == :to ? result : !result
        end
      end

      # A helper for `explicit` style
      # rubocop:disable Metrics/ModuleLength
      module ExplicitHelper
        extend NodePattern::Macros

        MSG_EXPLICIT = 'Prefer using `%<predicate_name>s` over ' \
                       '`%<matcher_name>s` matcher.'.freeze
        BUILT_IN_MATCHERS = %w[
          be_truthy be_falsey be_falsy
          have_attributes have_received
          be_between be_within
        ].freeze

        private

        def check_explicit(node) # rubocop:disable Metrics/MethodLength
          predicate_matcher_block?(node) do |_actual, matcher|
            add_offense(
              node,
              location: :expression,
              message: message_explicit(matcher)
            )
            ignore_node(node.children.first)
            return
          end

          return if part_of_ignored_node?(node)

          predicate_matcher?(node) do |_actual, matcher|
            add_offense(
              node,
              location: :expression,
              message: message_explicit(matcher)
            )
          end
        end

        def_node_matcher :predicate_matcher?, <<-PATTERN
          (send
            (send nil? :expect $!nil?)
            {:to :not_to :to_not}
            {$(send nil? #predicate_matcher_name? ...)
              (block $(send nil? #predicate_matcher_name? ...) ...)})
        PATTERN

        def_node_matcher :predicate_matcher_block?, <<-PATTERN
          (block
            (send
              (send nil? :expect $!nil?)
              {:to :not_to :to_not}
              $(send nil? #predicate_matcher_name?))
            ...)
        PATTERN

        def predicate_matcher_name?(name)
          name = name.to_s
          name.start_with?('be_', 'have_') &&
            !BUILT_IN_MATCHERS.include?(name) &&
            !name.end_with?('?')
        end

        def message_explicit(matcher)
          format(MSG_EXPLICIT,
                 predicate_name: to_predicate_method(matcher.method_name),
                 matcher_name: matcher.method_name)
        end

        def autocorrect_explicit(node)
          autocorrect_explicit_send(node) ||
            autocorrect_explicit_block(node)
        end

        def autocorrect_explicit_send(node)
          predicate_matcher?(node) do |actual, matcher|
            corrector_explicit(node, actual, matcher, matcher)
          end
        end

        def autocorrect_explicit_block(node)
          predicate_matcher_block?(node) do |actual, matcher|
            to_node = node.send_node
            corrector_explicit(to_node, actual, matcher, to_node)
          end
        end

        def corrector_explicit(to_node, actual, matcher, block_child)
          lambda do |corrector|
            replacement_matcher = replacement_matcher(to_node)
            corrector.replace(matcher.loc.expression, replacement_matcher)
            move_predicate(corrector, actual, matcher, block_child)
            corrector.replace(to_node.loc.selector, 'to')
          end
        end

        def move_predicate(corrector, actual, matcher, block_child)
          predicate = to_predicate_method(matcher.method_name)
          args = args_loc(matcher).source
          block_loc = block_loc(block_child)
          block = block_loc ? block_loc.source : ''

          corrector.remove(block_loc) if block_loc
          corrector.insert_after(actual.loc.expression,
                                 ".#{predicate}" + args + block)
        end

        # rubocop:disable Metrics/MethodLength
        def to_predicate_method(matcher)
          case matcher = matcher.to_s
          when 'be_a', 'be_an', 'be_a_kind_of', 'a_kind_of', 'be_kind_of'
            'is_a?'
          when 'be_an_instance_of', 'be_instance_of', 'an_instance_of'
            'instance_of?'
          when 'include', 'respond_to'
            matcher + '?'
          when /^have_(.+)/
            "has_#{Regexp.last_match(1)}?"
          else
            matcher[/^be_(.+)/, 1] + '?'
          end
        end
        # rubocop:enable Metrics/MethodLength

        def replacement_matcher(node)
          case [cop_config['Strict'], node.method_name == :to]
          when [true, true]
            'be(true)'
          when [true, false]
            'be(false)'
          when [false, true]
            'be_truthy'
          when [false, false]
            'be_falsey'
          end
        end
      end
      # rubocop:enable Metrics/ModuleLength

      # Prefer using predicate matcher over using predicate method directly.
      #
      # RSpec defines magic matchers for predicate methods.
      # This cop recommends to use the predicate matcher instead of using
      # predicate method directly.
      #
      # @example Strict: true, EnforcedStyle: inflected (default)
      #   # bad
      #   expect(foo.something?).to be_truthy
      #
      #   # good
      #   expect(foo).to be_something
      #
      #   # also good - It checks "true" strictly.
      #   expect(foo).to be(true)
      #
      # @example Strict: false, EnforcedStyle: inflected
      #   # bad
      #   expect(foo.something?).to be_truthy
      #   expect(foo).to be(true)
      #
      #   # good
      #   expect(foo).to be_something
      #
      # @example Strict: true, EnforcedStyle: explicit
      #   # bad
      #   expect(foo).to be_something
      #
      #   # good - the above code is rewritten to it by this cop
      #   expect(foo.something?).to be(true)
      #
      # @example Strict: false, EnforcedStyle: explicit
      #   # bad
      #   expect(foo).to be_something
      #
      #   # good - the above code is rewritten to it by this cop
      #   expect(foo.something?).to be_truthy
      class PredicateMatcher < Cop
        include ConfigurableEnforcedStyle
        include InflectedHelper
        include ExplicitHelper
        include RangeHelp

        def on_send(node)
          case style
          when :inflected
            check_inflected(node)
          when :explicit
            check_explicit(node)
          end
        end

        def on_block(node)
          check_explicit(node) if style == :explicit
        end

        def autocorrect(node)
          case style
          when :inflected
            autocorrect_inflected(node)
          when :explicit
            autocorrect_explicit(node)
          end
        end

        private

        # returns args location with whitespace
        # @example
        #   foo 1, 2
        #      ^^^^^
        def args_loc(send_node)
          range_between(send_node.loc.selector.end_pos,
                        send_node.loc.expression.end_pos)
        end

        # returns block location with whitespace
        # @example
        #   foo { bar }
        #      ^^^^^^^^
        def block_loc(send_node)
          parent = send_node.parent
          return unless parent.block_type?

          range_between(
            send_node.loc.expression.end_pos,
            parent.loc.expression.end_pos
          )
        end
      end
    end
  end
end