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

com.google.javascript.jscomp.PeepholeSubstituteAlternateSyntax 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 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.checkState;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.CodingConvention.Bind;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;

/**
 * A peephole optimization that minimizes code by simplifying conditional
 * expressions, replacing IFs with HOOKs, replacing object constructors
 * with literals, and simplifying returns.
 */
class PeepholeSubstituteAlternateSyntax
  extends AbstractPeepholeOptimization {

  private final boolean late;

  private static final int STRING_SPLIT_OVERHEAD = ".split('.')".length();

  /**
   * @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 this is true, we would
   * do anything to minimize for size.
   */
  PeepholeSubstituteAlternateSyntax(boolean late) {
    this.late = late;
  }

  /**
   * Tries apply our various peephole minimizations on the passed in node.
   */
  @Override
  @SuppressWarnings("fallthrough")
  public Node optimizeSubtree(Node node) {
    switch (node.getToken()) {
      case ASSIGN_SUB:
        return reduceSubstractionAssignment(node);

      case TRUE:
      case FALSE:
        return reduceTrueFalse(node);

      case NEW:
        node = tryFoldStandardConstructors(node);
        if (!node.isCall()) {
          return node;
        }
        // Fall through on purpose because tryFoldStandardConstructors() may
        // convert a NEW node into a CALL node
      case CALL:
        Node result =  tryFoldLiteralConstructor(node);
        if (result == node) {
          result = tryFoldSimpleFunctionCall(node);
          if (result == node) {
            result = tryFoldImmediateCallToBoundFunction(node);
          }
        }
        return result;

      case RETURN:
        return tryReduceReturn(node);

      case EXPR_RESULT:
        if (node.getFirstChild().isComma()) {
          return trySplitComma(node);
        }
        return node;

      case NAME:
        return tryReplaceUndefined(node);

      case ARRAYLIT:
        return tryMinimizeArrayLiteral(node);

      case GETPROP:
        return tryMinimizeWindowRefs(node);

      case TEMPLATELIT:
        return tryTurnTemplateStringsToStrings(node);

      case MUL:
      case AND:
      case OR:
      case BITOR:
      case BITXOR:
      case BITAND:
      case COALESCE:
        return tryRotateAssociativeOperator(node);

      default:
        return node; //Nothing changed
    }
  }

  private static final ImmutableSet BUILTIN_EXTERNS = ImmutableSet.of(
      "Object",
      "Array",
      "Error",
      "RegExp",
      "Math");

  private Node tryMinimizeWindowRefs(Node node) {
    // Normalization needs to be done to ensure there's no shadowing.
    if (!isASTNormalized()) {
      return node;
    }

    checkArgument(node.isGetProp());

    if (node.getFirstChild().isName()) {
      Node nameNode = node.getFirstChild();

      // Since normalization has run we know we're referring to the global window.
      if ("window".equals(nameNode.getString()) && BUILTIN_EXTERNS.contains(node.getString())) {
        Node newNameNode = IR.name(node.getString());
        Node parentNode = node.getParent();

        newNameNode.srcref(node);
        node.replaceWith(newNameNode);

        if (parentNode.isCall() || parentNode.isOptChainCall()) {
          // e.g. when converting `window.Array?.()` to `Array?.()`, ensure that the
          // OPTCHAIN_CALL gets marked as `FREE_CALL`
          parentNode.putBooleanProp(Node.FREE_CALL, true);
        }
        reportChangeToEnclosingScope(parentNode);
        return newNameNode;
      }
    }

    return node;
  }

  private Node tryRotateAssociativeOperator(Node n) {
    if (!late) {
      return n;
    }
    // All commutative operators are also associative
    checkArgument(NodeUtil.isAssociative(n.getToken()));
    Node rhs = n.getLastChild();
    if (n.getToken() == rhs.getToken()) {
      // Transform a * (b * c) to a * b * c
      Node first = n.removeFirstChild();
      Node second = rhs.removeFirstChild();
      Node third = rhs.getLastChild().detach();
      Node newLhs = new Node(n.getToken(), first, second).srcrefIfMissing(n);
      Node newRoot = new Node(rhs.getToken(), newLhs, third).srcrefIfMissing(rhs);
      n.replaceWith(newRoot);
      reportChangeToEnclosingScope(newRoot);
      return newRoot;
    } else if (NodeUtil.isCommutative(n.getToken()) && !mayHaveSideEffects(n)) {
      // Transform a * (b / c) to b / c * a
      Node lhs = n.getFirstChild();
      while (lhs.getToken() == n.getToken()) {
        lhs = lhs.getFirstChild();
      }
      int precedence = NodeUtil.precedence(n.getToken());
      int lhsPrecedence = NodeUtil.precedence(lhs.getToken());
      int rhsPrecedence = NodeUtil.precedence(rhs.getToken());
      if (rhsPrecedence == precedence && lhsPrecedence != precedence) {
        rhs.detach();
        lhs.replaceWith(rhs);
        n.addChildToBack(lhs);
        reportChangeToEnclosingScope(n);
        return n;
      }
    }
    return n;
  }

  private Node tryFoldSimpleFunctionCall(Node n) {
    checkState(n.isCall(), n);
    Node callTarget = n.getFirstChild();
    if (callTarget == null || !callTarget.isName()) {
      return n;
    }
    String targetName = callTarget.getString();
    switch (targetName) {
      case "Boolean":
        {
          // Fold Boolean(a) to !!a
          // http://www.ecma-international.org/ecma-262/6.0/index.html#sec-boolean-constructor-boolean-value
          // and
          // http://www.ecma-international.org/ecma-262/6.0/index.html#sec-logical-not-operator-runtime-semantics-evaluation
          int paramCount = n.getChildCount() - 1;
          // only handle the single known parameter case
          if (paramCount == 1) {
            Node value = n.getLastChild().detach();
            Node replacement;
            if (NodeUtil.isBooleanResult(value)) {
              // If it is already a boolean do nothing.
              replacement = value;
            } else {
              // Replace it with a "!!value"
              replacement = IR.not(IR.not(value).srcref(n));
            }
            n.replaceWith(replacement);
            reportChangeToEnclosingScope(replacement);
          }
          break;
        }

      case "String":
        {
          // Fold String(a) to '' + (a) on immutable literals,
          // which allows further optimizations
          //
          // We can't do this in the general case, because String(a) has
          // slightly different semantics than '' + (a). See
          // https://blickly.github.io/closure-compiler-issues/#759
          Node value = callTarget.getNext();
          if (value != null && value.getNext() == null && NodeUtil.isImmutableValue(value)) {
            Node addition = IR.add(IR.string("").srcref(callTarget), value.detach());
            n.replaceWith(addition);
            reportChangeToEnclosingScope(addition);
            return addition;
          }
          break;
        }

      default:
        // nothing.
        break;
    }
    return n;
  }

  private Node tryFoldImmediateCallToBoundFunction(Node n) {
    // Rewriting "(fn.bind(a,b))()" to "fn.call(a,b)" makes it inlinable
    checkState(n.isCall());
    Node callTarget = n.getFirstChild();
    Bind bind = getCodingConvention()
        .describeFunctionBind(callTarget, false, false);
    if (bind != null) {
      // replace the call target
      bind.target.detach();
      callTarget.replaceWith(bind.target);
      callTarget = bind.target;

      // push the parameters
      addParameterAfter(bind.parameters, callTarget);

      // add the this value before the parameters if necessary
      if (bind.thisValue != null && !NodeUtil.isUndefined(bind.thisValue)) {
        // rewrite from "fn(a, b)" to "fn.call(thisValue, a, b)"
        Node newCallTarget = IR.getprop(callTarget.cloneTree(), "call");
        markNewScopesChanged(newCallTarget);
        callTarget.replaceWith(newCallTarget);
        markFunctionsDeleted(callTarget);
        bind.thisValue.cloneTree().insertAfter(newCallTarget);
        n.putBooleanProp(Node.FREE_CALL, false);
      } else {
        n.putBooleanProp(Node.FREE_CALL, true);
      }
      reportChangeToEnclosingScope(n);
    }
    return n;
  }

  private static void addParameterAfter(Node parameterList, Node after) {
    if (parameterList != null) {
      // push the last parameter to the head of the list first.
      addParameterAfter(parameterList.getNext(), after);
      parameterList.cloneTree().insertAfter(after);
    }
  }

  /**
   * Converts expressions of a potentially nested comma expression into a sequence of expression
   * result statements and inserts them into the AST.
   *
   * @param insert Whether or not the leftmost expression is inserted into the AST.
   * @return The leftmost expression.
   */
  private Node splitComma(Node n, boolean insert, Node insertAfter) {
    while (n.isComma()) {
      Node left = n.getFirstChild();
      Node right = n.getLastChild();
      n.detachChildren();
      if (right.isComma()) {
        splitComma(right, true, insertAfter);
      } else {
        // Add the right expression after the optimized expression.
        Node newStatement = IR.exprResult(right);
        newStatement.srcrefIfMissing(right);
        newStatement.insertAfter(insertAfter);
      }
      n = left;
    }
    if (insert) {
      Node newStatement = IR.exprResult(n);
      newStatement.srcrefIfMissing(n);
      newStatement.insertAfter(insertAfter);
      return newStatement;
    }
    return n;
  }

  private Node trySplitComma(Node n) {
    if (late) {
      return n;
    }
    checkState(n.isExprResult());
    if (n.getParent().isLabel()) {
      // Do not split labeled comma expressions.
      return n;
    }
    Node comma = n.getFirstChild();
    checkState(comma.isComma());
    Node leftmost = splitComma(comma, false, n);
    // Replace original expression with leftmost comma expression.
    n.removeChildren();
    n.addChildToFront(leftmost);
    n.srcref(leftmost);
    reportChangeToEnclosingScope(leftmost);
    return leftmost;
  }

  /**
   * Use "void 0" in place of "undefined"
   */
  private Node tryReplaceUndefined(Node n) {
    // TODO(johnlenz): consider doing this as a normalization.
    if (isASTNormalized()
        && NodeUtil.isUndefined(n)
        && !NodeUtil.isLValue(n)) {
      Node replacement = NodeUtil.newUndefinedNode(n);
      n.replaceWith(replacement);
      reportChangeToEnclosingScope(replacement);
      return replacement;
    }
    return n;
  }

  /**
   * Reduce "return undefined" or "return void 0" to simply "return".
   *
   * @return The original node, maybe simplified.
   */
  private Node tryReduceReturn(Node n) {
    Node result = n.getFirstChild();

    if (result != null) {
      switch (result.getToken()) {
        case VOID:
          Node operand = result.getFirstChild();
          if (!mayHaveSideEffects(operand)) {
            n.removeFirstChild();
            reportChangeToEnclosingScope(n);
          }
          break;
        case NAME:
          String name = result.getString();
          if (name.equals("undefined")) {
            n.removeFirstChild();
            reportChangeToEnclosingScope(n);
          }
          break;
        default:
          break;
      }
    }

    return n;
  }

  private static final ImmutableSet STANDARD_OBJECT_CONSTRUCTORS =
    // String, Number, and Boolean functions return non-object types, whereas
    // new String, new Number, and new Boolean return object types, so don't
    // include them here.
    ImmutableSet.of(
      "Object",
      "Array",
      "Error"
      );

  /**
   * Fold "new Object()" to "Object()".
   */
  private Node tryFoldStandardConstructors(Node n) {
    checkState(n.isNew());

    if (canFoldStandardConstructors(n)) {
      n.setToken(Token.CALL);
      n.putBooleanProp(Node.FREE_CALL, true);
      reportChangeToEnclosingScope(n);
    }

    return n;
  }

  /**
   * @return Whether "new Object()" can be folded to "Object()" on {@code n}.
   */
  private boolean canFoldStandardConstructors(Node n) {
    // If name normalization has been run then we know that
    // new Object() does in fact refer to what we think it is
    // and not some custom-defined Object().
    if (isASTNormalized() && n.getFirstChild().isName()) {
      String className = n.getFirstChild().getString();
      if (STANDARD_OBJECT_CONSTRUCTORS.contains(className)) {
        return true;
      }
      if ("RegExp".equals(className)) {
        // Fold "new RegExp()" to "RegExp()", but only if the argument is a string.
        // See issue 1260.
        if (n.getSecondChild() == null || n.getSecondChild().isStringLit()) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Replaces a new Array, Object, or RegExp node with a literal, unless the
   * call is to a local constructor function with the same name.
   */
  private Node tryFoldLiteralConstructor(Node n) {
    checkArgument(n.isCall() || n.isNew());

    Node constructorNameNode = n.getFirstChild();

    Node newLiteralNode = null;

    // We require the AST to be normalized to ensure that, say,
    // Object() really refers to the built-in Object constructor
    // and not a user-defined constructor with the same name.

    if (isASTNormalized() && constructorNameNode.isName()) {

      String className = constructorNameNode.getString();

      boolean constructorHasArgs = constructorNameNode.getNext() != null;

      if ("Object".equals(className) && !constructorHasArgs) {
        // "Object()" --> "{}"
        newLiteralNode = IR.objectlit();
      } else if ("Array".equals(className)) {
        // "Array(arg0, arg1, ...)" --> "[arg0, arg1, ...]"
        Node arg0 = constructorNameNode.getNext();
        FoldArrayAction action = isSafeToFoldArrayConstructor(arg0);

        if (action == FoldArrayAction.SAFE_TO_FOLD_WITH_ARGS
            || action == FoldArrayAction.SAFE_TO_FOLD_WITHOUT_ARGS) {
          newLiteralNode = IR.arraylit();
          n.removeFirstChild(); // discard the function name
          Node elements = n.removeChildren();
          if (action == FoldArrayAction.SAFE_TO_FOLD_WITH_ARGS) {
            newLiteralNode.addChildrenToFront(elements);
          }
        }
      }

      if (newLiteralNode != null) {
        n.replaceWith(newLiteralNode);
        reportChangeToEnclosingScope(newLiteralNode);
        return newLiteralNode;
      }
    }
    return n;
  }

  private static enum FoldArrayAction {
    NOT_SAFE_TO_FOLD, SAFE_TO_FOLD_WITH_ARGS, SAFE_TO_FOLD_WITHOUT_ARGS}

  /**
   * Checks if it is safe to fold Array() constructor into []. It can be
   * obviously done, if the initial constructor has either no arguments or
   * at least two. The remaining case may be unsafe since Array(number)
   * actually reserves memory for an empty array which contains number elements.
   */
  private static FoldArrayAction isSafeToFoldArrayConstructor(Node arg) {
    FoldArrayAction action = FoldArrayAction.NOT_SAFE_TO_FOLD;

    if (arg == null) {
      action = FoldArrayAction.SAFE_TO_FOLD_WITHOUT_ARGS;
    } else if (arg.getNext() != null) {
      action = FoldArrayAction.SAFE_TO_FOLD_WITH_ARGS;
    } else {
      switch (arg.getToken()) {
        case STRINGLIT:
          // "Array('a')" --> "['a']"
          action = FoldArrayAction.SAFE_TO_FOLD_WITH_ARGS;
          break;
        case NUMBER:
          // "Array(0)" --> "[]"
          if (arg.getDouble() == 0) {
            action = FoldArrayAction.SAFE_TO_FOLD_WITHOUT_ARGS;
          }
          break;
        case ARRAYLIT:
          // "Array([args])" --> "[[args]]"
          action = FoldArrayAction.SAFE_TO_FOLD_WITH_ARGS;
          break;
        default:
      }
    }
    return action;
  }

  private Node reduceSubstractionAssignment(Node n) {
    Node right = n.getLastChild();
    boolean isNegative = false;
    if (right.isNeg()) {
      isNegative = true;
      right = right.getOnlyChild();
    }

    if (right.isNumber() && right.getDouble() == 1) {
      Node left = n.removeFirstChild();
      Node newNode = isNegative ? IR.inc(left, false) : IR.dec(left, false);
      n.replaceWith(newNode);
      reportChangeToEnclosingScope(newNode);
      return newNode;
    }

    return n;
  }

  private Node reduceTrueFalse(Node n) {
    if (late) {
      switch (n.getParent().getToken()) {
        case EQ:
        case GT:
        case GE:
        case LE:
        case LT:
        case NE:
          Node number = IR.number(n.isTrue() ? 1 : 0);
          n.replaceWith(number);
          reportChangeToEnclosingScope(number);
          return number;
        default:
          break;
      }

      Node not = IR.not(IR.number(n.isTrue() ? 0 : 1));
      not.srcrefTreeIfMissing(n);
      n.replaceWith(not);
      reportChangeToEnclosingScope(not);
      return not;
    }
    return n;
  }

  private Node tryMinimizeArrayLiteral(Node n) {
    boolean allStrings = true;
    for (Node cur = n.getFirstChild(); cur != null; cur = cur.getNext()) {
      if (!cur.isStringLit()) {
        allStrings = false;
      }
    }

    if (allStrings) {
      return tryMinimizeStringArrayLiteral(n);
    } else {
      return n;
    }
  }

  private Node tryMinimizeStringArrayLiteral(Node n) {
    if (!late) {
      return n;
    }

    int numElements = n.getChildCount();
    // We save two bytes per element.
    int saving = numElements * 2 - STRING_SPLIT_OVERHEAD;
    if (saving <= 0) {
      return n;
    }

    String[] strings = new String[numElements];
    int idx = 0;
    for (Node cur = n.getFirstChild(); cur != null; cur = cur.getNext()) {
      strings[idx++] = cur.getString();
    }

    // These delimiters are chars that appears a lot in the program therefore
    // probably have a small Huffman encoding.
    String delimiter = pickDelimiter(strings);
    if (delimiter != null) {
      String template = Joiner.on(delimiter).join(strings);
      Node call = IR.call(IR.getprop(IR.string(template), "split"), IR.string("" + delimiter));
      call.srcrefTreeIfMissing(n);
      n.replaceWith(call);
      reportChangeToEnclosingScope(call);
      return call;
    }
    return n;
  }

  private Node tryTurnTemplateStringsToStrings(Node n) {
    checkState(n.isTemplateLit(), n);
    if (n.getParent().isTaggedTemplateLit()) {
      return n;
    }
    String string = getSideEffectFreeStringValue(n);
    if (string == null) {
      return n;
    }
    Node stringNode = IR.string(string).srcref(n);
    n.replaceWith(stringNode);
    reportChangeToEnclosingScope(stringNode);
    return stringNode;
  }

  /**
   * Find a delimiter that does not occur in the given strings
   * @param strings The strings that must be separated.
   * @return a delimiter string or null
   */
  private static String pickDelimiter(String[] strings) {
    boolean allLength1 = true;
    for (String s : strings) {
      if (s.length() != 1) {
        allLength1 = false;
        break;
      }
    }

    if (allLength1) {
      return "";
    }

    String[] delimiters = new String[]{" ", ";", ",", "{", "}", null};
    int i = 0;
    NEXT_DELIMITER: for (; delimiters[i] != null; i++) {
      for (String cur : strings) {
        if (cur.contains(delimiters[i])) {
          continue NEXT_DELIMITER;
        }
      }
      break;
    }
    return delimiters[i];
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy