com.google.javascript.jscomp.PeepholeMinimizeConditions Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
/*
* Copyright 2010 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Predicate;
import com.google.javascript.jscomp.MinimizedCondition.MeasuredNode;
import com.google.javascript.jscomp.MinimizedCondition.MinimizationStyle;
import com.google.javascript.jscomp.base.Tri;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import org.jspecify.nullness.Nullable;
/**
* A peephole optimization that minimizes conditional expressions
* according to De Morgan's laws.
* Also rewrites conditional statements as expressions by replacing them
* with HOOKs and short-circuit binary operators.
*
* Based on PeepholeSubstituteAlternateSyntax
*/
class PeepholeMinimizeConditions
extends AbstractPeepholeOptimization {
private static final int AND_PRECEDENCE = NodeUtil.precedence(Token.AND);
private final boolean late;
/**
* @param late When late is false, this mean we are currently running before most of the other
* optimizations. In this case we would avoid optimizations that would make the code harder to
* analyze (such as using string splitting, merging statements with commas, etc). When late is
* true, we would do anything to minimize for size.
*/
PeepholeMinimizeConditions(boolean late) {
this.late = late;
}
/** Tries to apply our various peephole minimizations on the passed in node. */
@Override
public Node optimizeSubtree(Node node) {
switch (node.getToken()) {
case THROW:
case RETURN: {
Node result = tryRemoveRedundantExit(node);
if (result != node) {
return result;
}
return tryReplaceExitWithBreak(node);
}
// TODO(johnlenz): Maybe remove redundant BREAK and CONTINUE. Overlaps
// with MinimizeExitPoints.
case NOT:
tryMinimizeCondition(node.getFirstChild());
return tryMinimizeNot(node);
case IF:
performConditionSubstitutions(node.getFirstChild());
return tryMinimizeIf(node);
case EXPR_RESULT:
performConditionSubstitutions(node.getFirstChild());
return tryMinimizeExprResult(node);
case HOOK:
performConditionSubstitutions(node.getFirstChild());
return tryMinimizeHook(node);
case WHILE:
case DO:
tryMinimizeCondition(NodeUtil.getConditionExpression(node));
return node;
case FOR:
tryJoinForCondition(node);
tryMinimizeCondition(NodeUtil.getConditionExpression(node));
return node;
case BLOCK:
return tryReplaceIf(node);
default:
return node; //Nothing changed
}
}
private void tryJoinForCondition(Node n) {
if (!late) {
return;
}
Node block = n.getLastChild();
Node maybeIf = block.getFirstChild();
if (maybeIf != null && maybeIf.isIf()) {
Node thenBlock = maybeIf.getSecondChild();
Node maybeBreak = thenBlock.getFirstChild();
if (maybeBreak != null && maybeBreak.isBreak()
&& !maybeBreak.hasChildren()) {
// Preserve the IF ELSE expression is there is one.
if (maybeIf.hasXChildren(3)) {
maybeIf.replaceWith(maybeIf.getLastChild().detach());
} else {
NodeUtil.redeclareVarsInsideBranch(thenBlock);
block.removeFirstChild();
}
Node ifCondition = maybeIf.removeFirstChild();
Node fixedIfCondition = IR.not(ifCondition)
.srcref(ifCondition);
// OK, join the IF expression with the FOR expression
Node forCondition = NodeUtil.getConditionExpression(n);
if (forCondition.isEmpty()) {
forCondition.replaceWith(fixedIfCondition);
reportChangeToEnclosingScope(fixedIfCondition);
} else {
Node replacement = new Node(Token.AND);
forCondition.replaceWith(replacement);
replacement.addChildToBack(forCondition);
replacement.addChildToBack(fixedIfCondition);
reportChangeToEnclosingScope(replacement);
}
}
}
}
/**
* Use "return x?1:2;" in place of "if(x)return 1;return 2;"
*/
private Node tryReplaceIf(Node n) {
Node next = null;
for (Node child = n.getFirstChild();
child != null; child = next){
next = child.getNext();
if (child.isIf()){
Node cond = child.getFirstChild();
Node thenBranch = cond.getNext();
Node elseBranch = thenBranch.getNext();
Node nextNode = child.getNext();
if (nextNode != null && elseBranch == null
&& isReturnBlock(thenBranch)
&& nextNode.isIf()) {
Node nextCond = nextNode.getFirstChild();
Node nextThen = nextCond.getNext();
Node nextElse = nextThen.getNext();
if (areNodesEqualForInlining(thenBranch, nextThen)) {
// Transform
// if (x) return 1; if (y) return 1;
// to
// if (x||y) return 1;
child.detach();
child.detachChildren();
Node newCond = new Node(Token.OR, cond);
nextCond.replaceWith(newCond);
newCond.addChildToBack(nextCond);
reportChangeToEnclosingScope(newCond);
} else if (nextElse != null && areNodesEqualForInlining(thenBranch, nextElse)) {
// Transform
// if (x) return 1; if (y) foo() else return 1;
// to
// if (!x&&y) foo() else return 1;
child.detach();
child.detachChildren();
Node newCond = new Node(Token.AND,
IR.not(cond).srcref(cond));
nextCond.replaceWith(newCond);
newCond.addChildToBack(nextCond);
reportChangeToEnclosingScope(newCond);
}
} else if (nextNode != null && elseBranch == null
&& isReturnBlock(thenBranch) && isReturnExpression(nextNode)) {
Node thenExpr = null;
// if(x)return; return 1 -> return x?void 0:1
if (isReturnExpressBlock(thenBranch)) {
thenExpr = getBlockReturnExpression(thenBranch);
thenExpr.detach();
} else {
thenExpr = NodeUtil.newUndefinedNode(child);
}
Node elseExpr = nextNode.getFirstChild();
cond.detach();
elseExpr.detach();
Node returnNode = IR.returnNode(
IR.hook(cond, thenExpr, elseExpr)
.srcref(child));
child.replaceWith(returnNode);
nextNode.detach();
reportChangeToEnclosingScope(n);
// everything else in the block is dead code.
break;
} else if (elseBranch != null && statementMustExitParent(thenBranch)) {
elseBranch.detach();
elseBranch.insertAfter(child);
reportChangeToEnclosingScope(n);
}
}
}
return n;
}
private static boolean statementMustExitParent(Node n) {
switch (n.getToken()) {
case THROW:
case RETURN:
return true;
case BLOCK:
if (n.hasChildren()) {
Node child = n.getLastChild();
return statementMustExitParent(child);
}
return false;
// TODO(johnlenz): handle TRY/FINALLY
case FUNCTION:
default:
return false;
}
}
/**
* Replace duplicate exits in control structures. If the node following
* the exit node expression has the same effect as exit node, the node can
* be replaced or removed.
* For example:
* "while (a) {return f()} return f();" ==> "while (a) {break} return f();"
* "while (a) {throw 'ow'} throw 'ow';" ==> "while (a) {break} throw 'ow';"
*
* @param n An follow control exit expression (a THROW or RETURN node)
* @return The replacement for n, or the original if no change was made.
*/
private Node tryReplaceExitWithBreak(Node n) {
Node result = n.getFirstChild();
// Find the enclosing control structure, if any, that a "break" would exit
// from.
Node breakTarget = n;
for (; !ControlFlowAnalysis.isBreakTarget(breakTarget, null /* no label */);
breakTarget = breakTarget.getParent()) {
if (breakTarget.isFunction() || breakTarget.isScript()) {
// No break target.
return n;
}
}
Node follow = ControlFlowAnalysis.computeFollowNode(breakTarget);
// Skip pass all the finally blocks because both the break and return will
// also trigger all the finally blocks. However, the order of execution is
// slightly changed. Consider:
//
// return a() -> finally { b() } -> return a()
//
// which would call a() first. However, changing the first return to a
// break will result in calling b().
Node prefinallyFollows = follow;
follow = skipFinallyNodes(follow);
if (prefinallyFollows != follow) {
// There were finally clauses
if (!isPure(result)) {
// Can't defer the exit
return n;
}
}
if (follow == null && (n.isThrow() || result != null)) {
// Can't complete remove a throw here or a return with a result.
return n;
}
// When follow is null, this mean the follow of a break target is the
// end of a function. This means a break is same as return.
if (follow == null || areMatchingExits(n, follow)) {
Node replacement = IR.breakNode();
n.replaceWith(replacement);
reportChangeToEnclosingScope(replacement);
return replacement;
}
return n;
}
/**
* Remove duplicate exits. If the node following the exit node expression has the same effect as
* exit node, the node can be removed. For example: "if (a) {return f()} return f();" ==> "if (a)
* {} return f();" "if (a) {throw 'ow'} throw 'ow';" ==> "if (a) {} throw 'ow';"
*
* @param n An follow control exit expression (a THROW or RETURN node)
* @return The replacement for n, or the original if no change was made.
*/
private @Nullable Node tryRemoveRedundantExit(Node n) {
Node exitExpr = n.getFirstChild();
Node follow = ControlFlowAnalysis.computeFollowNode(n);
// Skip pass all the finally blocks because both the fall through and return
// will also trigger all the finally blocks.
Node prefinallyFollows = follow;
follow = skipFinallyNodes(follow);
if (prefinallyFollows != follow) {
// There were finally clauses
if (!isPure(exitExpr)) {
// Can't replace the return
return n;
}
}
if (follow == null && (n.isThrow() || exitExpr != null)) {
// Can't complete remove a throw here or a return with a result.
return n;
}
// When follow is null, this mean the follow of a break target is the
// end of a function. This means a break is same as return.
if (follow == null || areMatchingExits(n, follow)) {
reportChangeToEnclosingScope(n);
n.detach();
return null;
}
return n;
}
/**
* @return Whether the expression does not produces and can not be affected
* by side-effects.
*/
boolean isPure(Node n) {
return n == null
|| (!NodeUtil.canBeSideEffected(n)
&& !mayHaveSideEffects(n));
}
/**
* @return n or the node following any following finally nodes.
*/
static Node skipFinallyNodes(Node n) {
while (n != null && NodeUtil.isTryFinallyNode(n.getParent(), n)) {
n = ControlFlowAnalysis.computeFollowNode(n);
}
return n;
}
/**
* Check whether one exit can be replaced with another. Verify:
* 1) They are identical expressions
* 2) If an exception is possible that the statements, the original
* and the potential replacement are in the same exception handler.
*/
boolean areMatchingExits(Node nodeThis, Node nodeThat) {
if (!isASTNormalized()
&& (nodeThis.isThrow() || nodeThis.isReturn())
&& nodeThis.hasChildren()) {
// if the ast isn't normalized "return a" or "throw a" may not mean the same thing in
// different blocks.
return false;
}
return nodeThis.isEquivalentTo(nodeThat)
&& (!isExceptionPossible(nodeThis)
|| getExceptionHandler(nodeThis) == getExceptionHandler(nodeThat));
}
static boolean isExceptionPossible(Node n) {
// TODO(johnlenz): maybe use ControlFlowAnalysis.mayThrowException?
checkState(n.isReturn() || n.isThrow(), n);
return n.isThrow()
|| (n.hasChildren()
&& !NodeUtil.isLiteralValue(n.getLastChild(), true));
}
static Node getExceptionHandler(Node n) {
return ControlFlowAnalysis.getExceptionHandler(n);
}
/**
* Try to minimize NOT nodes such as !(x==y).
*
* Returns the replacement for n or the original if no change was made
*/
private Node tryMinimizeNot(Node n) {
checkArgument(n.isNot());
Node parent = n.getParent();
Node notChild = n.getFirstChild();
// negative operator of the current one : == -> != for instance.
Token complementOperator;
switch (notChild.getToken()) {
case EQ:
complementOperator = Token.NE;
break;
case NE:
complementOperator = Token.EQ;
break;
case SHEQ:
complementOperator = Token.SHNE;
break;
case SHNE:
complementOperator = Token.SHEQ;
break;
// GT, GE, LT, LE are not handled in this because !(x=NaN.
default:
return n;
}
Node newOperator = n.removeFirstChild();
newOperator.setToken(complementOperator);
n.replaceWith(newOperator);
reportChangeToEnclosingScope(parent);
return newOperator;
}
/**
* Try to remove leading NOTs from EXPR_RESULTS.
*
* Returns the replacement for n or the original if no replacement was
* necessary.
*/
private Node tryMinimizeExprResult(Node n) {
Node originalExpr = n.getFirstChild();
MinimizedCondition minCond = MinimizedCondition.fromConditionNode(originalExpr);
MeasuredNode mNode =
minCond.getMinimized(MinimizationStyle.ALLOW_LEADING_NOT);
if (mNode.isNot()) {
// Remove the leading NOT in the EXPR_RESULT.
replaceNode(originalExpr, mNode.withoutNot());
} else {
replaceNode(originalExpr, mNode);
}
return n;
}
/**
* Try flipping HOOKs that have negated conditions.
*
* Returns the replacement for n or the original if no replacement was
* necessary.
*/
private Node tryMinimizeHook(Node n) {
Node originalCond = n.getFirstChild();
MinimizedCondition minCond = MinimizedCondition.fromConditionNode(originalCond);
MeasuredNode mNode =
minCond.getMinimized(MinimizationStyle.ALLOW_LEADING_NOT);
if (mNode.isNot()) {
// Swap the HOOK
Node thenBranch = n.getSecondChild();
replaceNode(originalCond, mNode.withoutNot());
thenBranch.detach();
n.addChildToBack(thenBranch);
reportChangeToEnclosingScope(n);
} else {
replaceNode(originalCond, mNode);
}
return n;
}
/**
* Try turning IF nodes into smaller HOOKs
*
* Returns the replacement for n or the original if no replacement was
* necessary.
*/
private Node tryMinimizeIf(Node n) {
Node parent = n.getParent();
Node originalCond = n.getFirstChild();
/* If the condition is a literal, we'll let other
* optimizations try to remove useless code.
*/
if (NodeUtil.isLiteralValue(originalCond, true)) {
return n;
}
Node thenBranch = originalCond.getNext();
Node elseBranch = thenBranch.getNext();
MinimizedCondition minCond = MinimizedCondition.fromConditionNode(originalCond);
// Compute two minimized representations. The first representation counts
// a leading NOT node, and the second ignores a leading NOT node.
// If we can fold the if statement into a HOOK or boolean operation,
// then the NOT node does not matter, and we prefer the second condition.
// If we cannot fold the if statement, then we prefer the first condition.
MeasuredNode unnegatedCond = minCond.getMinimized(MinimizationStyle.PREFER_UNNEGATED);
MeasuredNode shortCond = minCond.getMinimized(MinimizationStyle.ALLOW_LEADING_NOT);
if (elseBranch == null) {
if (isFoldableExpressBlock(thenBranch)) {
Node expr = getBlockExpression(thenBranch);
if (!late && isPropertyAssignmentInExpression(expr)) {
// Keep opportunities for CollapseProperties such as
// a.longIdentifier || a.longIdentifier = ... -> var a = ...;
// until CollapseProperties has been run.
replaceNode(originalCond, unnegatedCond);
return n;
}
if (shortCond.isNot()) {
// if(!x)bar(); -> x||bar();
Node replacementCond = replaceNode(originalCond, shortCond.withoutNot()).detach();
Node or = IR.or(
replacementCond,
expr.removeFirstChild()).srcref(n);
Node newExpr = NodeUtil.newExpr(or);
n.replaceWith(newExpr);
reportChangeToEnclosingScope(parent);
return newExpr;
}
// True, but removed for performance reasons.
// Preconditions.checkState(shortCond.isEquivalentTo(unnegatedCond));
// if(x)foo(); -> x&&foo();
if (shortCond.isLowerPrecedenceThan(AND_PRECEDENCE)
&& isLowerPrecedence(expr.getFirstChild(), AND_PRECEDENCE)) {
// One additional set of parentheses is worth the change even if
// there is no immediate code size win. However, two extra pair of
// {}, we would have to think twice. (unless we know for sure the
// we can further optimize its parent.
replaceNode(originalCond, shortCond);
return n;
}
Node replacementCond = replaceNode(originalCond, shortCond).detach();
Node and = IR.and(replacementCond, expr.removeFirstChild()).srcref(n);
Node newExpr = NodeUtil.newExpr(and);
n.replaceWith(newExpr);
reportChangeToEnclosingScope(parent);
return newExpr;
} else {
// Try to combine two IF-ELSE
if (NodeUtil.isStatementBlock(thenBranch) && thenBranch.hasOneChild()) {
Node innerIf = thenBranch.getFirstChild();
if (innerIf.isIf()) {
Node innerCond = innerIf.getFirstChild();
Node innerThenBranch = innerCond.getNext();
Node innerElseBranch = innerThenBranch.getNext();
if (innerElseBranch == null
&& !(unnegatedCond.isLowerPrecedenceThan(AND_PRECEDENCE)
&& isLowerPrecedence(innerCond, AND_PRECEDENCE))) {
Node replacementCond = replaceNode(originalCond, unnegatedCond).detach();
n.detachChildren();
n.addChildToBack(
IR.and(
replacementCond,
innerCond.detach())
.srcref(originalCond));
n.addChildToBack(innerThenBranch.detach());
reportChangeToEnclosingScope(n);
// Not worth trying to fold the current IF-ELSE into && because
// the inner IF-ELSE wasn't able to be folded into && anyways.
return n;
}
}
}
}
replaceNode(originalCond, unnegatedCond);
return n;
}
/* TODO(dcc) This modifies the siblings of n, which is undesirable for a
* peephole optimization. This should probably get moved to another pass.
*/
tryRemoveRepeatedStatements(n);
// if(!x)foo();else bar(); -> if(x)bar();else foo();
// An additional set of curly braces isn't worth it.
if (shortCond.isNot() && !consumesDanglingElse(elseBranch)) {
replaceNode(originalCond, shortCond.withoutNot());
thenBranch.detach();
n.addChildToBack(thenBranch);
reportChangeToEnclosingScope(n);
return n;
}
// if(x)return 1;else return 2; -> return x?1:2;
if (isReturnExpressBlock(thenBranch) && isReturnExpressBlock(elseBranch)) {
Node thenExpr = getBlockReturnExpression(thenBranch);
Node elseExpr = getBlockReturnExpression(elseBranch);
Node replacementCond = replaceNode(originalCond, shortCond).detach();
thenExpr.detach();
elseExpr.detach();
// note - we ignore any cases with "return;", technically this
// can be converted to "return undefined;" or some variant, but
// that does not help code size.
Node returnNode = IR.returnNode(
IR.hook(replacementCond, thenExpr, elseExpr)
.srcref(n));
n.replaceWith(returnNode);
reportChangeToEnclosingScope(returnNode);
return returnNode;
}
boolean thenBranchIsExpressionBlock = isFoldableExpressBlock(thenBranch);
boolean elseBranchIsExpressionBlock = isFoldableExpressBlock(elseBranch);
if (thenBranchIsExpressionBlock && elseBranchIsExpressionBlock) {
Node thenOp = getBlockExpression(thenBranch).getFirstChild();
Node elseOp = getBlockExpression(elseBranch).getFirstChild();
if (thenOp.getToken() == elseOp.getToken()) {
// if(x)a=1;else a=2; -> a=x?1:2;
if (NodeUtil.isAssignmentOp(thenOp)) {
Node lhs = thenOp.getFirstChild();
if (areNodesEqualForInlining(lhs, elseOp.getFirstChild())
// if LHS has side effects, don't proceed [since the optimization
// evaluates LHS before cond]
// NOTE - there are some circumstances where we can
// proceed even if there are side effects...
&& !mayEffectMutableState(lhs)
&& (!mayHaveSideEffects(originalCond)
|| (thenOp.isAssign() && thenOp.getFirstChild().isName()))) {
Node replacementCond = replaceNode(originalCond, shortCond).detach();
Node assignName = thenOp.removeFirstChild();
Node thenExpr = thenOp.removeFirstChild();
Node elseExpr = elseOp.getLastChild();
elseExpr.detach();
Node hookNode = IR.hook(replacementCond, thenExpr, elseExpr)
.srcref(n);
Node assign = new Node(thenOp.getToken(), assignName, hookNode).srcref(thenOp);
Node expr = NodeUtil.newExpr(assign);
n.replaceWith(expr);
reportChangeToEnclosingScope(parent);
return expr;
}
}
}
// if(x)foo();else bar(); -> x?foo():bar()
Node replacementCond = replaceNode(originalCond, shortCond).detach();
thenOp.detach();
elseOp.detach();
Node expr = IR.exprResult(
IR.hook(replacementCond, thenOp, elseOp).srcref(n));
n.replaceWith(expr);
reportChangeToEnclosingScope(parent);
return expr;
}
boolean thenBranchIsVar = isVarBlock(thenBranch);
boolean elseBranchIsVar = isVarBlock(elseBranch);
// if(x)var y=1;else y=2 -> var y=x?1:2
if (thenBranchIsVar && elseBranchIsExpressionBlock
&& getBlockExpression(elseBranch).getFirstChild().isAssign()) {
Node var = getBlockVar(thenBranch);
Node elseAssign = getBlockExpression(elseBranch).getFirstChild();
Node name1 = var.getFirstChild();
Node maybeName2 = elseAssign.getFirstChild();
if (name1.hasChildren()
&& maybeName2.isName()
&& name1.getString().equals(maybeName2.getString())) {
checkState(name1.hasOneChild());
Node thenExpr = name1.removeFirstChild();
Node elseExpr = elseAssign.getLastChild().detach();
Node replacementCond = replaceNode(originalCond, shortCond).detach();
Node hookNode = IR.hook(replacementCond, thenExpr, elseExpr).srcref(n);
var.detach();
name1.addChildToBack(hookNode);
n.replaceWith(var);
reportChangeToEnclosingScope(parent);
return var;
}
// if(x)y=1;else var y=2 -> var y=x?1:2
} else if (elseBranchIsVar && thenBranchIsExpressionBlock
&& getBlockExpression(thenBranch).getFirstChild().isAssign()) {
Node var = getBlockVar(elseBranch);
Node thenAssign = getBlockExpression(thenBranch).getFirstChild();
Node maybeName1 = thenAssign.getFirstChild();
Node name2 = var.getFirstChild();
if (name2.hasChildren()
&& maybeName1.isName()
&& maybeName1.getString().equals(name2.getString())) {
Node thenExpr = thenAssign.getLastChild().detach();
checkState(name2.hasOneChild());
Node elseExpr = name2.removeFirstChild();
Node replacementCond = replaceNode(originalCond, shortCond).detach();
Node hookNode = IR.hook(replacementCond, thenExpr, elseExpr).srcref(n);
var.detach();
name2.addChildToBack(hookNode);
n.replaceWith(var);
reportChangeToEnclosingScope(parent);
return var;
}
}
replaceNode(originalCond, unnegatedCond);
return n;
}
/**
* Try to remove duplicate statements from IF blocks. For example:
*
* if (a) {
* x = 1;
* return true;
* } else {
* x = 2;
* return true;
* }
*
* becomes:
*
* if (a) {
* x = 1;
* } else {
* x = 2;
* }
* return true;
*
* @param n The IF node to examine.
*/
private void tryRemoveRepeatedStatements(Node n) {
// Only run this if variable names are guaranteed to be unique. Otherwise bad things can happen:
// see PeepholeMinimizeConditionsTest#testDontRemoveDuplicateStatementsWithoutNormalization
if (!isASTNormalized()) {
return;
}
checkState(n.isIf(), n);
Node parent = n.getParent();
if (!NodeUtil.isStatementBlock(parent)) {
// If the immediate parent is something like a label, we
// can't move the statement, so bail.
return;
}
Node cond = n.getFirstChild();
Node trueBranch = cond.getNext();
Node falseBranch = trueBranch.getNext();
checkNotNull(trueBranch);
checkNotNull(falseBranch);
while (true) {
Node lastTrue = trueBranch.getLastChild();
Node lastFalse = falseBranch.getLastChild();
if (lastTrue == null || lastFalse == null
|| !areNodesEqualForInlining(lastTrue, lastFalse)) {
break;
}
lastTrue.detach();
lastFalse.detach();
lastTrue.insertAfter(n);
reportChangeToEnclosingScope(parent);
}
}
/**
* @return Whether the node is a block with a single statement that is
* an expression.
*/
private static boolean isFoldableExpressBlock(Node n) {
if (n.isBlock()) {
if (n.hasOneChild()) {
Node maybeExpr = n.getFirstChild();
if (maybeExpr.isExprResult()) {
// IE has a bug where event handlers behave differently when
// their return value is used vs. when their return value is in
// an EXPR_RESULT. It's pretty freaking weird. See:
// http://blickly.github.io/closure-compiler-issues/#291
// We try to detect this case, and not fold EXPR_RESULTs
// into other expressions.
// e.g.:
// if (e.onchange) {
// e.onchange({
// _extendedByPrototype: Prototype.emptyFunction,
// target: e
// });
// }
if (maybeExpr.getFirstChild().isCall() || maybeExpr.getFirstChild().isOptChainCall()) {
Node calledFn = maybeExpr.getFirstFirstChild();
// We only have to worry about methods with an implicit 'this'
// param, or this doesn't happen.
if (calledFn.isGetElem() || calledFn.isOptChainGetElem()) {
return false;
} else if ((calledFn.isGetProp() || calledFn.isOptChainGetProp())
&& calledFn.getString().startsWith("on")) {
return false;
}
}
return true;
}
return false;
}
}
return false;
}
/**
* @return The expression node.
*/
private static Node getBlockExpression(Node n) {
checkState(isFoldableExpressBlock(n));
return n.getFirstChild();
}
/**
* @return Whether the node is a block with a single statement that is
* an return with or without an expression.
*/
private static boolean isReturnBlock(Node n) {
if (n.isBlock()) {
if (n.hasOneChild()) {
Node first = n.getFirstChild();
return first.isReturn();
}
}
return false;
}
/**
* @return Whether the node is a block with a single statement that is
* an return.
*/
private static boolean isReturnExpressBlock(Node n) {
if (n.isBlock()) {
if (n.hasOneChild()) {
Node first = n.getFirstChild();
if (first.isReturn()) {
return first.hasOneChild();
}
}
}
return false;
}
/**
* @return Whether the node is a single return statement.
*/
private static boolean isReturnExpression(Node n) {
if (n.isReturn()) {
return n.hasOneChild();
}
return false;
}
/**
* @return The expression that is part of the return.
*/
private static Node getBlockReturnExpression(Node n) {
checkState(isReturnExpressBlock(n));
return n.getFirstFirstChild();
}
/**
* @return Whether the node is a block with a single statement that is
* a VAR declaration of a single variable.
*/
private static boolean isVarBlock(Node n) {
if (n.isBlock()) {
if (n.hasOneChild()) {
Node first = n.getFirstChild();
if (first.isVar()) {
return first.hasOneChild();
}
}
}
return false;
}
/**
* @return The var node.
*/
private static Node getBlockVar(Node n) {
checkState(isVarBlock(n));
return n.getFirstChild();
}
/**
* Does a statement consume a 'dangling else'? A statement consumes
* a 'dangling else' if an 'else' token following the statement
* would be considered by the parser to be part of the statement.
*/
private static boolean consumesDanglingElse(Node n) {
while (true) {
switch (n.getToken()) {
case IF:
if (n.getChildCount() < 3) {
return true;
}
// This IF node has no else clause.
n = n.getLastChild();
continue;
case BLOCK:
if (!n.hasOneChild()) {
return false;
}
// This BLOCK has no curly braces.
n = n.getLastChild();
continue;
case WITH:
case WHILE:
case FOR:
case FOR_IN:
n = n.getLastChild();
continue;
default:
return false;
}
}
}
/**
* Whether the node type has lower precedence than "precedence"
*/
static boolean isLowerPrecedence(Node n, int precedence) {
return NodeUtil.precedence(n.getToken()) < precedence;
}
/**
* Does the expression contain a property assignment?
*/
private static boolean isPropertyAssignmentInExpression(Node n) {
Predicate isPropertyAssignmentInExpressionPredicate =
(Node input) -> (input.isGetProp() && input.getParent().isAssign());
return NodeUtil.has(n, isPropertyAssignmentInExpressionPredicate, NodeUtil.MATCH_NOT_FUNCTION);
}
/**
* Try to minimize condition expression, as there are additional
* assumptions that can be made when it is known that the final result
* is a boolean.
*
* @return The replacement for n, or the original if no change was made.
*/
private Node tryMinimizeCondition(Node n) {
n = performConditionSubstitutions(n);
MinimizedCondition minCond = MinimizedCondition.fromConditionNode(n);
return replaceNode(
n,
minCond.getMinimized(MinimizationStyle.PREFER_UNNEGATED));
}
private Node replaceNode(Node original, MeasuredNode measuredNodeReplacement) {
if (measuredNodeReplacement.willChange(original)) {
Node replacement = measuredNodeReplacement.applyTo(original);
reportChangeToEnclosingScope(replacement);
return replacement;
}
return original;
}
/**
* Try to minimize the given condition by applying local substitutions.
*
* The following types of transformations are performed:
* x || true --> true
* x && true --> x
* x ? false : true --> !x
* x ? true : y --> x || y
* x ? x : y --> x || y
*
* Returns the replacement for n, or the original if no change was made
*/
private Node performConditionSubstitutions(Node n) {
Node parent = n.getParent();
switch (n.getToken()) {
case OR:
case AND:
{
Node left = n.getFirstChild();
Node right = n.getLastChild();
// Because the expression is in a boolean context minimize
// the children, this can't be done in the general case.
left = performConditionSubstitutions(left);
right = performConditionSubstitutions(right);
// Remove useless conditionals
// Handle the following cases:
// x || false --> x
// x && true --> x
// This works regardless of whether x has side effects.
//
// If x does not have side effects:
// x || true --> true
// x && false --> false
//
// If x may have side effects:
// x || true --> x,true
// x && false --> x,false
//
// In the last two cases, code size may increase slightly (adding
// some parens because the comma operator has a low precedence) but
// the new AST is easier for other passes to handle.
Tri rightVal = getSideEffectFreeBooleanValue(right);
if (getSideEffectFreeBooleanValue(right) != Tri.UNKNOWN) {
Token type = n.getToken();
Node replacement = null;
boolean rval = rightVal.toBoolean(true);
// (x || FALSE) => x
// (x && TRUE) => x
if ((type == Token.OR && !rval) || (type == Token.AND && rval)) {
replacement = left;
} else if (!mayHaveSideEffects(left)) {
replacement = right;
} else {
// expr_with_sideeffects || true => expr_with_sideeffects, true
// expr_with_sideeffects && false => expr_with_sideeffects, false
n.detachChildren();
replacement = IR.comma(left, right);
}
if (replacement != null) {
n.detachChildren();
n.replaceWith(replacement);
reportChangeToEnclosingScope(parent);
return replacement;
}
}
return n;
}
case HOOK:
{
Node condition = n.getFirstChild();
Node trueNode = n.getSecondChild();
Node falseNode = n.getLastChild();
// Because the expression is in a boolean context minimize
// the result children, this can't be done in the general case.
// The condition is handled in the general case in #optimizeSubtree
trueNode = performConditionSubstitutions(trueNode);
falseNode = performConditionSubstitutions(falseNode);
// Handle five cases:
// x ? true : false --> x
// x ? false : true --> !x
// x ? true : y --> x || y
// x ? y : false --> x && y
// Only when x is NAME, hence x does not have side effects
// x ? x : y --> x || y
Node replacement = null;
Tri trueNodeVal = getSideEffectFreeBooleanValue(trueNode);
Tri falseNodeVal = getSideEffectFreeBooleanValue(falseNode);
if (trueNodeVal == Tri.TRUE && falseNodeVal == Tri.FALSE) {
// Remove useless conditionals, keep the condition
condition.detach();
replacement = condition;
} else if (trueNodeVal == Tri.FALSE && falseNodeVal == Tri.TRUE) {
// Remove useless conditionals, keep the condition
condition.detach();
replacement = IR.not(condition);
} else if (trueNodeVal == Tri.TRUE) {
// Remove useless true case.
n.detachChildren();
replacement = IR.or(condition, falseNode);
} else if (falseNodeVal == Tri.FALSE) {
// Remove useless false case
n.detachChildren();
replacement = IR.and(condition, trueNode);
} else if (!mayHaveSideEffects(condition)
&& !mayHaveSideEffects(trueNode)
&& condition.isEquivalentTo(trueNode)) {
// Remove redundant condition
n.detachChildren();
replacement = IR.or(trueNode, falseNode);
}
if (replacement != null) {
n.replaceWith(replacement);
reportChangeToEnclosingScope(replacement);
n = replacement;
}
return n;
}
default:
// while(true) --> while(1)
Tri nVal = getSideEffectFreeBooleanValue(n);
if (nVal != Tri.UNKNOWN) {
boolean result = nVal.toBoolean(true);
int equivalentResult = result ? 1 : 0;
return maybeReplaceChildWithNumber(n, equivalentResult);
}
// We can't do anything else currently.
return n;
}
}
/**
* Replaces a node with a number node if the new number node is not equivalent to the current
* node.
*
* Returns the replacement for n if it was replaced, otherwise returns n.
*/
private Node maybeReplaceChildWithNumber(Node n, int num) {
Node newNode = IR.number(num);
if (!newNode.isEquivalentTo(n)) {
n.replaceWith(newNode);
reportChangeToEnclosingScope(newNode);
markFunctionsDeleted(n);
return newNode;
}
return n;
}
}