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

com.google.javascript.jscomp.PeepholeReplaceKnownMethods 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 2011 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.Strings.isNullOrEmpty;

import com.google.common.base.Preconditions;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

/**
 * Just to fold known methods when they are called with constants.
 *
 */
class PeepholeReplaceKnownMethods extends AbstractPeepholeOptimization {

  private final boolean late;
  private final boolean useTypes;

  /**
   * @param late When late is true, this mean we are currently running after
   * most of the other optimizations. In this case we avoid changes that make
   * the code larger (but otherwise easier to analyze - such as using string
   * splitting).
   */
  PeepholeReplaceKnownMethods(boolean late, boolean useTypes) {
    this.late = late;
    this.useTypes = useTypes;
  }

  @Override
  Node optimizeSubtree(Node subtree) {
    if (subtree.isCall()){
      return tryFoldKnownMethods(subtree);
    }
    return subtree;
  }

  private Node tryFoldKnownMethods(Node subtree) {
    // For now we only support string methods .join(),
    // .indexOf(), .substring() and .substr()
    // and numeric methods parseInt() and parseFloat().

    subtree = tryFoldArrayJoin(subtree);

    if (subtree.isCall()) {
      Node callTarget = subtree.getFirstChild();
      if (callTarget == null) {
        return subtree;
      }

      if (NodeUtil.isGet(callTarget)) {
        subtree = tryFoldKnownStringMethods(subtree, callTarget);
      } else if (callTarget.isName()) {
        subtree = tryFoldKnownNumericMethods(subtree, callTarget);
      }
    }

    return subtree;
  }

  /**
   * Try to evaluate known String methods
   *    .indexOf(), .substr(), .substring()
   */
  private Node tryFoldKnownStringMethods(Node subtree, Node callTarget) {
    Preconditions.checkArgument(subtree.isCall());

    // check if this is a call on a string method
    // then dispatch to specific folding method.
    Node stringNode = callTarget.getFirstChild();
    Node functionName = callTarget.getLastChild();

    if (!functionName.isString()) {
      return subtree;
    }

    boolean isStringLiteral = stringNode.isString();
    String functionNameString = functionName.getString();
    Node firstArg = callTarget.getNext();
    if (isStringLiteral) {
      if (functionNameString.equals("split")) {
        return tryFoldStringSplit(subtree, stringNode, firstArg);
      } else if (firstArg == null) {
        switch (functionNameString) {
          case "toLowerCase":
            return tryFoldStringToLowerCase(subtree, stringNode);
          case "toUpperCase":
            return tryFoldStringToUpperCase(subtree, stringNode);
        }
      } else {
        if (NodeUtil.isImmutableValue(firstArg)) {
          switch (functionNameString) {
            case "indexOf":
            case "lastIndexOf":
              return tryFoldStringIndexOf(subtree, functionNameString, stringNode, firstArg);
            case "substr":
              return tryFoldStringSubstr(subtree, stringNode, firstArg);
            case "substring":
            case "slice":
              return tryFoldStringSubstringOrSlice(subtree, stringNode, firstArg);
            case "charAt":
              return tryFoldStringCharAt(subtree, stringNode, firstArg);
            case "charCodeAt":
              return tryFoldStringCharCodeAt(subtree, stringNode, firstArg);
          }
        }
      }
    }
    if (useTypes
        && firstArg != null
        && (isStringLiteral
            || (stringNode.getTypeI() != null
                && stringNode.getTypeI().isStringValueType()))) {
      if (subtree.hasXChildren(3)) {
        Double maybeStart = NodeUtil.getNumberValue(firstArg, useTypes);
        if (maybeStart != null) {
          int start = maybeStart.intValue();
          Double maybeLengthOrEnd = NodeUtil.getNumberValue(firstArg.getNext(), useTypes);
          if (maybeLengthOrEnd != null) {
            switch (functionNameString) {
              case "substr":
                int length = maybeLengthOrEnd.intValue();
                if (start >= 0 && length == 1) {
                  return replaceWithCharAt(subtree, callTarget, firstArg);
                }
                break;
              case "substring":
              case "slice":
                int end = maybeLengthOrEnd.intValue();
                if (end - start == 1) {
                  return replaceWithCharAt(subtree, callTarget, firstArg);
                }
            }
          }
        }
      }
    }
    return subtree;
  }

  /**
   * Try to evaluate known Numeric methods
   *    parseInt(), parseFloat()
   */
  private Node tryFoldKnownNumericMethods(Node subtree, Node callTarget) {
    Preconditions.checkArgument(subtree.isCall());

    if (isASTNormalized()) {
      // check if this is a call on a string method
      // then dispatch to specific folding method.
      String functionNameString = callTarget.getString();
      Node firstArgument = callTarget.getNext();
      if ((firstArgument != null) && (firstArgument.isString() || firstArgument.isNumber())
          && (functionNameString.equals("parseInt") || functionNameString.equals("parseFloat"))) {
        subtree = tryFoldParseNumber(subtree, functionNameString, firstArgument);
      }
    }
    return subtree;
  }

  /**
   * @return The lowered string Node.
   */
  private Node tryFoldStringToLowerCase(Node subtree, Node stringNode) {
    // From Rhino, NativeString.java. See ECMA 15.5.4.11
    String lowered = stringNode.getString().toLowerCase(Locale.ROOT);
    Node replacement = IR.string(lowered);
    subtree.replaceWith(replacement);
    reportCodeChange();
    return replacement;
  }

  /**
   * @return The upped string Node.
   */
  private Node tryFoldStringToUpperCase(Node subtree, Node stringNode) {
    // From Rhino, NativeString.java. See ECMA 15.5.4.12
    String upped = stringNode.getString().toUpperCase(Locale.ROOT);
    Node replacement = IR.string(upped);
    subtree.replaceWith(replacement);
    reportCodeChange();
    return replacement;
  }

  /**
   * @param input string representation of a number
   * @return string with leading and trailing zeros removed
   */
  private static String normalizeNumericString(String input) {
    if (isNullOrEmpty(input)) {
      return input;
    }

    int startIndex = 0;
    int endIndex = input.length() - 1;

    // Remove leading zeros
    while (startIndex < input.length() && input.charAt(startIndex) == '0' &&
        input.charAt(startIndex) != '.') {
      startIndex++;
    }

    // Remove trailing zeros only after the decimal
    if (input.indexOf('.') >= 0) {
      while (endIndex >= 0 && input.charAt(endIndex) == '0') {
        endIndex--;
      }
      if (input.charAt(endIndex) == '.') {
        endIndex--;
      }
    }
    if (startIndex >= endIndex) {
      return input;
    }

    return input.substring(startIndex, endIndex + 1);
  }

  /**
   * Try to evaluate parseInt, parseFloat:
   *     parseInt("1") -> 1
   *     parseInt("1", 10) -> 1
   *     parseFloat("1.11") -> 1.11
   */
  private Node tryFoldParseNumber(
      Node n, String functionName, Node firstArg) {
    Preconditions.checkArgument(n.isCall());

    boolean isParseInt = functionName.equals("parseInt");
    Node secondArg = firstArg.getNext();

    // Second argument is only used as the radix for parseInt
    int radix = 0;
    if (secondArg != null) {
      if (!isParseInt) {
        return n;
      }

      // Third-argument and non-numeric second arg are problematic. Discard.
      if (secondArg.getNext() != null || !secondArg.isNumber()) {
        return n;
      } else {
        double tmpRadix = secondArg.getDouble();
        if (tmpRadix != (int) tmpRadix) {
          return n;
        }
        radix = (int) tmpRadix;
        if (radix < 0 || radix == 1 || radix > 36) {
          return n;
        }
      }
    }

    // stringVal must be a valid string.
    String stringVal = null;
    Double checkVal;
    if (firstArg.isNumber()) {
      checkVal = NodeUtil.getNumberValue(firstArg);
      if (!(radix == 0 || radix == 10) && isParseInt) {
        //Convert a numeric first argument to a different base
        stringVal = String.valueOf(checkVal.intValue());
      } else {
        // If parseFloat is called with a numeric argument,
        // replace it with just the number.
        // If parseInt is called with a numeric first argument and the radix
        // is 10 or omitted, just replace it with the number
        Node numericNode;
        if (isParseInt) {
          numericNode = IR.number(checkVal.intValue());
        } else {
          numericNode = IR.number(checkVal);
        }
        n.replaceWith(numericNode);
        reportCodeChange();
        return numericNode;
      }
    } else {
      stringVal = NodeUtil.getStringValue(firstArg);
      if (stringVal == null) {
        return n;
      }

      //Check that the string is in a format we can recognize
      checkVal = NodeUtil.getStringNumberValue(stringVal);
      if (checkVal == null) {
        return n;
      }

      stringVal = NodeUtil.trimJsWhiteSpace(stringVal);
      if (stringVal.isEmpty()) {
        return n;
      }
    }

    Node newNode;
    if (stringVal.equals("0")) {
      // Special case for parseInt("0") or parseFloat("0")
      newNode = IR.number(0);
    } else if (isParseInt) {
      if (radix == 0 || radix == 16) {
        if (stringVal.length() > 1 &&
            stringVal.substring(0, 2).equalsIgnoreCase("0x")) {
          radix = 16;
          stringVal = stringVal.substring(2);
        } else if (radix == 0) {
          // if a radix is not specified or is 0 and the most
          // significant digit is "0", the string will parse
          // with a radix of 8 on some browsers, so leave
          // this case alone. This check does not apply in
          // script mode ECMA5 or greater
          if (!isEcmaScript5OrGreater() &&
              stringVal.substring(0, 1).equals("0")) {
            return n;
          }

          radix = 10;
        }
      }
      int newVal = 0;
      try {
        newVal = Integer.parseInt(stringVal, radix);
      } catch (NumberFormatException e) {
        return n;
      }

      newNode = IR.number(newVal);
    } else {
      String normalizedNewVal = "0";
      try {
        double newVal = Double.parseDouble(stringVal);
        newNode = IR.number(newVal);
        normalizedNewVal = normalizeNumericString(String.valueOf(newVal));
      } catch (NumberFormatException e) {
        return n;
      }
      // Make sure that the parsed number matches the original string
      // This prevents rounding differences between the Java implementation
      // and native script.
      if (!normalizeNumericString(stringVal).equals(normalizedNewVal)) {
        return n;
      }
    }

    n.replaceWith(newNode);

    reportCodeChange();

    return newNode;
  }

  /**
   * Try to evaluate String.indexOf/lastIndexOf:
   *     "abcdef".indexOf("bc") -> 1
   *     "abcdefbc".indexOf("bc", 3) -> 6
   */
  private Node tryFoldStringIndexOf(
      Node n, String functionName, Node lstringNode, Node firstArg) {
    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(lstringNode.isString());

    String lstring = NodeUtil.getStringValue(lstringNode);
    boolean isIndexOf = functionName.equals("indexOf");
    Node secondArg = firstArg.getNext();
    String searchValue = NodeUtil.getStringValue(firstArg);
    // searchValue must be a valid string.
    if (searchValue == null) {
      return n;
    }
    int fromIndex = isIndexOf ? 0 : lstring.length();
    if (secondArg != null) {
      // Third-argument and non-numeric second arg are problematic. Discard.
      if (secondArg.getNext() != null || !secondArg.isNumber()) {
        return n;
      } else {
        fromIndex = (int) secondArg.getDouble();
      }
    }
    int indexVal = isIndexOf ? lstring.indexOf(searchValue, fromIndex)
                             : lstring.lastIndexOf(searchValue, fromIndex);
    Node newNode = IR.number(indexVal);
    n.replaceWith(newNode);

    reportCodeChange();

    return newNode;
  }

