All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.PeepholeRemoveDeadCode Maven / Gradle / Ivy

Go to download

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.

There is a newer version: v20240317
Show newest version
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy