lib/rubocop/cop/style/unless_logical_operators.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for the use of logical operators in an `unless` condition.
# It discourages such code, as the condition becomes more difficult
# to read and understand.
#
# This cop supports two styles:
#
# - `forbid_mixed_logical_operators` (default)
# - `forbid_logical_operators`
#
# `forbid_mixed_logical_operators` style forbids the use of more than one type
# of logical operators. This makes the `unless` condition easier to read
# because either all conditions need to be met or any condition need to be met
# in order for the expression to be truthy or falsey.
#
# `forbid_logical_operators` style forbids any use of logical operator.
# This makes it even more easy to read the `unless` condition as
# there is only one condition in the expression.
#
# @example EnforcedStyle: forbid_mixed_logical_operators (default)
# # bad
# return unless a || b && c
# return unless a && b || c
# return unless a && b and c
# return unless a || b or c
# return unless a && b or c
# return unless a || b and c
#
# # good
# return unless a && b && c
# return unless a || b || c
# return unless a and b and c
# return unless a or b or c
# return unless a?
#
# @example EnforcedStyle: forbid_logical_operators
# # bad
# return unless a || b
# return unless a && b
# return unless a or b
# return unless a and b
#
# # good
# return unless a
# return unless a?
class UnlessLogicalOperators < Base
include ConfigurableEnforcedStyle
FORBID_MIXED_LOGICAL_OPERATORS = 'Do not use mixed logical operators in an `unless`.'
FORBID_LOGICAL_OPERATORS = 'Do not use any logical operator in an `unless`.'
# @!method or_with_and?(node)
def_node_matcher :or_with_and?, <<~PATTERN
(if (or <`and ...> ) ...)
PATTERN
# @!method and_with_or?(node)
def_node_matcher :and_with_or?, <<~PATTERN
(if (and <`or ...> ) ...)
PATTERN
# @!method logical_operator?(node)
def_node_matcher :logical_operator?, <<~PATTERN
(if ({and or} ... ) ...)
PATTERN
def on_if(node)
return unless node.unless?
if style == :forbid_mixed_logical_operators && mixed_logical_operator?(node)
add_offense(node, message: FORBID_MIXED_LOGICAL_OPERATORS)
elsif style == :forbid_logical_operators && logical_operator?(node)
add_offense(node, message: FORBID_LOGICAL_OPERATORS)
end
end
private
def mixed_logical_operator?(node)
or_with_and?(node) ||
and_with_or?(node) ||
mixed_precedence_and?(node) ||
mixed_precedence_or?(node)
end
def mixed_precedence_and?(node)
and_sources = node.condition.each_descendant(:and).map(&:operator)
and_sources << node.condition.operator if node.condition.and_type?
!(and_sources.all?('&&') || and_sources.all?('and'))
end
def mixed_precedence_or?(node)
or_sources = node.condition.each_descendant(:or).map(&:operator)
or_sources << node.condition.operator if node.condition.or_type?
!(or_sources.all?('||') || or_sources.all?('or'))
end
end
end
end
end