class RuboCop::Cop::Style::InvertibleUnlessCondition
foo if !condition
# good (if)
foo if x == y && x.odd?
# good
foo unless x != y || x.even?
# bad (complex condition)
foo if even?
foo if x.odd?
foo if x < 10
foo if x == y
foo if bar
# good
foo unless odd?
foo unless x.even?
foo unless x >= 10
foo unless x != y
foo unless !bar
# bad (simple condition)
@example
actually inverse of each other.
and its inverse method are both defined on receiver, and also are
This cop is unsafe because it cannot be guaranteed that the method
@safety
will suggest both ‘even?` and `odd?` to be inverted, but only `!=` (and not `==`).
—-
:odd?: :even?
:even?: :odd?
:!=: :==
For example,
the relationship of inverse methods needs to be defined in both directions.
Methods that can be inverted should be defined in `InverseMethods`. Note that
is disabled by default.
Code without `unless` is easier to read, but that is subjective, so this cop
Checks for usages of `unless` which can be replaced by `if` with inverted condition.
def autocorrect(corrector, node)
def autocorrect(corrector, node) case node.type when :begin autocorrect(corrector, node.children.first) when :send autocorrect_send_node(corrector, node) when :or, :and corrector.replace(node.loc.operator, node.inverse_operator) autocorrect(corrector, node.lhs) autocorrect(corrector, node.rhs) end end
def autocorrect_send_node(corrector, node)
def autocorrect_send_node(corrector, node) if node.method?(:!) corrector.remove(node.loc.selector) else corrector.replace(node.loc.selector, inverse_methods[node.method_name]) end end
def inheritance_check?(node)
def inheritance_check?(node) argument = node.first_argument node.method?(:<) && argument.const_type? && argument.short_name.to_s.upcase != argument.short_name.to_s end
def inverse_methods
def inverse_methods @inverse_methods ||= cop_config['InverseMethods'] end
def invertible?(node) # rubocop:disable Metrics/CyclomaticComplexity
def invertible?(node) # rubocop:disable Metrics/CyclomaticComplexity case node&.type when :begin invertible?(node.children.first) when :send return false if inheritance_check?(node) node.method?(:!) || inverse_methods.key?(node.method_name) when :or, :and invertible?(node.lhs) && invertible?(node.rhs) else false end end
def on_if(node)
def on_if(node) return unless node.unless? condition = node.condition return unless invertible?(condition) message = format(MSG, prefer: "#{node.inverse_keyword} #{preferred_condition(condition)}", current: "#{node.keyword} #{condition.source}") add_offense(node, message: message) do |corrector| corrector.replace(node.loc.keyword, node.inverse_keyword) autocorrect(corrector, condition) end end
def preferred_condition(node)
def preferred_condition(node) case node.type when :begin then "(#{preferred_condition(node.children.first)})" when :send then preferred_send_condition(node) when :or, :and then preferred_logical_condition(node) end end
def preferred_logical_condition(node)
def preferred_logical_condition(node) preferred_lhs = preferred_condition(node.lhs) preferred_rhs = preferred_condition(node.rhs) "#{preferred_lhs} #{node.inverse_operator} #{preferred_rhs}" end
def preferred_send_condition(node) # rubocop:disable Metrics/CyclomaticComplexity
def preferred_send_condition(node) # rubocop:disable Metrics/CyclomaticComplexity receiver_source = node.receiver&.source return receiver_source if node.method?(:!) # receiver may be implicit (self) dotted_receiver_source = receiver_source ? "#{receiver_source}." : '' inverse_method_name = inverse_methods[node.method_name] return "#{dotted_receiver_source}#{inverse_method_name}" unless node.arguments? argument_list = node.arguments.map(&:source).join(', ') if node.operator_method? return "#{receiver_source} #{inverse_method_name} #{argument_list}" end if node.parenthesized? return "#{dotted_receiver_source}#{inverse_method_name}(#{argument_list})" end "#{dotted_receiver_source}#{inverse_method_name} #{argument_list}" end