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

com.google.javascript.jscomp.ExploitAssigns 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. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2006 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 com.google.javascript.rhino.Node;

/**
 * Tries to chain assignments together.
 *
 * @author [email protected] (Nick Santos)
 *
 */
class ExploitAssigns extends AbstractPeepholeOptimization {

  @Override
  Node optimizeSubtree(Node subtree) {
    for (Node child = subtree.getFirstChild(); child != null;) {
      Node next = child.getNext();
      if (NodeUtil.isExprAssign(child)) {
        collapseAssign(child.getFirstChild(), child, subtree);
      }
      child = next;
    }
    return subtree;
  }

  /** Try to collapse the given assign into subsequent expressions. */
  private void collapseAssign(Node assign, Node expr, Node exprParent) {
    Node leftValue = assign.getFirstChild();
    Node rightValue = leftValue.getNext();

    if (leftValue.isDestructuringPattern()) {
      // We don't collapse expressions containing these because they can have side effects:
      //   - Reassigning RHS names. (e.g. `() => { ({c: a} = a); return a; }`).
      //   - Calling a getter defined on the RHS.
      //   - Evaluating a default value.
      // TODO(b/123102446): Check for these issues and optimize when they aren't present.
      return;
    } else if (isCollapsibleValue(leftValue, true)
        && collapseAssignEqualTo(expr, exprParent, leftValue)) {
      // Condition has side-effects.
    } else if (isCollapsibleValue(rightValue, false)
        && collapseAssignEqualTo(expr, exprParent, rightValue)) {
      // Condition has side-effects.
    } else if (rightValue.isAssign()) {
      // Recursively deal with nested assigns.
      collapseAssign(rightValue, expr, exprParent);
    }
  }

  /**
   * Determines whether we know enough about the given value to be able
   * to collapse it into subsequent expressions.
   *
   * For example, we can collapse booleans and variable names:
   * 
   * x = 3; y = x; // y = x = 3;
   * a = true; b = true; // b = a = true;
   * 
   * But we won't try to collapse complex expressions.
   *
   * @param value The value node.
   * @param isLValue Whether it's on the left-hand side of an expr.
   */
  private static boolean isCollapsibleValue(Node value, boolean isLValue) {
    switch (value.getToken()) {
      case GETPROP:
        // Do not collapse GETPROPs on arbitrary objects, because
        // they may be implemented setter functions, and oftentimes
        // setter functions fail on native objects. This is OK for "THIS"
        // objects, because we assume that they are non-native.
        return !isLValue || value.getFirstChild().isThis();

      case NAME:
        return true;

      default:
        return NodeUtil.isImmutableValue(value);
    }
  }

  /**
   * Collapse the given assign expression into the expression directly following it, if possible.
   *
   * @param expr The expression that may be moved.
   * @param exprParent The parent of {@code expr}.
   * @param value The value of this expression, expressed as a node. Each expression may have
   *     multiple values, so this function may be called multiple times for the same expression. For
   *     example, 
   *     a = true;
   *      is equal to the name "a" and the boolean "true".
   * @return Whether the expression was collapsed successfully.
   */
  private boolean collapseAssignEqualTo(Node expr, Node exprParent, Node value) {
    Node assign = expr.getFirstChild();
    Node parent = exprParent;
    Node next = expr.getNext();
    while (next != null) {
      switch (next.getToken()) {
        case AND:
        case OR:
        case HOOK:
        case IF:
        case RETURN:
        case EXPR_RESULT:
          // Dive down the left side
          parent = next;
          next = next.getFirstChild();
          break;

        case CONST:
        case LET:
        case VAR:
          if (next.getFirstChild().hasChildren()) {
            parent = next.getFirstChild();
            next = parent.getFirstChild();
            break;
          }
          return false;

        case GETPROP:
        case NAME:
          if (next.isQualifiedName()) {
            if (value.isQualifiedName() &&
                next.matchesQualifiedName(value)) {
              // If the previous expression evaluates to value of a
              // qualified name, and that qualified name is used again
              // shortly, then we can exploit the assign here.

              // Verify the assignment doesn't change its own value.
              if (!isSafeReplacement(next, assign)) {
                return false;
              }

              exprParent.removeChild(expr);
              expr.removeChild(assign);
              parent.replaceChild(next, assign);
              reportChangeToEnclosingScope(parent);
              return true;
            }
          }
          return false;

        case ASSIGN:
          // Assigns are really tricky. In lots of cases, we want to inline
          // into the right side of the assign. But the left side of the
          // assign is evaluated first, and it may have convoluted logic:
          //   a = null;
          //   (a = b).c = null;
          // We don't want to exploit the first assign. Similarly:
          //   a.b = null;
          //   a.b.c = null;
          // We don't want to exploit the first assign either.
          //
          // To protect against this, we simply only inline when the left side
          // is guaranteed to evaluate to the same L-value no matter what.
          Node leftSide = next.getFirstChild();
          if (leftSide.isName() || (leftSide.isGetProp() && leftSide.getFirstChild().isThis())) {
            // Dive down the right side of the assign.
            parent = next;
            next = leftSide.getNext();
            break;
          } else {
            return false;
          }

        default:
          if (NodeUtil.isImmutableValue(next)
              && next.isEquivalentTo(value)) {
            // If the r-value of the expr assign is an immutable value,
            // and the value is used again shortly, then we can exploit
            // the assign here.
            exprParent.removeChild(expr);
            expr.removeChild(assign);
            parent.replaceChild(next, assign);
            reportChangeToEnclosingScope(parent);
            return true;
          }
          // Return without inlining a thing
          return false;
      }
    }

    return false;
  }

  /**
   * Checks name referenced in node to determine if it might have
   * changed.
   * @return Whether the replacement can be made.
   */
  private boolean isSafeReplacement(Node node, Node replacement) {
    // No checks are needed for simple names.
    if (node.isName()) {
      return true;
    }
    checkArgument(node.isGetProp());

    while (node.isGetProp()) {
      node = node.getFirstChild();
    }
    return !(node.isName() && isNameAssignedTo(node.getString(), replacement));

  }

  /**
   * @return Whether name is assigned in the expression rooted at node.
   */

  private static boolean isNameAssignedTo(String name, Node node) {
    for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
      if (isNameAssignedTo(name, c)) {
        return true;
      }
    }

    if (node.isName()) {
      Node parent = node.getParent();
      if (parent.isAssign() && parent.getFirstChild() == node) {
        if (name.equals(node.getString())) {
          return true;
        }
      }
    }

    return false;
  }
}