  /**
   * Try to fold an array join: ['a', 'b', 'c'].join('') -> 'abc';
   */
  private Node tryFoldArrayJoin(Node n) {
    Node callTarget = n.getFirstChild();

    if (callTarget == null || !callTarget.isGetProp()) {
      return n;
    }

    Node right = callTarget.getNext();
    if (right != null) {
      if (right.getNext() != null || !NodeUtil.isImmutableValue(right)) {
        return n;
      }
    }

    Node arrayNode = callTarget.getFirstChild();
    Node functionName = arrayNode.getNext();

    if (!arrayNode.isArrayLit() ||
        !functionName.getString().equals("join")) {
      return n;
    }

    if (right != null && right.isString()
        && ",".equals(right.getString())) {
      // "," is the default, it doesn't need to be explicit
      n.removeChild(right);
      reportCodeChange();
    }

    String joinString = (right == null) ? "," : NodeUtil.getStringValue(right);
    List arrayFoldedChildren = new LinkedList<>();
    StringBuilder sb = null;
    int foldedSize = 0;
    Node prev = null;
    Node elem = arrayNode.getFirstChild();
    // Merges adjacent String nodes.
    while (elem != null) {
      if (NodeUtil.isImmutableValue(elem) || elem.isEmpty()) {
        if (sb == null) {
          sb = new StringBuilder();
        } else {
          sb.append(joinString);
        }
        sb.append(NodeUtil.getArrayElementStringValue(elem));
      } else {
        if (sb != null) {
          Preconditions.checkNotNull(prev);
          // + 2 for the quotes.
          foldedSize += sb.length() + 2;
          arrayFoldedChildren.add(
              IR.string(sb.toString()).useSourceInfoIfMissingFrom(prev));
          sb = null;
        }
        foldedSize += InlineCostEstimator.getCost(elem);
        arrayFoldedChildren.add(elem);
      }
      prev = elem;
      elem = elem.getNext();
    }

    if (sb != null) {
      Preconditions.checkNotNull(prev);
      // + 2 for the quotes.
      foldedSize += sb.length() + 2;
      arrayFoldedChildren.add(
          IR.string(sb.toString()).useSourceInfoIfMissingFrom(prev));
    }
    // one for each comma.
    foldedSize += arrayFoldedChildren.size() - 1;

    int originalSize = InlineCostEstimator.getCost(n);
    switch (arrayFoldedChildren.size()) {
      case 0:
        Node emptyStringNode = IR.string("");
        n.getParent().replaceChild(n, emptyStringNode);
        reportCodeChange();
        return emptyStringNode;
      case 1:
        Node foldedStringNode = arrayFoldedChildren.remove(0);
        if (foldedSize > originalSize) {
          return n;
        }
        arrayNode.detachChildren();
        if (!foldedStringNode.isString()) {
          // If the Node is not a string literal, ensure that
          // it is coerced to a string.
          Node replacement = IR.add(
              IR.string("").srcref(n),
              foldedStringNode);
          foldedStringNode = replacement;
        }
        n.getParent().replaceChild(n, foldedStringNode);
        reportCodeChange();
        return foldedStringNode;
      default:
        // No folding could actually be performed.
        if (arrayNode.hasXChildren(arrayFoldedChildren.size())) {
          return n;
        }
        int kJoinOverhead = "[].join()".length();
        foldedSize += kJoinOverhead;
        foldedSize += (right != null) ? InlineCostEstimator.getCost(right) : 0;
        if (foldedSize > originalSize) {
          return n;
        }
        arrayNode.detachChildren();
        for (Node node : arrayFoldedChildren) {
          arrayNode.addChildToBack(node);
        }
        reportCodeChange();
        break;
    }

    return n;
  }

