Skip to content

Commit 846058a

Browse files
committed
Transform if/unless into ternary
1 parent 9794a06 commit 846058a

File tree

1 file changed

+90
-6
lines changed

1 file changed

+90
-6
lines changed

lib/syntax_tree/node.rb

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4671,6 +4671,13 @@ def initialize(keyword, node)
46714671
end
46724672

46734673
def format(q)
4674+
# If we can transform this node into a ternary, then we're going to print
4675+
# a special version that uses the ternary operator if it fits on one line.
4676+
if can_ternary?
4677+
format_ternary(q)
4678+
return
4679+
end
4680+
46744681
# If the predicate of the conditional contains an assignment (in which
46754682
# case we can't know for certain that that assignment doesn't impact the
46764683
# statements inside the conditional) then we can't use the modifier form
@@ -4680,12 +4687,7 @@ def format(q)
46804687
return
46814688
end
46824689

4683-
if contains_conditional?
4684-
format_break(q, force: true)
4685-
return
4686-
end
4687-
4688-
if node.consequent || node.statements.empty?
4690+
if node.consequent || node.statements.empty? || contains_conditional?
46894691
q.group { format_break(q, force: true) }
46904692
else
46914693
q.group do
@@ -4722,10 +4724,92 @@ def format_break(q, force:)
47224724
q.text("end")
47234725
end
47244726

4727+
def format_ternary(q)
4728+
q.group do
4729+
q.if_break do
4730+
q.text("#{keyword} ")
4731+
q.nest(keyword.length + 1) { q.format(node.predicate) }
4732+
4733+
q.indent do
4734+
q.breakable
4735+
q.format(node.statements)
4736+
end
4737+
4738+
q.breakable
4739+
q.group do
4740+
q.format(node.consequent.keyword)
4741+
q.indent do
4742+
q.breakable
4743+
q.format(node.consequent.statements)
4744+
end
4745+
end
4746+
4747+
q.breakable
4748+
q.text("end")
4749+
end.if_flat do
4750+
Parentheses.flat(q) do
4751+
q.format(node.predicate)
4752+
q.text(" ? ")
4753+
4754+
statements = [node.statements, node.consequent.statements]
4755+
statements.reverse! if keyword == "unless"
4756+
4757+
q.format(statements[0])
4758+
q.text(" : ")
4759+
q.format(statements[1])
4760+
end
4761+
end
4762+
end
4763+
end
4764+
47254765
def contains_conditional?
47264766
node.statements.body.length == 1 &&
47274767
[If, IfMod, IfOp, Unless, UnlessMod].include?(node.statements.body.first.class)
47284768
end
4769+
4770+
# In order for an `if` or `unless` expression to be shortened to a ternary,
4771+
# there has to be one and only one consequent clause which is an Else. Both
4772+
# the body of the main node and the body of the Else node must have only one
4773+
# statement, and that statement must pass the `can_ternary_statements?`
4774+
# check.
4775+
def can_ternary?
4776+
case node
4777+
in { predicate: Assign | Command | CommandCall | MAssign | OpAssign }
4778+
false
4779+
in { consequent: Else[statements:] }
4780+
can_ternary_statements?(statements) &&
4781+
can_ternary_statements?(node.statements)
4782+
else
4783+
false
4784+
end
4785+
end
4786+
4787+
# Certain expressions cannot be reduced to a ternary without adding
4788+
# parentheses around them. In this case we say they cannot be ternaried and
4789+
# default instead to breaking them into multiple lines.
4790+
def can_ternary_statements?(statements)
4791+
return false if statements.body.length != 1
4792+
statement = statements.body.first
4793+
4794+
# This is a list of nodes that should not be allowed to be a part of a
4795+
# ternary clause.
4796+
no_ternary = [
4797+
Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp,
4798+
Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super,
4799+
Undef, Unless, UnlessMod, Until, UntilMod, VarAlias, VoidStmt, While,
4800+
WhileMod, Yield, Yield0, ZSuper
4801+
]
4802+
4803+
# Here we're going to check that the only statement inside the statements
4804+
# node is no a part of our denied list of nodes that can be ternaries.
4805+
#
4806+
# If the user is using one of the lower precedence "and" or "or"
4807+
# operators, then we can't use a ternary expression as it would break the
4808+
# flow control.
4809+
#
4810+
!no_ternary.include?(statement.class) &&
4811+
!(statement.is_a?(Binary) && %i[and or].include?(statement.operator))
4812+
end
47294813
end
47304814

47314815
# If represents the first clause in an +if+ chain.

0 commit comments

Comments
 (0)