# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
module Rails
# Check if using Minitest-like matchers.
#
# Check the use of minitest-like matchers
# starting with `assert_` or `refute_`.
#
# @example
# # bad
# assert_equal(a, b)
# assert_equal a, b, "must be equal"
# assert_not_includes a, b
# refute_equal(a, b)
# assert_nil a
# refute_empty(b)
# assert_true(a)
# assert_false(a)
#
# # good
# expect(b).to eq(a)
# expect(b).to(eq(a), "must be equal")
# expect(a).not_to include(b)
# expect(b).not_to eq(a)
# expect(a).to eq(nil)
# expect(a).not_to be_empty
# expect(a).to be(true)
# expect(a).to be(false)
#
class MinitestAssertions < Base
extend AutoCorrector
# :nodoc:
class BasicAssertion
extend NodePattern::Macros
attr_reader :expected, :actual, :failure_message
def self.minitest_assertion
raise NotImplementedError
end
def initialize(expected, actual, failure_message)
@expected = expected&.source
@actual = actual.source
@failure_message = failure_message&.source
end
def replaced(node)
runner = negated?(node) ? 'not_to' : 'to'
if failure_message.nil?
"expect(#{actual}).#{runner} #{assertion}"
else
"expect(#{actual}).#{runner}(#{assertion}, #{failure_message})"
end
end
def negated?(node)
node.method_name.start_with?('assert_not_', 'refute_')
end
def assertion
raise NotImplementedError
end
end
# :nodoc:
class EqualAssertion < BasicAssertion
MATCHERS = %i[
assert_equal
assert_not_equal
refute_equal
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_equal :assert_not_equal :refute_equal} $_ $_ $_?)
PATTERN
def self.match(expected, actual, failure_message)
new(expected, actual, failure_message.first)
end
def assertion
"eq(#{expected})"
end
end
# :nodoc:
class KindOfAssertion < BasicAssertion
MATCHERS = %i[
assert_kind_of
assert_not_kind_of
refute_kind_of
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_kind_of :assert_not_kind_of :refute_kind_of} $_ $_ $_?)
PATTERN
def self.match(expected, actual, failure_message)
new(expected, actual, failure_message.first)
end
def assertion
"be_a_kind_of(#{expected})"
end
end
# :nodoc:
class InstanceOfAssertion < BasicAssertion
MATCHERS = %i[
assert_instance_of
assert_not_instance_of
refute_instance_of
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_instance_of :assert_not_instance_of :refute_instance_of} $_ $_ $_?)
PATTERN
def self.match(expected, actual, failure_message)
new(expected, actual, failure_message.first)
end
def assertion
"be_an_instance_of(#{expected})"
end
end
# :nodoc:
class IncludesAssertion < BasicAssertion
MATCHERS = %i[
assert_includes
assert_not_includes
refute_includes
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_includes :assert_not_includes :refute_includes} $_ $_ $_?)
PATTERN
def self.match(collection, expected, failure_message)
new(expected, collection, failure_message.first)
end
def assertion
"include(#{expected})"
end
end
# :nodoc:
class InDeltaAssertion < BasicAssertion
MATCHERS = %i[
assert_in_delta
assert_not_in_delta
refute_in_delta
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_in_delta :assert_not_in_delta :refute_in_delta} $_ $_ $_? $_?)
PATTERN
def self.match(expected, actual, delta, failure_message)
new(expected, actual, delta.first, failure_message.first)
end
def initialize(expected, actual, delta, fail_message)
super(expected, actual, fail_message)
@delta = delta&.source || '0.001'
end
def assertion
"be_within(#{@delta}).of(#{expected})"
end
end
# :nodoc:
class PredicateAssertion < BasicAssertion
MATCHERS = %i[
assert_predicate
assert_not_predicate
refute_predicate
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_predicate :assert_not_predicate :refute_predicate} $_ ${sym} $_?)
PATTERN
def self.match(subject, predicate, failure_message)
return nil unless predicate.value.end_with?('?')
new(predicate, subject, failure_message.first)
end
def assertion
"be_#{expected.delete_prefix(':').delete_suffix('?')}"
end
end
# :nodoc:
class MatchAssertion < BasicAssertion
MATCHERS = %i[
assert_match
refute_match
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_match :refute_match} $_ $_ $_?)
PATTERN
def self.match(matcher, actual, failure_message)
new(matcher, actual, failure_message.first)
end
def assertion
"match(#{expected})"
end
end
# :nodoc:
class NilAssertion < BasicAssertion
MATCHERS = %i[
assert_nil
assert_not_nil
refute_nil
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_nil :assert_not_nil :refute_nil} $_ $_?)
PATTERN
def self.match(actual, failure_message)
new(nil, actual, failure_message.first)
end
def assertion
'eq(nil)'
end
end
# :nodoc:
class EmptyAssertion < BasicAssertion
MATCHERS = %i[
assert_empty
assert_not_empty
refute_empty
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_empty :assert_not_empty :refute_empty} $_ $_?)
PATTERN
def self.match(actual, failure_message)
new(nil, actual, failure_message.first)
end
def assertion
'be_empty'
end
end
# :nodoc:
class TrueAssertion < BasicAssertion
MATCHERS = %i[
assert_true
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_true} $_ $_?)
PATTERN
def self.match(actual, failure_message)
new(nil, actual, failure_message.first)
end
def assertion
'be(true)'
end
end
# :nodoc:
class FalseAssertion < BasicAssertion
MATCHERS = %i[
assert_false
].freeze
# @!method self.minitest_assertion(node)
def_node_matcher 'self.minitest_assertion', <<~PATTERN # rubocop:disable InternalAffairs/NodeMatcherDirective
(send nil? {:assert_false} $_ $_?)
PATTERN
def self.match(actual, failure_message)
new(nil, actual, failure_message.first)
end
def assertion
'be(false)'
end
end
MSG = 'Use `%<prefer>s`.'
# TODO: replace with `BasicAssertion.subclasses` in Ruby 3.1+
ASSERTION_MATCHERS = constants(false).filter_map do |c|
const = const_get(c)
const if const.is_a?(Class) && const.superclass == BasicAssertion
end
RESTRICT_ON_SEND = ASSERTION_MATCHERS.flat_map { |m| m::MATCHERS }
def on_send(node)
ASSERTION_MATCHERS.each do |m|
m.minitest_assertion(node) do |*args|
assertion = m.match(*args)
next if assertion.nil?
on_assertion(node, assertion)
end
end
end
def on_assertion(node, assertion)
preferred = assertion.replaced(node)
add_offense(node, message: message(preferred)) do |corrector|
corrector.replace(node, preferred)
end
end
def message(preferred)
format(MSG, prefer: preferred)
end
end
end
end
end
end