com.google.javascript.jscomp.PeepholeRemoveDeadCode Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2004 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.base.Tri;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import org.jspecify.nullness.Nullable;
/**
* Peephole optimization to remove useless code such as IF's with false guard conditions, comma
* operator's left hand sides with no side effects, etc.
*/
class PeepholeRemoveDeadCode extends AbstractPeepholeOptimization {
// TODO(dcc): Some (all) of these can probably be better achieved
// using the control flow graph (like CheckUnreachableCode).
// There is an existing CFG pass (UnreachableCodeElimination) that
// could be changed to use code from CheckUnreachableCode to do this.
@Override
Node optimizeSubtree(Node subtree) {
switch (subtree.getToken()) {
case ASSIGN:
return tryFoldAssignment(subtree);
case COMMA:
return tryFoldComma(subtree);
case SCRIPT:
case BLOCK:
return tryOptimizeBlock(subtree);
case EXPR_RESULT:
return tryFoldExpr(subtree);
case HOOK:
return tryFoldHook(subtree);
case SWITCH:
return tryOptimizeSwitch(subtree);
case IF:
return tryFoldIf(subtree);
case WHILE:
throw checkNormalization(false, "WHILE");
case FOR:
{
Node condition = NodeUtil.getConditionExpression(subtree);
if (condition != null) {
tryFoldForCondition(condition);
}
return tryFoldFor(subtree);
}
case DO:
Node foldedDo = tryFoldDoAway(subtree);
if (foldedDo.isDo()) {
return tryFoldEmptyDo(foldedDo);
}
return foldedDo;
case TRY:
return tryFoldTry(subtree);
case LABEL:
return tryFoldLabel(subtree);
case ARRAY_PATTERN:
return tryOptimizeArrayPattern(subtree);
case OBJECT_PATTERN:
return tryOptimizeObjectPattern(subtree);
case VAR:
case CONST:
case LET:
return tryOptimizeNameDeclaration(subtree);
case DEFAULT_VALUE:
return tryRemoveDefaultValue(subtree);
case OPTCHAIN_CALL:
return tryRemoveOptionalCall(subtree);
default:
return subtree;
}
}
private Node tryRemoveDefaultValue(Node defaultValue) {
checkArgument(defaultValue.isDefaultValue(), defaultValue);
Node lValue = defaultValue.getFirstChild();
Node val = defaultValue.getSecondChild();
boolean removeVal = false;
// If the default is `undefined` always remove the value
if (val.isName() && val.getString().equals("undefined")) {
removeVal = true;
}
// If the `void` application is pure, remove the value
if (val.isVoid()) {
Node voidArg = val.getFirstChild();
removeVal = !mayHaveSideEffects(voidArg);
}
if (removeVal) {
defaultValue.replaceWith(lValue.detach());
reportChangeToEnclosingScope(lValue);
return lValue;
}
return defaultValue;
}
private @Nullable Node tryFoldLabel(Node n) {
String labelName = n.getFirstChild().getString();
Node stmt = n.getLastChild();
if (stmt.isEmpty()) {
reportChangeToEnclosingScope(n);
n.detach();
return null;
}
if (stmt.isBlock() && !stmt.hasChildren()) {
reportChangeToEnclosingScope(n);
if (n.getParent().isLabel()) {
// If the parent is itself a label, replace this label
// with its contained block to keep the AST in a valid state.
n.replaceWith(stmt.detach());
} else {
n.detach();
}
return null;
}
Node child = getOnlyInterestingChild(stmt);
if (child != null) {
stmt = child;
}
if (stmt.isBreak() && stmt.getFirstChild().getString().equals(labelName)) {
reportChangeToEnclosingScope(n);
n.detach();
return null;
}
return n;
}
/**
* Return the only "interesting" child of {@code block}, if it has exactly one interesting child,
* otherwise return null. For purposes of this method, a node is considered "interesting" unless
* it is an empty synthetic block.
*/
private static @Nullable Node getOnlyInterestingChild(Node block) {
if (!block.isBlock()) {
return null;
}
if (block.hasOneChild()) {
return block.getOnlyChild();
}
Node ret = null;
for (Node child = block.getFirstChild(); child != null; child = child.getNext()) {
if (child.isSyntheticBlock() && !child.hasChildren()) {
// Uninteresting child.
} else if (ret != null) {
// Found more than one interesting child.
return null;
} else {
ret = child;
}
}
return ret;
}
/**
* Remove try blocks without catch blocks and with empty or not existent finally blocks. Or, only
* leave the finally blocks if try body blocks are empty
*
* @return the replacement node, if changed, or the original if not
*/
private @Nullable Node tryFoldTry(Node n) {
checkState(n.isTry(), n);
Node body = n.getFirstChild();
Node catchBlock = body.getNext();
Node finallyBlock = catchBlock.getNext();
// Removes TRYs that had its CATCH removed and/or empty FINALLY.
if (!catchBlock.hasChildren() && (finallyBlock == null || !finallyBlock.hasChildren())) {
checkState(!n.getParent().isLabel());
body.detach();
n.replaceWith(body);
reportChangeToEnclosingScope(body);
return body;
}
// Only leave FINALLYs if TRYs are not empty
if (!body.hasChildren()) {
NodeUtil.redeclareVarsInsideBranch(catchBlock);
reportChangeToEnclosingScope(n);
if (finallyBlock != null) {
finallyBlock.detach();
checkState(!n.getParent().isLabel());
n.replaceWith(finallyBlock);
return finallyBlock;
} else {
checkState(!n.getParent().isLabel());
n.detach();
return null;
}
}
return n;
}
/**
* Try removing identity assignments and empty destructuring pattern assignments
*
* @return the replacement node, if changed, or the original if not
*/
private Node tryFoldAssignment(Node subtree) {
checkState(subtree.isAssign());
Node left = subtree.getFirstChild();
Node right = subtree.getLastChild();
if (left.isName()
&& right.isName()
&& left.getString().equals(right.getString())) {
// Only names
subtree.replaceWith(right.detach());
reportChangeToEnclosingScope(right);
return right;
} else if (left.isDestructuringPattern() && !left.hasChildren()) {
// `[] = ` becomes ``
// Note that this does potentially change behavior. If `` is not iterable and this
// code originally threw, it will no longer throw.
subtree.replaceWith(right.detach());
reportChangeToEnclosingScope(right);
return right;
}
return subtree;
}
/**
* Try removing identity assignments and empty destructuring pattern assignments
*
* @return the replacement node, if changed, or the original if not
*/
private Node tryOptimizeNameDeclaration(Node subtree) {
checkState(NodeUtil.isNameDeclaration(subtree));
Node left = subtree.getFirstChild();
if (left.isDestructuringLhs() && left.hasTwoChildren()) {
Node pattern = left.getFirstChild();
if (!pattern.hasChildren()) {
// `var [] = foo();` becomes `foo();`
Node value = left.getSecondChild();
subtree.replaceWith(IR.exprResult(value.detach()).srcref(value));
reportChangeToEnclosingScope(value);
}
}
return subtree;
}
/**
* Try folding EXPR_RESULT nodes by removing useless Ops and expressions.
* @return the replacement node, if changed, or the original if not
*/
private Node tryFoldExpr(Node subtree) {
Node result = trySimplifyUnusedResult(subtree.getFirstChild());
if (result == null) {
Node parent = subtree.getParent();
// If the EXPR_RESULT no longer has any children, remove it as well.
if (parent.isLabel()) {
Node replacement = IR.block().srcref(subtree);
subtree.replaceWith(replacement);
subtree = replacement;
} else {
subtree.detach();
subtree = null;
}
}
return subtree;
}
/**
* Replaces {@code expression} with an expression that contains only side-effects of the original.
*
* This replacement is made under the assumption that the result of {@code expression} is
* unused and therefore it is correct to eliminate non-side-effectful nodes.
*
* @return The replacement expression, or {@code null} if there were no side-effects to preserve.
*/
private @Nullable Node trySimplifyUnusedResult(Node expression) {
ArrayDeque sideEffectRoots = new ArrayDeque<>();
boolean atFixedPoint = trySimplifyUnusedResultInternal(expression, sideEffectRoots);
if (atFixedPoint) {
// `expression` is in a form that cannot be further optimized.
return expression;
} else if (sideEffectRoots.isEmpty()) {
deleteNode(expression);
return null;
} else if (sideEffectRoots.peekFirst() == expression) {
// Expression was a conditional that was transformed. There can't be any other side-effects,
// but we also can't detach the transformed root.
checkState(sideEffectRoots.size() == 1, sideEffectRoots);
reportChangeToEnclosingScope(expression);
return expression;
} else {
Node sideEffects = asDetachedExpression(sideEffectRoots.pollFirst());
// Assemble a tree of comma expressions for all the side-effects. The tree must execute the
// side-effects in FIFO order with respect to the queue. It must also be left leaning to match
// the parser's preferred strucutre.
while (!sideEffectRoots.isEmpty()) {
Node next = asDetachedExpression(sideEffectRoots.pollFirst());
sideEffects = IR.comma(sideEffects, next).srcref(next);
}
sideEffects.insertBefore(expression);
deleteNode(expression);
return sideEffects;
}
}
/**
* Collects any potentially side-effectful subtrees within {@code tree} into {@code
* sideEffectRoots}.
*
* When a node is determined to have side-effects its descendants are not explored. This method
* assumes the entire subtree of such a node must be preserved. As a corollary, the contents of
* {@code sideEffectRoots} are a forest.
*
*
This operation generally does not mutate {@code tree}; however, exceptions are made for
* expressions that alter control-flow. Such expression will be pruned of their side-effectless
* branches. Even in this case, {@code tree} is never detached.
*
* @param sideEffectRoots The roots of subtrees determined to have side-effects, in execution
* order.
* @return {@code true} iff there is no code to be removed from within {@code tree}; it is already
* at a fixed point for code removal.
*/
private boolean trySimplifyUnusedResultInternal(Node tree, ArrayDeque sideEffectRoots) {
// Special cases for conditional expressions that may be using results.
switch (tree.getToken()) {
case HOOK:
// Try to remove one or more of the conditional children and transform the HOOK to an
// equivalent operation. Remember that if either value branch still exists, the result of
// the predicate expression is being used, and so cannot be removed.
// x() ? foo() : 1 --> x() && foo()
// x() ? 1 : foo() --> x() || foo()
// x() ? 1 : 1 --> x()
// x ? 1 : 1 --> null
Node trueNode = trySimplifyUnusedResult(tree.getSecondChild());
Node falseNode = trySimplifyUnusedResult(tree.getLastChild());
if (trueNode == null && falseNode != null) {
checkState(tree.hasTwoChildren(), tree);
tree.setToken(Token.OR);
sideEffectRoots.addLast(tree);
return false; // The node type was changed.
} else if (trueNode != null && falseNode == null) {
checkState(tree.hasTwoChildren(), tree);
tree.setToken(Token.AND);
sideEffectRoots.addLast(tree);
return false; // The node type was changed.
} else if (trueNode == null && falseNode == null) {
// Don't bother adding true and false branch children to make the AST valid; this HOOK is
// going to be deleted. We just need to collect any side-effects from the predicate
// expression.
trySimplifyUnusedResultInternal(tree.getOnlyChild(), sideEffectRoots);
return false; // This HOOK must be cleaned up.
} else {
sideEffectRoots.addLast(tree);
return hasFixedPointParent(tree);
}
case AND:
case OR:
case COALESCE:
// Try to remove the second operand from a AND, OR, and COALESCE operations. Remember that
// if the second
// child still exists, the result of the first expression is being used, and so cannot be
// removed.
// x() ?? f --> x()
// x() || f --> x()
// x() && f --> x()
Node conditionalResultNode = trySimplifyUnusedResult(tree.getLastChild());
if (conditionalResultNode == null) {
// Don't bother adding a second child to make the AST valid; this op is going to be
// deleted. We just need to collect any side-effects from the predicate first child.
trySimplifyUnusedResultInternal(tree.getOnlyChild(), sideEffectRoots);
return false; // This op must be cleaned up.
} else {
sideEffectRoots.addLast(tree);
return hasFixedPointParent(tree);
}
case FUNCTION:
// Functions that aren't being invoked are dead. If they were invoked we'd see the CALL
// before arriving here. We don't want to look at any children since they'll never execute.
return false;
default:
// This is the meat of this function. It covers the general case of nodes which are unused
if (nodeTypeMayHaveSideEffects(tree)) {
sideEffectRoots.addLast(tree);
return hasFixedPointParent(tree);
} else if (!tree.hasChildren()) {
return false; // A node must have children or side-effects to be at fixed-point.
}
boolean atFixedPoint = hasFixedPointParent(tree);
for (Node child = tree.getFirstChild(); child != null; child = child.getNext()) {
atFixedPoint &= trySimplifyUnusedResultInternal(child, sideEffectRoots);
}
return atFixedPoint;
}
}
/**
* Returns an expression executing {@code expr} which is legal in any expression context.
*
* @param expr An attached expression
* @return A detached expression
*/
private static Node asDetachedExpression(Node expr) {
switch (expr.getToken()) {
case ITER_SPREAD:
case OBJECT_SPREAD:
switch (expr.getParent().getToken()) {
case ARRAYLIT:
case NEW:
case CALL: // `Math.sin(...c)`
case OPTCHAIN_CALL: // `Math?.sin(...c)`
expr = IR.arraylit(expr.detach()).srcref(expr);
break;
case OBJECTLIT:
expr = IR.objectlit(expr.detach()).srcref(expr);
break;
default:
throw new IllegalStateException(expr.toStringTree());
}
break;
default:
break;
}
if (expr.hasParent()) {
expr.detach();
}
checkState(IR.mayBeExpression(expr), expr);
return expr;
}
/**
* Returns {@code true} iff {@code expr} is parented such that it is valid in a fixed-point
* representation of an unused expression tree.
*
* A fixed-point representation is one in which no futher nodes should be changed or removed
* when removing unused code. This method assumes that the expression tree in question is unused,
* so only side-effects are relevant.
*/
private static boolean hasFixedPointParent(Node expr) {
// Most kinds of nodes shouldn't be branches in the fixed-point tree of an unused
// expression. Those listed below are the only valid kinds.
switch (expr.getParent().getToken()) {
case AND:
case COMMA:
case HOOK:
case OR:
case COALESCE:
return true;
case ARRAYLIT:
case OBJECTLIT:
// Make a special allowance for SPREADs so they remain in a legal context. Parent types
// other than ARRAYLIT and OBJECTLIT are not fixed-point because they are the tersest legal
// parents and are known to be side-effect free.
return expr.isSpread();
default:
// Statments are always fixed-point parents. All other expressions are not.
return NodeUtil.isStatement(expr.getParent());
}
}
/**
* A predicate for matching anything except function nodes.
*/
private static class MatchUnnamedBreak implements Predicate{
@Override
public boolean apply(Node n) {
return n.isBreak() && !n.hasChildren();
}
}
static final Predicate MATCH_UNNAMED_BREAK = new MatchUnnamedBreak();
private void removeIfUnnamedBreak(Node maybeBreak) {
if (maybeBreak != null && maybeBreak.isBreak() && !maybeBreak.hasChildren()) {
reportChangeToEnclosingScope(maybeBreak);
maybeBreak.detach();
}
}
private Node tryRemoveSwitchWithSingleCase(Node n, boolean shouldHoistCondition) {
Node caseBlock = n.getLastChild().getLastChild();
removeIfUnnamedBreak(caseBlock.getLastChild());
// Back off if the switch contains statements like "if (a) { break; }"
if (NodeUtil.has(caseBlock, MATCH_UNNAMED_BREAK, NodeUtil.MATCH_NOT_FUNCTION)) {
return n;
}
if (shouldHoistCondition) {
Node switchBlock = caseBlock.getGrandparent();
IR.exprResult(n.removeFirstChild()).srcref(n).insertBefore(switchBlock);
}
n.replaceWith(caseBlock.detach());
reportChangeToEnclosingScope(caseBlock);
return caseBlock;
}
private Node tryRemoveSwitch(Node n) {
if (n.hasOneChild()) {
// Remove the switch if there are no remaining cases
Node condition = n.removeFirstChild();
Node replacement = IR.exprResult(condition).srcref(n);
n.replaceWith(replacement);
reportChangeToEnclosingScope(replacement);
return replacement;
} else if (n.hasTwoChildren() && n.getLastChild().isDefaultCase()) {
if (n.getFirstChild().isCall() || n.getFirstChild().isOptChainCall()) {
// Before removing switch, we must preserve the switch condition if it is a call
return tryRemoveSwitchWithSingleCase(n, true);
} else {
return tryRemoveSwitchWithSingleCase(n, false);
}
} else {
return n;
}
}
/**
* Remove useless switches and cases.
*/
private Node tryOptimizeSwitch(Node n) {
checkState(n.isSwitch(), n);
Node defaultCase = tryOptimizeDefaultCase(n);
// Generally, it is unsafe to remove other cases when the default case is not the last one.
if (defaultCase == null || n.getLastChild().isDefaultCase()) {
Node cond = n.getFirstChild();
Node prev = null;
Node next = null;
Node cur;
for (cur = cond.getNext(); cur != null; cur = next) {
next = cur.getNext();
if (!mayHaveSideEffects(cur.getFirstChild()) && isUselessCase(cur, prev, defaultCase)) {
removeCase(n, cur);
} else {
prev = cur;
}
}
// Optimize switches with constant condition
if (NodeUtil.isLiteralValue(cond, false)) {
Node caseLabel;
Tri caseMatches = Tri.TRUE;
// Remove cases until you find one that may match
for (cur = cond.getNext(); cur != null; cur = next) {
next = cur.getNext();
caseLabel = cur.getFirstChild();
caseMatches = PeepholeFoldConstants.evaluateComparison(this, Token.SHEQ, cond, caseLabel);
if (caseMatches == Tri.TRUE) {
break;
} else if (caseMatches == Tri.UNKNOWN) {
break;
} else {
removeCase(n, cur);
}
}
if (cur != null && caseMatches == Tri.TRUE) {
// Skip cases until you find one whose last stm is a removable break
Node matchingCase = cur;
Node matchingCaseBlock = matchingCase.getLastChild();
while (cur != null) {
Node block = cur.getLastChild();
Node lastStm = block.getLastChild();
boolean isLastStmRemovableBreak = false;
if (lastStm != null && isExit(lastStm)) {
removeIfUnnamedBreak(lastStm);
isLastStmRemovableBreak = true;
}
next = cur.getNext();
// Remove the fallthrough case labels
if (cur != matchingCase) {
while (block.hasChildren()) {
matchingCaseBlock.addChildToBack(block.removeFirstChild());
}
reportChangeToEnclosingScope(cur);
cur.detach();
}
cur = next;
if (isLastStmRemovableBreak) {
break;
}
}
// Remove any remaining cases
for (; cur != null; cur = next) {
next = cur.getNext();
removeCase(n, cur);
}
// If there is one case left, we may be able to fold it
cur = cond.getNext();
if (cur != null && cur.getNext() == null) {
return tryRemoveSwitchWithSingleCase(n, false);
}
}
}
}
return tryRemoveSwitch(n);
}
/**
* @return the default case node or null if there is no default case or if the default case is
* removed.
*/
private @Nullable Node tryOptimizeDefaultCase(Node n) {
checkState(n.isSwitch(), n);
Node lastNonRemovable = n.getFirstChild(); // The switch condition
// The first child is the switch conditions skip it when looking for cases.
for (Node c = n.getSecondChild(); c != null; c = c.getNext()) {
if (c.isDefaultCase()) {
// Remove cases that fall-through to the default case
Node caseToRemove = lastNonRemovable.getNext();
for (Node next; caseToRemove != c; caseToRemove = next) {
next = caseToRemove.getNext();
removeCase(n, caseToRemove);
}
// Don't use the switch condition as the previous case.
Node prevCase = (lastNonRemovable == n.getFirstChild())
? null : lastNonRemovable;
// Remove the default case if we can
if (isUselessCase(c, prevCase, c)) {
removeCase(n, c);
return null;
}
return c;
} else {
checkState(c.isCase());
if (c.getLastChild().hasChildren()
|| mayHaveSideEffects(c.getFirstChild())) {
lastNonRemovable = c;
}
}
}
return null;
}
/**
* Remove the case from the switch redeclaring any variables declared in it.
* @param caseNode The case to remove.
*/
private void removeCase(Node switchNode, Node caseNode) {
NodeUtil.redeclareVarsInsideBranch(caseNode);
caseNode.detach();
reportChangeToEnclosingScope(switchNode);
}
/**
* The function assumes that when checking a CASE node there is no DEFAULT_CASE node in the
* SWITCH, or the DEFAULT_CASE is the last case in the SWITCH.
*
* @return Whether the CASE or DEFAULT_CASE block does anything useful.
*/
private boolean isUselessCase(
Node caseNode, @Nullable Node previousCase, @Nullable Node defaultCase) {
checkState(previousCase == null || previousCase.getNext() == caseNode);
// A case isn't useless if a previous case falls through to it unless it happens to be the last
// case in the switch.
Node switchNode = caseNode.getParent();
if (switchNode.getLastChild() != caseNode && previousCase != null) {
Node previousBlock = previousCase.getLastChild();
if (!previousBlock.hasChildren()
|| !isExit(previousBlock.getLastChild())) {
return false;
}
}
Node executingCase = caseNode;
while (executingCase != null) {
checkState(executingCase.isDefaultCase() || executingCase.isCase());
// We only expect a DEFAULT case if the case we are checking is the
// DEFAULT case. Otherwise, we assume the DEFAULT case has already
// been removed.
checkState(caseNode == executingCase || !executingCase.isDefaultCase());
if (!executingCase.isDefaultCase() && mayHaveSideEffects(executingCase.getFirstChild())) {
// The case falls thru to a case whose condition has a potential side-effect,
// removing the candidate case would skip that side-effect, so don't.
return false;
}
Node block = executingCase.getLastChild();
checkState(block.isBlock());
if (block.hasChildren()) {
for (Node blockChild = block.getFirstChild();
blockChild != null;
blockChild = blockChild.getNext()) {
// If this is a block with a labelless break, it is useless.
switch (blockChild.getToken()) {
case BREAK:
// A case with a single labelless break is useless if it is the default case or if
// there is no default case. A break to a different control structure isn't useless.
return !blockChild.hasChildren()
&& (defaultCase == null || defaultCase == executingCase);
case VAR:
if (blockChild.hasOneChild()
&& blockChild.getFirstFirstChild() == null) {
// Variable declarations without initializations are OK.
continue;
}
return false;
default:
return false;
}
}
}
// Look at the fallthrough case
executingCase = executingCase.getNext();
}
return true;
}
/**
* @return Whether the node is an obvious control flow exit.
*/
private static boolean isExit(Node n) {
switch (n.getToken()) {
case BREAK:
case CONTINUE:
case RETURN:
case THROW:
return true;
default:
return false;
}
}
private Node tryFoldComma(Node n) {
// If the left side does nothing replace the comma with the result.
Node parent = n.getParent();
Node left = n.getFirstChild();
Node right = left.getNext();
left = trySimplifyUnusedResult(left);
if (left == null || !mayHaveSideEffects(left)) {
// Fold it!
right.detach();
n.replaceWith(right);
reportChangeToEnclosingScope(parent);
return right;
}
return n;
}
/** Try removing unneeded block nodes and their useless children */
@Nullable
Node tryOptimizeBlock(Node n) {
// Remove any useless children
for (Node c = n.getFirstChild(); c != null; ) {
Node next = c.getNext(); // save c.next, since 'c' may be removed
if (!isUnremovableNode(c) && !mayHaveSideEffects(c)) {
checkNormalization(!NodeUtil.isFunctionDeclaration(n), "function declaration");
// TODO(johnlenz): determine what this is actually removing. Candidates
// include: EMPTY nodes, control structures without children
// (removing infinite loops), empty try blocks. What else?
c.detach();
reportChangeToEnclosingScope(n);
markFunctionsDeleted(c);
} else {
tryOptimizeConditionalAfterAssign(c);
}
c = next;
}
if (n.isSyntheticBlock() || n.isScript() || n.getParent() == null) {
return n;
}
// Try to merge the block with its parent, or remove it if it is an empty class static block.
Node parent = n.getParent();
if (NodeUtil.tryMergeBlock(n, isASTNormalized())) {
reportChangeToEnclosingScope(parent);
return null;
} else if (parent.isClassMembers() && !n.hasChildren()) {
n.detach();
reportChangeToEnclosingScope(parent);
return null;
}
return n;
}
/**
* Some nodes that are unremovable don't have side effects so they aren't caught by
* mayHaveSideEffects
*/
private static boolean isUnremovableNode(Node n) {
return (n.isBlock() && n.isSyntheticBlock()) || n.isScript();
}
// TODO(johnlenz): Consider moving this to a separate peephole pass.
/**
* Attempt to replace the condition of if or hook immediately that is a
* reference to a name that is assigned immediately before.
*/
private void tryOptimizeConditionalAfterAssign(Node n) {
Node next = n.getNext();
// Look for patterns like the following and replace the if-condition with
// a constant value so it can later be folded:
// var a = /a/;
// if (a) {foo(a)}
// or
// a = 0;
// a ? foo(a) : c;
// or
// a = 0;
// a || foo(a);
// or
// a = 0;
// a && foo(a)
// or
// a = 0;
// a ?? foo(a)
// TODO(johnlenz): This would be better handled by control-flow sensitive
// constant propagation. As the other case that I want to handle is:
// i=0; for(;i<0;i++){}
// as right now nothing facilitates removing a loop like that.
// This is here simply to remove the cruft left behind goog.userAgent and
// similar cases.
if (!isSimpleAssignment(n)) {
return;
}
Node conditionalRoot = getConditionalRoot(next);
if (conditionalRoot == null) {
return;
}
Node lhsAssign = getSimpleAssignmentName(n);
Node condition = getConditionalStatementCondition(next);
if (!lhsAssign.matchesName(condition)) {
return;
}
Node rhsAssign = getSimpleAssignmentValue(n);
switch (conditionalRoot.getToken()) {
case AND:
case OR:
case IF:
case HOOK:
// conditionals that coerce their condition to a boolean
Tri value = NodeUtil.getBooleanValue(rhsAssign);
if (value != Tri.UNKNOWN) {
Node replacementConditionNode = NodeUtil.booleanNode(value.toBoolean(true));
condition.replaceWith(replacementConditionNode);
reportChangeToEnclosingScope(replacementConditionNode);
}
return;
case COALESCE:
// conditional that checks whether its operand is nullish
NodeUtil.ValueType valueType = NodeUtil.getKnownValueType(rhsAssign);
switch (valueType) {
case NULL:
case VOID:
condition.replaceWith(NodeUtil.newUndefinedNode(condition));
reportChangeToEnclosingScope(conditionalRoot);
break;
case NUMBER:
case BIGINT:
case STRING:
case BOOLEAN:
case OBJECT:
// semi-arbitrarily use '0' as a short non-nullish conditional
condition.replaceWith(IR.number(0).srcref(condition));
reportChangeToEnclosingScope(conditionalRoot);
break;
case UNDETERMINED:
break;
}
return;
default:
throw new AssertionError("Unhandled condition " + conditionalRoot);
}
}
/**
* @return whether the node is a assignment to a simple name, or simple var
* declaration with initialization.
*/
private static boolean isSimpleAssignment(Node n) {
// For our purposes we define a simple assignment to be a assignment
// to a NAME node, or a VAR declaration with one child and a initializer.
if (NodeUtil.isExprAssign(n)
&& n.getFirstFirstChild().isName()) {
return true;
} else if (NodeUtil.isNameDeclaration(n) && n.hasOneChild() && n.getFirstFirstChild() != null) {
return true;
}
return false;
}
/**
* @return The name being assigned to.
*/
private Node getSimpleAssignmentName(Node n) {
checkState(isSimpleAssignment(n));
if (NodeUtil.isExprAssign(n)) {
return n.getFirstFirstChild();
} else {
// A var declaration.
return n.getFirstChild();
}
}
/**
* @return The value assigned in the simple assignment
*/
private Node getSimpleAssignmentValue(Node n) {
checkState(isSimpleAssignment(n));
return n.getFirstChild().getLastChild();
}
/**
* @return the root node if a conditional statement or else null
*/
private @Nullable Node getConditionalRoot(Node n) {
// We defined a conditional statement to be a IF or EXPR_RESULT rooted with
// a HOOK, AND, or OR node.
if (n == null) {
return null;
}
if (n.isIf()) {
return n;
} else if (isExprConditional(n)) {
return n.getFirstChild();
}
return null;
}
/** @return Whether the node is a rooted with a HOOK, AND, OR, or COALESCE node. */
private static boolean isExprConditional(Node n) {
if (n.isExprResult()) {
switch (n.getFirstChild().getToken()) {
case HOOK:
case AND:
case OR:
case COALESCE:
return true;
default:
break;
}
}
return false;
}
/**
* @return The condition of a conditional statement.
*/
private Node getConditionalStatementCondition(Node n) {
if (n.isIf()) {
return NodeUtil.getConditionExpression(n);
} else {
checkState(isExprConditional(n));
return n.getFirstFirstChild();
}
}
/**
* Try folding IF nodes by removing dead branches.
*
* @return the replacement node, if changed, or the original if not
*/
private @Nullable Node tryFoldIf(Node n) {
checkState(n.isIf(), n);
Node parent = n.getParent();
checkNotNull(parent);
Token type = n.getToken();
Node cond = n.getFirstChild();
Node thenBody = cond.getNext();
Node elseBody = thenBody.getNext();
// if (x) { .. } else { } --> if (x) { ... }
if (elseBody != null && !mayHaveSideEffects(elseBody)) {
elseBody.detach();
reportChangeToEnclosingScope(n);
elseBody = null;
}
// if (x) { } else { ... } --> if (!x) { ... }
if (!mayHaveSideEffects(thenBody) && elseBody != null) {
elseBody.detach();
thenBody.replaceWith(elseBody);
Node notCond = new Node(Token.NOT);
cond.replaceWith(notCond);
reportChangeToEnclosingScope(n);
notCond.addChildToFront(cond);
cond = notCond;
thenBody = cond.getNext();
elseBody = null;
}
// `if (x()) { }` or `if (x?.()) { }`
if (!mayHaveSideEffects(thenBody) && elseBody == null) {
if (mayHaveSideEffects(cond)) {
// `x()` or `x?.()` has side effects, just leave the condition on its own.
cond.detach();
Node replacement = NodeUtil.newExpr(cond);
n.replaceWith(replacement);
reportChangeToEnclosingScope(parent);
return replacement;
} else {
// `x()` or `x?.()` has no side effects, the whole tree is useless now.
NodeUtil.removeChild(parent, n);
reportChangeToEnclosingScope(parent);
return null;
}
}
// Try transforms that apply to both IF and HOOK.
Tri condValue = NodeUtil.getBooleanValue(cond);
if (condValue == Tri.UNKNOWN) {
return n; // We can't remove branches otherwise!
}
if (mayHaveSideEffects(cond)) {
// Transform "if (a = 2) {x =2}" into "if (true) {a=2;x=2}"
boolean newConditionValue = condValue == Tri.TRUE;
// Add an elseBody if it is needed.
if (!newConditionValue && elseBody == null) {
elseBody = IR.block().srcref(n);
n.addChildToBack(elseBody);
}
Node newCond = NodeUtil.booleanNode(newConditionValue);
cond.replaceWith(newCond);
Node branchToKeep = newConditionValue ? thenBody : elseBody;
branchToKeep.addChildToFront(IR.exprResult(cond).srcref(cond));
reportChangeToEnclosingScope(branchToKeep);
cond = newCond;
}
boolean condTrue = condValue.toBoolean(true);
if (n.hasTwoChildren()) {
checkState(type == Token.IF);
if (condTrue) {
// Replace "if (true) { X }" with "X".
Node thenStmt = n.getSecondChild();
thenStmt.detach();
n.replaceWith(thenStmt);
reportChangeToEnclosingScope(thenStmt);
return thenStmt;
} else {
// Remove "if (false) { X }" completely.
NodeUtil.redeclareVarsInsideBranch(n);
NodeUtil.removeChild(parent, n);
reportChangeToEnclosingScope(parent);
markFunctionsDeleted(n);
return null;
}
} else {
// Replace "if (true) { X } else { Y }" with X, or
// replace "if (false) { X } else { Y }" with Y.
Node trueBranch = n.getSecondChild();
Node falseBranch = trueBranch.getNext();
Node branchToKeep = condTrue ? trueBranch : falseBranch;
Node branchToRemove = condTrue ? falseBranch : trueBranch;
NodeUtil.redeclareVarsInsideBranch(branchToRemove);
branchToKeep.detach();
n.replaceWith(branchToKeep);
reportChangeToEnclosingScope(branchToKeep);
markFunctionsDeleted(n);
return branchToKeep;
}
}
/**
* Try folding HOOK (?:) if the condition results of the condition is known.
* @return the replacement node, if changed, or the original if not
*/
private Node tryFoldHook(Node n) {
checkState(n.isHook(), n);
Node parent = n.getParent();
checkNotNull(parent);
Node cond = n.getFirstChild();
Node thenBody = cond.getNext();
Node elseBody = thenBody.getNext();
Tri condValue = NodeUtil.getBooleanValue(cond);
if (condValue == Tri.UNKNOWN) {
// If the result nodes are equivalent, then one of the nodes can be
// removed and it doesn't matter which.
if (!areNodesEqualForInlining(thenBody, elseBody)) {
return n; // We can't remove branches otherwise!
}
}
// Transform "(a = 2) ? x =2 : y" into "a=2,x=2"
Node branchToKeep;
Node branchToRemove;
if (condValue.toBoolean(true)) {
branchToKeep = thenBody;
branchToRemove = elseBody;
} else {
branchToKeep = elseBody;
branchToRemove = thenBody;
}
Node replacement;
boolean condHasSideEffects = mayHaveSideEffects(cond);
// Must detach after checking for side effects, to ensure that the parents
// of nodes are set correctly.
n.detachChildren();
if (condHasSideEffects) {
replacement = IR.comma(cond, branchToKeep).srcref(n);
} else {
replacement = branchToKeep;
markFunctionsDeleted(cond);
}
n.replaceWith(replacement);
reportChangeToEnclosingScope(replacement);
markFunctionsDeleted(branchToRemove);
return replacement;
}
/** Removes FORs that always evaluate to false. */
@Nullable
Node tryFoldFor(Node n) {
checkArgument(n.isVanillaFor());
Node init = n.getFirstChild();
Node cond = init.getNext();
Node increment = cond.getNext();
if (!init.isEmpty() && !NodeUtil.isNameDeclaration(init)) {
init = trySimplifyUnusedResult(init);
if (init == null) {
init = IR.empty().srcref(n);
n.addChildToFront(init);
}
}
if (!increment.isEmpty()) {
increment = trySimplifyUnusedResult(increment);
if (increment == null) {
increment = IR.empty().srcref(n);
increment.insertAfter(cond);
}
}
// There is an initializer skip it
if (!n.getFirstChild().isEmpty()) {
return n;
}
if (NodeUtil.getBooleanValue(cond) != Tri.FALSE) {
return n;
}
Node parent = n.getParent();
NodeUtil.redeclareVarsInsideBranch(n);
if (!mayHaveSideEffects(cond)) {
// Remove the entire loop and any associated labels.
while (parent.isLabel()) {
n = parent;
parent = parent.getParent();
}
n.detach();
} else {
Node statement = IR.exprResult(cond.detach()).srcrefIfMissing(cond);
if (parent.isLabel()) {
Node block = IR.block();
block.srcrefIfMissing(statement);
block.addChildToFront(statement);
statement = block;
}
n.replaceWith(statement);
}
reportChangeToEnclosingScope(parent);
return null;
}
/**
* Removes DOs that always evaluate to false. This leaves the
* statements that were in the loop in a BLOCK node.
* The block will be removed in a later pass, if possible.
*/
Node tryFoldDoAway(Node n) {
checkArgument(n.isDo());
Node cond = NodeUtil.getConditionExpression(n);
if (NodeUtil.getBooleanValue(cond) != Tri.FALSE) {
return n;
}
Node block = NodeUtil.getLoopCodeBlock(n);
if (n.getParent().isLabel() || hasUnnamedBreakOrContinue(block)) {
return n;
}
Node parent = n.getParent();
n.replaceWith(block.detach());
if (mayHaveSideEffects(cond)) {
Node condStatement = IR.exprResult(cond.detach()).srcref(cond);
condStatement.insertAfter(block);
}
reportChangeToEnclosingScope(parent);
return block;
}
/**
* Removes DOs that have empty bodies into FORs, which are
* much easier for the CFA to analyze.
*/
Node tryFoldEmptyDo(Node n) {
checkArgument(n.isDo());
Node body = NodeUtil.getLoopCodeBlock(n);
if (body.isBlock() && !body.hasChildren()) {
Node cond = NodeUtil.getConditionExpression(n);
Node forNode =
IR.forNode(IR.empty().srcref(n),
cond.detach(),
IR.empty().srcref(n),
body.detach());
n.replaceWith(forNode);
reportChangeToEnclosingScope(forNode);
return forNode;
}
return n;
}
/** Removes string keys with an empty pattern as their child */
Node tryOptimizeObjectPattern(Node pattern) {
checkArgument(pattern.isObjectPattern(), pattern);
if (pattern.hasChildren() && pattern.getLastChild().isRest()) {
// don't remove any elements in `const {f: [], ...rest} = obj` because that affects what's
// assigned to `rest`. only the last element can be object rest.
return pattern;
}
// remove trailing EMPTY nodes and empty destructuring patterns
for (Node child = pattern.getFirstChild(); child != null; ) {
Node key = child;
child = key.getNext(); // don't put this in the for loop since we might remove `child`
if (!key.isStringKey()) {
// don't try to remove rest or computed properties, since they might have side effects
continue;
}
if (isRemovableDestructuringTarget(key.getOnlyChild())) {
// e.g. `const {f: {}} = obj;`
key.detach();
reportChangeToEnclosingScope(pattern);
}
}
return pattern;
}
/** Removes trailing EMPTY nodes and empty array patterns */
Node tryOptimizeArrayPattern(Node pattern) {
checkArgument(pattern.isArrayPattern(), pattern);
for (Node lastChild = pattern.getLastChild(); lastChild != null; ) {
if (lastChild.isEmpty() || isRemovableDestructuringTarget(lastChild)) {
Node prev = lastChild.getPrevious();
lastChild.detach();
lastChild = prev;
reportChangeToEnclosingScope(pattern);
} else {
// don't remove any non-trailing empty nodes because that will change the ordering of the
// other assignments
// note that this case also covers array pattern rest, which must be the final element
break;
}
}
return pattern;
}
private boolean isRemovableDestructuringTarget(Node destructruringElement) {
Node target = destructruringElement;
Node defaultValue = null;
if (destructruringElement.isDefaultValue()) {
target = destructruringElement.getFirstChild();
defaultValue = destructruringElement.getSecondChild();
}
if (!target.isDestructuringPattern() || target.hasChildren()) {
return false;
}
// only remove default values without side effects
return defaultValue == null || !mayHaveSideEffects(defaultValue);
}
/**
* Returns whether a node has any unhandled breaks or continue.
*/
static boolean hasUnnamedBreakOrContinue(Node n) {
return NodeUtil.has(
n,
// Check for unlabeled breaks
(Node node) -> node.isBreak() && !node.hasChildren(),
// ...inside contexts that can contain breaks.
(Node node) ->
!IR.mayBeExpression(node) // Functions are not visited
&& !NodeUtil.isLoopStructure(node)
&& !node.isSwitch())
|| NodeUtil.has(
n,
// Check for unlabeled continues
(Node node) -> node.isContinue() && !node.hasChildren(),
// ...inside contexts that can contain continues.
(Node node) ->
!IR.mayBeExpression(node) // Functions are not visited
&& !NodeUtil.isLoopStructure(node));
}
/**
* Remove always true loop conditions.
*/
private void tryFoldForCondition(Node forCondition) {
if (getSideEffectFreeBooleanValue(forCondition) == Tri.TRUE) {
reportChangeToEnclosingScope(forCondition);
forCondition.replaceWith(IR.empty());
}
}
private Node tryRemoveOptionalCall(Node optionalCall) {
Node callee = optionalCall.getFirstChild();
if (!NodeUtil.isNullOrUndefined(callee)) {
return optionalCall;
}
final Node result;
if (this.mayHaveSideEffects(callee)) {
// Simplify `(void sideEffectFunction())?.()` to `(void sideEffectFunction())`
// The optional chain call won't execute but sideEffectFunction() is still evaluated.
optionalCall.replaceWith(callee.detach());
result = callee;
} else {
// Remove `(void 0)?.()` and (null)?.()
result = NodeUtil.newUndefinedNode(callee);
optionalCall.replaceWith(result);
}
this.markFunctionsDeleted(optionalCall);
this.reportChangeToEnclosingScope(result);
return result;
}
private static @Nullable IllegalStateException checkNormalization(
boolean condition, String feature) {
checkState(condition, "Unexpected %s. AST should be normalized.", feature);
return null;
}
}