module RuboCop module Cop module RSpec # A helper for `inflected` style module InflectedHelper extend NodePattern::Macros MSG_INFLECTED = 'Prefer using `%s` matcher over ' \ '`%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 'exist?', 'exists?' 'exist' 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 `%s` over ' \ '`%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 return false if BUILT_IN_MATCHERS.include?(name) name.start_with?('be_', 'have_') && !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