  /**
   * Try to fold .substr() calls on strings
   */
  private Node tryFoldStringSubstr(Node n, Node stringNode, Node arg1) {
    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(stringNode.isString());
    Preconditions.checkArgument(arg1 != null);

    int start;
    int length;
    String stringAsString = stringNode.getString();

    Double maybeStart = NodeUtil.getNumberValue(arg1, useTypes);
    if (maybeStart != null) {
      start = maybeStart.intValue();
    } else {
      return n;
    }

    Node arg2 = arg1.getNext();
    if (arg2 != null) {
      Double maybeLength = NodeUtil.getNumberValue(arg2, useTypes);
      if (maybeLength != null) {
        length = maybeLength.intValue();
      } else {
        return n;
      }

      if (arg2.getNext() != null) {
        // If we got more args than we expected, bail out.
        return n;
      }
    } else {
      // parameter 2 not passed
      length = stringAsString.length() - start;
    }

    // Don't handle these cases. The specification actually does
    // specify the behavior in some of these cases, but we haven't
    // done a thorough investigation that it is correctly implemented
    // in all browsers.
    if ((start + length) > stringAsString.length() ||
        (length < 0) ||
        (start < 0)) {
      return n;
    }

    String result = stringAsString.substring(start, start + length);
    Node resultNode = IR.string(result);

    Node parent = n.getParent();
    parent.replaceChild(n, resultNode);
    reportCodeChange();
    return resultNode;
  }

  /**
   * Try to fold .substring() or .slice() calls on strings
   */
  private Node tryFoldStringSubstringOrSlice(Node n, Node stringNode, Node arg1) {
    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(stringNode.isString());
    Preconditions.checkArgument(arg1 != null);

    int start;
    int end;
    String stringAsString = stringNode.getString();

    Double maybeStart = NodeUtil.getNumberValue(arg1, useTypes);
    if (maybeStart != null) {
      start = maybeStart.intValue();
    } else {
      return n;
    }

    Node arg2 = arg1.getNext();
    if (arg2 != null) {
      Double maybeEnd = NodeUtil.getNumberValue(arg2, useTypes);
      if (maybeEnd != null) {
        end = maybeEnd.intValue();
      } else {
        return n;
      }

      if (arg2.getNext() != null) {
        // If we got more args than we expected, bail out.
        return n;
      }
    } else {
      // parameter 2 not passed
      end = stringAsString.length();
    }

    // Don't handle these cases. The specification actually does
    // specify the behavior in some of these cases, but we haven't
    // done a thorough investigation that it is correctly implemented
    // in all browsers.
    if ((end > stringAsString.length()) || (start > stringAsString.length())
        || (start < 0) || (end < 0)
        || (start > end)) {
      return n;
    }

    String result = stringAsString.substring(start, end);
    Node resultNode = IR.string(result);

    Node parent = n.getParent();
    parent.replaceChild(n, resultNode);
    reportCodeChange();
    return resultNode;
  }

  private Node replaceWithCharAt(Node n, Node callTarget, Node firstArg) {
    // TODO(moz): Maybe correct the arity of the function type here.
    callTarget.getLastChild().setString("charAt");
    firstArg.getNext().detach();
    reportCodeChange();
    return n;
  }

  /**
   * Try to fold .charAt() calls on strings
   */
  private Node tryFoldStringCharAt(Node n, Node stringNode, Node arg1) {
    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(stringNode.isString());

    int index;
    String stringAsString = stringNode.getString();

    if (arg1 != null && arg1.isNumber()
        && arg1.getNext() == null) {
      index = (int) arg1.getDouble();
    } else {
      return n;
    }

    if (index < 0 || stringAsString.length() <= index) {
      // http://es5.github.com/#x15.5.4.4 says "" is returned when index is
      // out of bounds but we bail.
      return n;
    }

    Node resultNode = IR.string(
        stringAsString.substring(index, index + 1));
    Node parent = n.getParent();
    parent.replaceChild(n, resultNode);
    reportCodeChange();
    return resultNode;
  }

