# frozen_string_literal: true module RuboCop module Cop module RSpec # A helper for `inflected` style module InflectedHelper include RuboCop::RSpec::Language extend NodePattern::Macros MSG_INFLECTED = 'Prefer using `%s` matcher over ' \ '`%s`.' private def check_inflected(node) predicate_in_actual?(node) do |predicate, to, matcher| msg = message_inflected(predicate) add_offense(node, message: msg) 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_node_matcher :predicate_in_actual?, <<-PATTERN (send (send nil? :expect { (block $(send !nil? #predicate? ...) ...) $(send !nil? #predicate? ...)}) $#{Runners::ALL.node_pattern_union} $#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 remove_predicate(corrector, predicate) range = predicate.loc.dot.with( end_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 module ExplicitHelper include RuboCop::RSpec::Language extend NodePattern::Macros MSG_EXPLICIT = 'Prefer using `%s` over ' \ '`%s` matcher.' BUILT_IN_MATCHERS = %w[ be_truthy be_falsey be_falsy have_attributes have_received be_between be_within ].freeze private def allowed_explicit_matchers cop_config.fetch('AllowedExplicitMatchers', []) + BUILT_IN_MATCHERS end def check_explicit(node) # rubocop:disable Metrics/MethodLength predicate_matcher_block?(node) do |actual, matcher| add_offense(node, message: message_explicit(matcher)) do |corrector| to_node = node.send_node corrector_explicit(corrector, to_node, actual, matcher, to_node) end ignore_node(node.children.first) return end return if part_of_ignored_node?(node) predicate_matcher?(node) do |actual, matcher| add_offense(node, message: message_explicit(matcher)) do |corrector| corrector_explicit(corrector, node, actual, matcher, matcher) end end end def_node_matcher :predicate_matcher?, <<-PATTERN (send (send nil? :expect $!nil?) #{Runners::ALL.node_pattern_union} {$(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?) #{Runners::ALL.node_pattern_union} $(send nil? #predicate_matcher_name?)) ...) PATTERN def predicate_matcher_name?(name) name = name.to_s return false if allowed_explicit_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 corrector_explicit(corrector, to_node, actual, matcher, block_child) 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 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 # 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.something?).to be(true) # # @example Strict: false, EnforcedStyle: inflected # # bad # expect(foo.something?).to be_truthy # expect(foo.something?).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 extend AutoCorrector include ConfigurableEnforcedStyle include InflectedHelper include ExplicitHelper 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) send_node.loc.selector.end.with( 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? send_node.loc.expression.end.with( end_pos: parent.loc.expression.end_pos ) end end end end end