lib/rubocop/cop/rspec/rails/minitest_assertions.rb in rubocop-rspec-2.26.1 vs lib/rubocop/cop/rspec/rails/minitest_assertions.rb in rubocop-rspec-2.27.0

- old
+ new

@@ -2,57 +2,349 @@ module RuboCop module Cop module RSpec module Rails - # Check if using Minitest matchers. + # 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`.' - RESTRICT_ON_SEND = %i[assert_equal refute_equal].freeze - # @!method minitest_assertion(node) - def_node_matcher :minitest_assertion, <<~PATTERN - (send nil? {:assert_equal :refute_equal} $_ $_ $_?) - PATTERN + # 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) - minitest_assertion(node) do |expected, actual, failure_message| - prefer = replacement(node, expected, actual, - failure_message.first) - add_offense(node, message: message(prefer)) do |corrector| - corrector.replace(node, prefer) + 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 - private - - def replacement(node, expected, actual, failure_message) - runner = node.method?(:assert_equal) ? 'to' : 'not_to' - if failure_message.nil? - "expect(#{actual.source}).#{runner} eq(#{expected.source})" - else - "expect(#{actual.source}).#{runner}(eq(#{expected.source}), " \ - "#{failure_message.source})" + 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(prefer) - format(MSG, prefer: prefer) + def message(preferred) + format(MSG, prefer: preferred) end end end end end