  /**
   * Try to fold .charCodeAt() calls on strings
   */
  private Node tryFoldStringCharCodeAt(Node n, Node stringNode, Node arg1) {
    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(stringNode.isString());

    int index;
    String stringAsString = stringNode.getString();

    if (arg1 != null && arg1.isNumber()
        && arg1.getNext() == null) {
      index = (int) arg1.getDouble();
    } else {
      return n;
    }

    if (index < 0 || stringAsString.length() <= index) {
      // http://es5.github.com/#x15.5.4.5 says NaN is returned when index is
      // out of bounds but we bail.
      return n;
    }

    Node resultNode = IR.number(stringAsString.charAt(index));
    Node parent = n.getParent();
    parent.replaceChild(n, resultNode);
    reportCodeChange();
    return resultNode;
  }

  /**
   * Support function for jsSplit, find the first occurrence of
   * separator within stringValue starting at startIndex.
   */
  private static int jsSplitMatch(String stringValue, int startIndex,
                                  String separator) {

    if (startIndex + separator.length() > stringValue.length()) {
      return -1;
    }

    int matchIndex = stringValue.indexOf(separator, startIndex);

    if (matchIndex < 0) {
      return -1;
    }

    return matchIndex;
  }

  /**
   * Implement the JS String.split method using a string separator.
   */
  private String[] jsSplit(String stringValue, String separator, int limit) {
    Preconditions.checkArgument(limit >= 0);
    Preconditions.checkArgument(stringValue != null);

    // For limits of 0, return an empty array
    if (limit == 0) {
      return new String[0];
    }

    // If a separator is not specified, return the entire string as
    // the only element of an array.
    if (separator == null) {
      return new String[] {stringValue};
    }

    List splitStrings = new ArrayList<>();

    // If an empty string is specified for the separator, split apart each
    // character of the string.
    if (separator.isEmpty()) {
      for (int i = 0; i < stringValue.length() && i < limit; i++) {
        splitStrings.add(stringValue.substring(i, i + 1));
      }
    } else {
      int startIndex = 0;
      int matchIndex;
      while ((matchIndex =
          jsSplitMatch(stringValue, startIndex, separator)) >= 0 &&
          splitStrings.size() < limit) {
        splitStrings.add(stringValue.substring(startIndex, matchIndex));

        startIndex = matchIndex + separator.length();
      }

      if (splitStrings.size() < limit) {
        if (startIndex < stringValue.length()) {
          splitStrings.add(stringValue.substring(startIndex));
        } else {
          splitStrings.add("");
        }
      }
    }

    return splitStrings.toArray(new String[0]);
  }

  /**
   * Try to fold .split() calls on strings
   */
  private Node tryFoldStringSplit(Node n, Node stringNode, Node arg1) {
    if (late) {
      return n;
    }

    Preconditions.checkArgument(n.isCall());
    Preconditions.checkArgument(stringNode.isString());

    String separator = null;
    String stringValue = stringNode.getString();

    // Maximum number of possible splits
    int limit = stringValue.length() + 1;

    if (arg1 != null) {
      if (arg1.isString()) {
        separator = arg1.getString();
      } else if (!arg1.isNull()) {
        return n;
      }

      Node arg2 = arg1.getNext();
      if (arg2 != null) {
        if (arg2.isNumber()) {
          limit = Math.min((int) arg2.getDouble(), limit);
          if (limit < 0) {
            return n;
          }
        } else {
          return n;
        }
      }
    }

    // Split the string and convert the returned array into JS nodes
    String[] stringArray = jsSplit(stringValue, separator, limit);
    Node arrayOfStrings = IR.arraylit();
    for (String element : stringArray) {
      arrayOfStrings.addChildToBack(IR.string(element).srcref(stringNode));
    }

    Node parent = n.getParent();
    parent.replaceChild(n, arrayOfStrings);
    reportCodeChange();
    return arrayOfStrings;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy