lib/rubocop/cop/style/ternary_parentheses.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for the presence of parentheses around ternary
# conditions. It is configurable to enforce inclusion or omission of
# parentheses using `EnforcedStyle`. Omission is only enforced when
# removing the parentheses won't cause a different behavior.
#
# `AllowSafeAssignment` option for safe assignment.
# By safe assignment we mean putting parentheses around
# an assignment to indicate "I know I'm using an assignment
# as a condition. It's not a mistake."
#
# @example EnforcedStyle: require_no_parentheses (default)
# # bad
# foo = (bar?) ? a : b
# foo = (bar.baz?) ? a : b
# foo = (bar && baz) ? a : b
#
# # good
# foo = bar? ? a : b
# foo = bar.baz? ? a : b
# foo = bar && baz ? a : b
#
# @example EnforcedStyle: require_parentheses
# # bad
# foo = bar? ? a : b
# foo = bar.baz? ? a : b
# foo = bar && baz ? a : b
#
# # good
# foo = (bar?) ? a : b
# foo = (bar.baz?) ? a : b
# foo = (bar && baz) ? a : b
#
# @example EnforcedStyle: require_parentheses_when_complex
# # bad
# foo = (bar?) ? a : b
# foo = (bar.baz?) ? a : b
# foo = bar && baz ? a : b
#
# # good
# foo = bar? ? a : b
# foo = bar.baz? ? a : b
# foo = (bar && baz) ? a : b
#
# @example AllowSafeAssignment: true (default)
# # good
# foo = (bar = baz) ? a : b
#
# @example AllowSafeAssignment: false
# # bad
# foo = (bar = baz) ? a : b
#
class TernaryParentheses < Base
include SafeAssignment
include ConfigurableEnforcedStyle
include SurroundingSpace
extend AutoCorrector
VARIABLE_TYPES = AST::Node::VARIABLES
NON_COMPLEX_TYPES = [*VARIABLE_TYPES, :const, :defined?, :yield].freeze
MSG = '%<command>s parentheses for ternary conditions.'
MSG_COMPLEX = '%<command>s parentheses for ternary expressions with complex conditions.'
def on_if(node)
condition = node.condition
return if only_closing_parenthesis_is_last_line?(condition)
return if condition_as_parenthesized_one_line_pattern_matching?(condition)
return unless node.ternary? && offense?(node)
message = message(node)
add_offense(node.source_range, message: message) do |corrector|
autocorrect(corrector, node)
end
end
private
def only_closing_parenthesis_is_last_line?(condition)
condition.source.split("\n").last == ')'
end
def condition_as_parenthesized_one_line_pattern_matching?(condition)
return false unless condition.parenthesized_call?
return false unless (first_child = condition.children.first)
if target_ruby_version >= 3.0
first_child.match_pattern_p_type?
else
first_child.match_pattern_type? # For Ruby 2.7's one line pattern matching AST.
end
end
def autocorrect(corrector, node)
condition = node.condition
return nil if parenthesized?(condition) &&
(safe_assignment?(condition) || unsafe_autocorrect?(condition))
if parenthesized?(condition)
correct_parenthesized(corrector, condition)
else
correct_unparenthesized(corrector, condition)
end
end
def offense?(node)
condition = node.condition
if safe_assignment?(condition)
!safe_assignment_allowed?
else
parens = parenthesized?(condition)
case style
when :require_parentheses_when_complex
complex_condition?(condition) ? !parens : parens
else
require_parentheses? ? !parens : parens
end
end
end
# If the condition is parenthesized we recurse and check for any
# complex expressions within it.
def complex_condition?(condition)
if condition.begin_type?
condition.to_a.any? { |x| complex_condition?(x) }
else
!non_complex_expression?(condition)
end
end
# Anything that is not a variable, constant, or method/.method call
# will be counted as a complex expression.
def non_complex_expression?(condition)
NON_COMPLEX_TYPES.include?(condition.type) || non_complex_send?(condition)
end
def non_complex_send?(node)
return false unless node.call_type?
!node.operator_method? || node.method?(:[])
end
def message(node)
if require_parentheses_when_complex?
command = parenthesized?(node.condition) ? 'Only use' : 'Use'
format(MSG_COMPLEX, command: command)
else
command = require_parentheses? ? 'Use' : 'Omit'
format(MSG, command: command)
end
end
def require_parentheses?
style == :require_parentheses
end
def require_parentheses_when_complex?
style == :require_parentheses_when_complex
end
def parenthesized?(node)
node.begin_type?
end
def unsafe_autocorrect?(condition)
condition.children.any? do |child|
unparenthesized_method_call?(child) || below_ternary_precedence?(child)
end
end
def unparenthesized_method_call?(child)
/^[a-z]/i.match?(method_name(child)) && !child.parenthesized?
end
def below_ternary_precedence?(child)
# Handle English "or", e.g. 'foo or bar ? a : b'
(child.or_type? && child.semantic_operator?) ||
# Handle English "and", e.g. 'foo and bar ? a : b'
(child.and_type? && child.semantic_operator?) ||
# Handle English "not", e.g. 'not foo ? a : b'
(child.send_type? && child.prefix_not?)
end
# @!method method_name(node)
def_node_matcher :method_name, <<~PATTERN
{($:defined? _ ...)
(send {_ nil?} $_ _ ...)}
PATTERN
def correct_parenthesized(corrector, condition)
corrector.remove(condition.loc.begin)
corrector.remove(condition.loc.end)
# Ruby allows no space between the question mark and parentheses.
# If we remove the parentheses, we need to add a space or we'll
# generate invalid code.
corrector.insert_after(condition.loc.end, ' ') unless whitespace_after?(condition)
end
def correct_unparenthesized(corrector, condition)
corrector.wrap(condition, '(', ')')
end
def whitespace_after?(node)
last_token = processed_source.last_token_of(node)
last_token.space_after?
end
end
end
end
end