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

com.google.javascript.jscomp.CodeGenerator 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.debugging.sourcemap.Util;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.jscomp.parsing.parser.util.SourcePosition;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.NonJSDocComment;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;
import org.jspecify.nullness.Nullable;

/** CodeGenerator generates codes from a parse tree, sending it to the specified CodeConsumer. */
public class CodeGenerator {
  private static final String LT_ESCAPED = "\\x3c";
  private static final String GT_ESCAPED = "\\x3e";

  private final CodeConsumer cc;

  private final @Nullable OutputCharsetEncoder outputCharsetEncoder;

  private final boolean preferSingleQuotes;
  private final boolean preserveTypeAnnotations;
  private final boolean printNonJSDocComments;
  /**
   * To distinguish between gents and non-gents mode so that we can turn off checking the sanity of
   * the source location of comments, and also provide a different mode for comment printing between
   * those two.
   */
  private final boolean gentsMode;

  private final boolean trustedStrings;
  private final boolean quoteKeywordProperties;
  private final boolean useOriginalName;
  private final FeatureSet outputFeatureSet;
  private final JSDocInfoPrinter jsDocInfoPrinter;

  private CodeGenerator(CodeConsumer consumer) {
    cc = consumer;
    outputCharsetEncoder = null;
    preferSingleQuotes = false;
    trustedStrings = true;
    preserveTypeAnnotations = false;
    printNonJSDocComments = false;
    gentsMode = false;
    quoteKeywordProperties = false;
    useOriginalName = false;
    this.outputFeatureSet = FeatureSet.BARE_MINIMUM;
    this.jsDocInfoPrinter = new JSDocInfoPrinter(false);
  }

  protected CodeGenerator(CodeConsumer consumer, CompilerOptions options) {
    cc = consumer;

    this.outputCharsetEncoder = new OutputCharsetEncoder(options.getOutputCharset());
    this.preferSingleQuotes = options.preferSingleQuotes;
    this.trustedStrings = options.trustedStrings;
    this.preserveTypeAnnotations = options.preserveTypeAnnotations;
    this.printNonJSDocComments = options.getPreserveNonJSDocComments();
    this.gentsMode = options.gentsMode;
    this.quoteKeywordProperties = options.shouldQuoteKeywordProperties();
    this.useOriginalName = options.getUseOriginalNamesInOutput();
    this.outputFeatureSet = options.getOutputFeatureSet();
    this.jsDocInfoPrinter = new JSDocInfoPrinter(useOriginalName);
  }

  static CodeGenerator forCostEstimation(CodeConsumer consumer) {
    return new CodeGenerator(consumer);
  }

  /** Insert a top-level identifying file as .i.js generated typing file. */
  void tagAsTypeSummary() {
    add("/** @fileoverview @typeSummary */\n");
  }

  /** Insert a ECMASCRIPT 5 strict annotation. */
  public void tagAsStrict() {
    add("'use strict';");
    cc.endLine();
  }

  private void printJSDocComment(Node node, JSDocInfo jsDocInfo) {
    String jsdocAsString;
    // In gents mode, we print NonJSDocInfo without special handling, as we already have
    // pre-filtered the comments properly.
    if (gentsMode) {
      jsdocAsString = jsDocInfo.getOriginalCommentString();
    } else {
      jsdocAsString = jsDocInfoPrinter.print(jsDocInfo);
    }
    // Don't print an empty jsdoc
    if (jsdocAsString != null && !jsdocAsString.equals("/** */ ")) {
      add(jsdocAsString);
      if (!node.isCast()) {
        cc.endLine();
      }
    }
  }

  private void printNonJSDocComment(Node node, NonJSDocComment nonJSDocComment) {
    String nonJSDocCommentString = nonJSDocComment.getCommentString();
    if (!nonJSDocCommentString.isEmpty()) {
      addNonJsDoc_nonTrailing(node, nonJSDocComment);
    }
  }

  /**
   * Print Leading JSDocComments or NonJSDocComments for the given node in order, depending on their
   * source location.
   */
  protected void printLeadingCommentsInOrder(Node node) {
    JSDocInfo jsDocInfo = node.getJSDocInfo();
    NonJSDocComment nonJSDocComment = node.getNonJSDocComment();

    boolean printJSDoc = preserveTypeAnnotations && jsDocInfo != null;
    boolean printNonJSDoc = printNonJSDocComments && nonJSDocComment != null;
    if (printJSDoc && printNonJSDoc) {
      if (jsDocInfo.getOriginalCommentPosition() < nonJSDocComment.getStartPosition().getOffset()) {
        printJSDocComment(node, jsDocInfo);
        printNonJSDocComment(node, nonJSDocComment);
      } else {
        printNonJSDocComment(node, nonJSDocComment);
        printJSDocComment(node, jsDocInfo);
      }
      return;
    }

    if (printJSDoc) {
      printJSDocComment(node, jsDocInfo);
      return;
    }

    if (printNonJSDoc) {
      printNonJSDocComment(node, nonJSDocComment);
      return;
    }
  }

  /** Returns true when a node has a trailing comment. */
  private boolean hasTrailingCommentOnSameLine(Node node) {
    if (!printNonJSDocComments) {
      return false;
    }
    return !node.getTrailingNonJSDocCommentString().isEmpty();
  }

  protected void printTrailingComment(Node node) {
    // print any trailing nonJSDoc comment attached to this node
    if (!printNonJSDocComments) {
      return;
    }
    NonJSDocComment nonJSDocComment = node.getTrailingNonJSDocComment();
    if (nonJSDocComment != null) {
      String nonJSDocCommentString = node.getTrailingNonJSDocCommentString();
      if (!nonJSDocCommentString.isEmpty()) {
        addNonJsDoctrailing(nonJSDocComment, hasTrailingCommentOnSameLine(node));
      }
    }
  }

  protected void add(String str) {
    cc.add(str);
  }

  protected void add(Node n) {
    add(n, Context.OTHER);
  }

  private static final QualifiedName JSCOMP_SCOPE = QualifiedName.of("$jscomp.scope");

  protected void add(Node node, Context context) {
    add(node, context, true);
  }

  /** Generate the current node */
  protected void add(Node node, Context context, boolean printComments) {
    if (!cc.continueProcessing()) {
      return;
    }
    if (printComments) {
      printLeadingCommentsInOrder(node);
    }
    cc.trackLicenses(node);

    Token type = node.getToken();
    String opstr = NodeUtil.opToStr(type);
    int childCount = node.getChildCount();
    Node first = node.getFirstChild();
    Node last = node.getLastChild();

    // Handle all binary operators
    if (opstr != null && first != last) {
      Preconditions.checkState(
          childCount == 2,
          "Bad binary operator \"%s\": expected 2 arguments but got %s",
          opstr,
          childCount);
      int p = precedence(node);

      // For right-hand-side of operations, only pass context if it's
      // the IN_FOR_INIT_CLAUSE one.
      Context rhsContext = getContextForNoInOperator(context);

      boolean needsParens =
          (context == Context.START_OF_EXPR || context.atArrowFunctionBody())
              && first.isObjectPattern();
      if (node.isAssign() && needsParens) {
        add("(");
      }

      if (NodeUtil.isAssignmentOp(node) || type == Token.EXPONENT) {
        // Assignment operators and '**' are the only right-associative binary operators
        addExpr(first, p + 1, context);
        cc.addOp(opstr, true);
        addExpr(last, p, rhsContext);
      } else {
        unrollBinaryOperator(node, type, opstr, context, rhsContext, p, p + 1);
      }

      if (node.isAssign() && needsParens) {
        add(")");
      }
      return;
    }

    cc.startSourceMapping(node);

    switch (type) {
      case TRY:
        {
          checkState(first.getNext().isBlock() && !first.getNext().hasMoreThanOneChild());
          checkState(childCount >= 2 && childCount <= 3);

          add("try");
          add(first);

          // second child contains the catch block, or nothing if there
          // isn't a catch block
          Node catchblock = first.getNext().getFirstChild();
          if (catchblock != null) {
            add(catchblock);
          }

          if (childCount == 3) {
            cc.maybeInsertSpace();
            add("finally");
            add(last);
          }
          break;
        }

      case CATCH:
        Preconditions.checkState(childCount == 2, node);
        cc.maybeInsertSpace();
        add("catch");
        cc.maybeInsertSpace();

        if (!first.isEmpty()) {
          // optional catch binding
          add("(");
          add(first);
          add(")");
        }

        add(last);
        break;

      case THROW:
        Preconditions.checkState(childCount == 1, node);
        add("throw");
        cc.maybeInsertSpace();
        add(first);

        // Must have a ';' after a throw statement, otherwise safari can't
        // parse this.
        cc.endStatement(/*needSemiColon=*/ true, hasTrailingCommentOnSameLine(node));
        break;

      case RETURN:
        add("return");
        if (childCount == 1) {
          cc.maybeInsertSpace();
          if (preserveTypeAnnotations && first.getJSDocInfo() != null) {
            add("(");
            add(first);
            add(")");
          } else {
            add(first);
          }
        } else {
          checkState(childCount == 0, node);
        }
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case VAR:
        add("var ");
        addList(first, false, getContextForNoInOperator(context), ",");
        if (node.getParent() == null || NodeUtil.isStatement(node)) {
          cc.endStatement(hasTrailingCommentOnSameLine(node));
        }
        break;

      case CONST:
        add("const ");
        addList(first, false, getContextForNoInOperator(context), ",");
        if (node.getParent() == null || NodeUtil.isStatement(node)) {
          cc.endStatement(hasTrailingCommentOnSameLine(node));
        }
        break;

      case LET:
        add("let ");
        addList(first, false, getContextForNoInOperator(context), ",");
        if (node.getParent() == null || NodeUtil.isStatement(node)) {
          cc.endStatement(hasTrailingCommentOnSameLine(node));
        }
        break;

      case LABEL_NAME:
        Preconditions.checkState(!node.getString().isEmpty(), node);
        addIdentifier(node.getString());
        break;

      case DESTRUCTURING_LHS:
        add(first);
        if (first != last) {
          checkState(childCount == 2, node);
          cc.addOp("=", true);
          addExpr(last, NodeUtil.precedence(Token.ASSIGN), getContextForNoInOperator(context));
        }
        break;

      case NAME:
        if (useOriginalName && node.getOriginalName() != null) {
          addIdentifier(node.getOriginalName());
        } else {
          addIdentifier(node.getString());
        }
        maybeAddOptional(node);
        maybeAddTypeDecl(node);

        if (first != null && !first.isEmpty()) {
          checkState(childCount == 1, node);
          cc.addOp("=", true);
          addExpr(first, NodeUtil.precedence(Token.ASSIGN), getContextForNoInOperator(context));
        }
        break;

      case ARRAYLIT:
        add("[");
        addArrayList(first);
        add("]");
        break;

      case ARRAY_PATTERN:
        add("[");
        addArrayList(first);
        add("]");
        maybeAddTypeDecl(node);
        break;

      case PARAM_LIST:
        // If this is the list for a non-TypeScript arrow function with one simple name param.
        if (node.getParent().isArrowFunction()
            && node.hasOneChild()
            && first.isName()
            && !gentsMode) {
          add(first);
        } else {
          add("(");
          addList(first);
          add(")");
        }
        break;

      case DEFAULT_VALUE:
        add(first);
        maybeAddTypeDecl(node);
        cc.addOp("=", true);
        addExpr(first.getNext(), 1, Context.OTHER);
        break;

      case COMMA:
        Preconditions.checkState(childCount == 2, node);
        unrollBinaryOperator(
            node, Token.COMMA, ",", context, getContextForNoInOperator(context), 0, 0);
        break;

      case NUMBER:
        Preconditions.checkState(childCount == 0, node);
        cc.addNumber(node.getDouble(), node);
        break;

      case BIGINT:
        Preconditions.checkState(childCount == 0, node);
        cc.add(node.getBigInt() + "n");
        break;

      case TYPEOF:
      case VOID:
      case NOT:
      case BITNOT:
      case POS:
      case NEG:
        {
          // All of these unary operators are right-associative
          checkState(childCount == 1, node);
          cc.addOp(NodeUtil.opToStrNoFail(type), false);
          addExpr(first, NodeUtil.precedence(type), Context.OTHER);
          break;
        }

      case HOOK:
        {
          checkState(childCount == 3, "%s wrong number of children: %s", node, childCount);
          int p = NodeUtil.precedence(type);
          Context rhsContext = getContextForNoInOperator(context);
          addExpr(first, p + 1, context);
          cc.addOp("?", true);
          addExpr(first.getNext(), 1, rhsContext);
          cc.addOp(":", true);
          addExpr(last, 1, rhsContext);
          break;
        }

      case REGEXP:
        if (!first.isStringLit() || !last.isStringLit()) {
          throw new Error("Expected children to be strings");
        }

        String regexp = regexpEscape(first.getString());

        // I only use one .add because whitespace matters
        if (childCount == 2) {
          add(regexp + last.getString());
        } else {
          checkState(childCount == 1, node);
          add(regexp);
        }
        break;

      case FUNCTION:
        {
          if (node.getClass() != Node.class) {
            throw new Error("Unexpected Node subclass.");
          }
          checkState(childCount == 3, node);
          if (node.isArrowFunction()) {
            addArrowFunction(node, first, last, context);
          } else {
            addFunction(node, first, last, context);
          }
          break;
        }
      case ITER_REST:
      case OBJECT_REST:
        add("...");
        add(first);
        maybeAddTypeDecl(node);
        break;

      case ITER_SPREAD:
      case OBJECT_SPREAD:
        add("...");
        addExpr(first, NodeUtil.precedence(type), Context.OTHER);
        break;

      case EXPORT:
        add("export");
        if (node.getBooleanProp(Node.EXPORT_DEFAULT)) {
          add("default");
        }
        if (node.getBooleanProp(Node.EXPORT_ALL_FROM)) {
          add("*");
          checkState(first != null && first.isEmpty(), node);
        } else {
          add(first);
        }
        if (childCount == 2) {
          add("from");
          add(last);
        }
        processEnd(first, context);
        break;

      case IMPORT:
        add("import");

        Node second = first.getNext();
        if (!first.isEmpty()) {
          add(first);
          if (!second.isEmpty()) {
            cc.listSeparator();
          }
        }
        if (!second.isEmpty()) {
          add(second);
        }
        if (!first.isEmpty() || !second.isEmpty()) {
          add("from");
        }
        add(last);
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case EXPORT_SPECS:
      case IMPORT_SPECS:
        add("{");
        for (Node c = first; c != null; c = c.getNext()) {
          if (c != first) {
            cc.listSeparator();
          }
          add(c);
        }
        add("}");
        break;

      case EXPORT_SPEC:
      case IMPORT_SPEC:
        add(first);
        if (node.isShorthandProperty() && first.getString().equals(last.getString())) {
          break;
        }
        add("as");
        add(last);
        break;

      case IMPORT_STAR:
        add("*");
        add("as");
        add(node.getString());
        break;

      case DYNAMIC_IMPORT:
        add("import(");
        addExpr(first, NodeUtil.precedence(type), context);
        add(")");
        break;

      case IMPORT_META:
        add("import.meta");
        break;

        // CLASS -> NAME,EXPR|EMPTY,BLOCK
      case CLASS:
        {
          checkState(childCount == 3, node);
          boolean classNeedsParens = (context == Context.START_OF_EXPR);
          if (classNeedsParens) {
            add("(");
          }

          Node name = first;
          Node superClass = first.getNext();
          Node members = last;

          add("class");
          if (!name.isEmpty()) {
            add(name);
          }

          maybeAddGenericTypes(first);

          if (!superClass.isEmpty()) {
            add("extends");

            // Parentheses are required for a comma expression
            addExpr(superClass, 1, Context.OTHER);
          }

          Node interfaces = (Node) node.getProp(Node.IMPLEMENTS);
          if (interfaces != null) {
            add("implements");
            Node child = interfaces.getFirstChild();
            add(child);
            while ((child = child.getNext()) != null) {
              add(",");
              cc.maybeInsertSpace();
              add(child);
            }
          }
          add(members);
          cc.endClass(context == Context.STATEMENT);

          if (classNeedsParens) {
            add(")");
          }
        }
        break;

      case CLASS_MEMBERS:
      case INTERFACE_MEMBERS:
      case NAMESPACE_ELEMENTS:
        cc.beginBlock();
        for (Node c = first; c != null; c = c.getNext()) {
          add(c);
          processEnd(c, context);
          cc.endLine();
        }
        cc.endBlock(false);
        break;
      case ENUM_MEMBERS:
        cc.beginBlock();
        for (Node c = first; c != null; c = c.getNext()) {
          add(c);
          if (c.getNext() != null) {
            add(",");
          }
          cc.endLine();
        }
        cc.endBlock(false);
        break;
      case GETTER_DEF:
      case SETTER_DEF:
      case MEMBER_FUNCTION_DEF:
      case MEMBER_VARIABLE_DEF:
        {
          checkState(
              node.getParent().isObjectLit()
                  || node.getParent().isClassMembers()
                  || node.getParent().isInterfaceMembers()
                  || node.getParent().isRecordType()
                  || node.getParent().isIndexSignature());

          maybeAddAccessibilityModifier(node);
          if (node.isStaticMember()) {
            add("static ");
          }

          if (node.isMemberFunctionDef() && node.getFirstChild().isAsyncFunction()) {
            add("async ");
          }

          if (!node.isMemberVariableDef() && node.getFirstChild().isGeneratorFunction()) {
            checkState(type == Token.MEMBER_FUNCTION_DEF, node);
            add("*");
          }

          switch (type) {
            case GETTER_DEF:
              // Get methods have no parameters.
              Preconditions.checkState(!first.getSecondChild().hasChildren(), node);
              add("get ");
              break;
            case SETTER_DEF:
              // Set methods have one parameter.
              Preconditions.checkState(first.getSecondChild().hasOneChild(), node);
              add("set ");
              break;
            case MEMBER_FUNCTION_DEF:
            case MEMBER_VARIABLE_DEF:
              // nothing to do.
              break;
            default:
              break;
          }

          // The name is on the GET or SET node.
          String name = node.getString();
          if (node.isMemberVariableDef()) {
            add(node.getString());
            maybeAddOptional(node);
            maybeAddTypeDecl(node);
          } else {
            checkState(childCount == 1, node);
            checkState(first.isFunction(), first);

            // The function referenced by the definition should always be unnamed.
            checkState(first.getFirstChild().getString().isEmpty(), first);

            Node fn = first;
            Node parameters = fn.getSecondChild();
            Node body = fn.getLastChild();

            // Add the property name.
            if (!node.isQuotedStringKey()
                && TokenStream.isJSIdentifier(name)
                &&
                // do not encode literally any non-literal characters that were
                // Unicode escaped.
                NodeUtil.isLatin(name)) {
              add(name);
              maybeAddGenericTypes(fn.getFirstChild());
            } else {
              // Determine if the string is a simple number.
              double d = getSimpleNumber(name);
              if (!Double.isNaN(d)) {
                cc.addNumber(d, node);
              } else {
                addJsString(node);
              }
            }
            maybeAddOptional(fn);
            add(parameters);
            maybeAddTypeDecl(fn);
            add(body);
          }
          break;
        }

      case MEMBER_FIELD_DEF:
      case COMPUTED_FIELD_DEF:
        {
          checkState(node.getParent().isClassMembers());
          if (node.getBooleanProp(Node.STATIC_MEMBER)) {
            add("static ");
          }
          Node init = null;
          switch (type) {
            case MEMBER_FIELD_DEF:
              String propertyName = node.getString();
              add(propertyName);
              init = first;
              break;
            case COMPUTED_FIELD_DEF:
              add("[");
              // Must use addExpr() with a priority of 1, because comma expressions aren't allowed.
              // https://www.ecma-international.org/ecma-262/9.0/index.html#prod-ComputedPropertyName
              addExpr(first, 1, Context.OTHER);
              add("]");
              init = node.getSecondChild();
              break;
            default:
              break;
          }
          if (init != null) {
            add("=");
            addExpr(init, 1, Context.OTHER);
          }
          add(";");
          break;
        }
      case SCRIPT:
      case MODULE_BODY:
      case BLOCK:
      case ROOT:
        {
          if (node.getClass() != Node.class) {
            throw new Error("Unexpected Node subclass.");
          }
          if (node.hasParent()) {
            boolean staticBlock = node.isBlock() && node.getParent().isClassMembers();
            if (staticBlock) {
              add("static");
            }
          }
          boolean preserveBlock = node.isBlock() && !node.isSyntheticBlock();
          if (preserveBlock) {
            cc.beginBlock();
          }

          boolean preferLineBreaks =
              type == Token.SCRIPT
                  || (type == Token.BLOCK && !preserveBlock && node.getParent().isScript());
          for (Node c = first; c != null; c = c.getNext()) {
            add(c, Context.STATEMENT);

            if (c.isFunction() || c.isClass()) {
              cc.maybeLineBreak();
            }

            // Prefer to break lines in between top-level statements
            // because top-level statements are more homogeneous.
            if (preferLineBreaks) {
              cc.notePreferredLineBreak();
            }
          }
          if (preserveBlock) {
            cc.endBlock(cc.breakAfterBlockFor(node, context == Context.STATEMENT));
          }
          break;
        }

      case FOR:
        Preconditions.checkState(childCount == 4, node);
        add("for");
        cc.maybeInsertSpace();
        add("(");
        if (NodeUtil.isNameDeclaration(first)) {
          add(first, Context.IN_FOR_INIT_CLAUSE);
        } else {
          addExpr(first, 0, Context.IN_FOR_INIT_CLAUSE);
        }
        add(";");
        if (!first.getNext().isEmpty()) {
          cc.maybeInsertSpace();
        }
        add(first.getNext());
        add(";");
        if (!first.getNext().getNext().isEmpty()) {
          cc.maybeInsertSpace();
        }
        add(first.getNext().getNext());
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case FOR_IN:
        Preconditions.checkState(childCount == 3, node);
        add("for");
        cc.maybeInsertSpace();
        add("(");
        add(first);
        add("in");
        add(first.getNext());
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case FOR_OF:
        Preconditions.checkState(childCount == 3, node);
        add("for");
        cc.maybeInsertSpace();
        add("(");
        add(first);
        cc.maybeInsertSpace();
        add("of");
        cc.maybeInsertSpace();
        // the iterable must be an AssignmentExpression
        addExpr(first.getNext(), NodeUtil.precedence(Token.ASSIGN), Context.OTHER);
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case FOR_AWAIT_OF:
        Preconditions.checkState(childCount == 3, node);
        add("for await");
        cc.maybeInsertSpace();
        add("(");
        add(first);
        cc.maybeInsertSpace();
        add("of");
        cc.maybeInsertSpace();
        // the iterable must be an AssignmentExpression
        addExpr(first.getNext(), NodeUtil.precedence(Token.ASSIGN), Context.OTHER);
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case DO:
        Preconditions.checkState(childCount == 2, node);
        add("do");
        addNonEmptyStatement(first, Context.OTHER, false);
        cc.maybeInsertSpace();
        add("while");
        cc.maybeInsertSpace();
        add("(");
        add(last);
        add(")");
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case WHILE:
        Preconditions.checkState(childCount == 2, node);
        add("while");
        cc.maybeInsertSpace();
        add("(");
        add(first);
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case EMPTY:
        Preconditions.checkState(childCount == 0, node);
        break;

      case OPTCHAIN_GETPROP:
        {
          addExpr(first, NodeUtil.precedence(type), context);
          add(node.isOptionalChainStart() ? "?." : ".");
          addGetpropIdentifier(node);
          break;
        }

      case GETPROP:
        {
          // This attempts to convert rewritten aliased code back to the original code,
          // such as when using goog.scope(). See ScopedAliases.java for the original code.
          // NOTE: OPTCHAIN_GETPROP case doesn't need this logic, because it only applies to
          // qualified names.
          if (useOriginalName && node.getOriginalName() != null) {
            // The ScopedAliases pass will convert variable assignments and function declarations
            // to assignments to GETPROP nodes, like $jscomp.scope.SOME_VAR = 3;. This attempts to
            // rewrite it back to the original code.
            if (JSCOMP_SCOPE.matches(node.getFirstChild()) && node.getParent().isAssign()) {
              add("var ");
            }
            addGetpropIdentifier(node);
            break;
          }
          // We need parentheses to distinguish
          // `a?.b.c` from `(a?.b).c`
          boolean breakOutOfOptionalChain = NodeUtil.isOptChainNode(first);
          // `2.toString()` is invalid - it must be `(2).toString()`
          boolean needsParens = first.isNumber() || breakOutOfOptionalChain;
          if (needsParens) {
            add("(");
          }
          addExpr(first, NodeUtil.precedence(type), context);
          if (needsParens) {
            add(")");
          }
          if (quoteKeywordProperties && TokenStream.isKeyword(node.getString())) {
            // NOTE: We don't have to worry about quoting keyword properties in the
            // OPTCHAIN_GETPROP case above, because we only need to quote keywords for
            // ES3-compatible output.
            //
            // Must be a single call to `add` otherwise the generator will add a trailing space.
            add("[\"" + node.getString() + "\"]");
          } else {
            add(".");
            addGetpropIdentifier(node);
          }
          break;
        }

      case OPTCHAIN_GETELEM:
        {
          checkState(
              childCount == 2,
              "Bad GETELEM node: Expected 2 children but got %s. For node: %s",
              childCount,
              node);
          addExpr(first, NodeUtil.precedence(type), context);
          if (node.isOptionalChainStart()) {
            add("?.");
          }
          add("[");
          add(first.getNext());
          add("]");
          break;
        }

      case GETELEM:
        {
          checkState(
              childCount == 2,
              "Bad GETELEM node: Expected 2 children but got %s. For node: %s",
              childCount,
              node);
          boolean needsParens = NodeUtil.isOptChainNode(first);
          if (needsParens) {
            add("(");
          }
          addExpr(first, NodeUtil.precedence(type), context);
          if (needsParens) {
            add(")");
          }
          add("[");
          add(first.getNext());
          add("]");
          break;
        }

      case WITH:
        Preconditions.checkState(childCount == 2, node);
        add("with(");
        add(first);
        add(")");
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        break;

      case INC:
      case DEC:
        {
          checkState(childCount == 1, node);
          String o = type == Token.INC ? "++" : "--";
          boolean postProp = node.getBooleanProp(Node.INCRDECR_PROP);
          if (postProp) {
            addExpr(first, NodeUtil.precedence(type), context);
            cc.addOp(o, false);
          } else {
            cc.addOp(o, false);
            add(first);
          }
          break;
        }

      case OPTCHAIN_CALL:
        {
          // We have two special cases here:
          // 1) If the left hand side of the call is a direct reference to eval,
          // then it must have a DIRECT_EVAL annotation. If it does not, then
          // that means it was originally an indirect call to eval, and that
          // indirectness must be preserved.
          // 2) If the left hand side of the call is a property reference,
          // then the call must not a FREE_CALL annotation. If it does, then
          // that means it was originally an call without an explicit this and
          // that must be preserved.
          if (isIndirectEval(first)
              || (node.getBooleanProp(Node.FREE_CALL) && NodeUtil.isNormalOrOptChainGet(first))) {
            add("(0,");
            addExpr(first, NodeUtil.precedence(Token.COMMA), Context.OTHER);
            add(")");
          } else {
            addExpr(first, NodeUtil.precedence(type), context);
          }
          Node args = first.getNext();
          if (node.isOptionalChainStart()) {
            add("?.");
          }
          add("(");
          addList(args);
          add(")");
          break;
        }

      case CALL:
        this.addInvocationTarget(node, context);

        add("(");
        addList(first.getNext());
        add(")");
        break;

      case IF:
        Preconditions.checkState(childCount == 2 || childCount == 3, node);
        boolean hasElse = childCount == 3;
        boolean ambiguousElseClause = context == Context.BEFORE_DANGLING_ELSE && !hasElse;
        if (ambiguousElseClause) {
          cc.beginBlock();
        }

        add("if");
        cc.maybeInsertSpace();
        add("(");
        add(first);
        add(")");

        if (hasElse) {
          addNonEmptyStatement(first.getNext(), Context.BEFORE_DANGLING_ELSE, false);
          cc.maybeInsertSpace();
          add("else");
          addNonEmptyStatement(last, getContextForNonEmptyExpression(context), false);
        } else {
          addNonEmptyStatement(first.getNext(), Context.OTHER, false);
        }

        if (ambiguousElseClause) {
          cc.endBlock();
        }
        break;

      case NULL:
        Preconditions.checkState(childCount == 0, node);
        cc.addConstant("null");
        break;

      case THIS:
        Preconditions.checkState(childCount == 0, node);
        add("this");
        break;

      case SUPER:
        Preconditions.checkState(childCount == 0, node);
        add("super");
        break;

      case NEW_TARGET:
        Preconditions.checkState(childCount == 0, node);
        add("new.target");
        break;

      case YIELD:
        add("yield");
        if (node.isYieldAll()) {
          checkNotNull(first);
          add("*");
        }
        if (first != null) {
          cc.maybeInsertSpace();
          addExpr(first, NodeUtil.precedence(type), Context.OTHER);
        }
        break;

      case AWAIT:
        add("await ");
        addExpr(first, NodeUtil.precedence(type), Context.OTHER);
        break;

      case FALSE:
        Preconditions.checkState(childCount == 0, node);
        cc.addConstant("false");
        break;

      case TRUE:
        Preconditions.checkState(childCount == 0, node);
        cc.addConstant("true");
        break;

      case CONTINUE:
        Preconditions.checkState(childCount <= 1, node);
        add("continue");
        if (childCount == 1) {
          if (!first.isLabelName()) {
            throw new Error("Unexpected token type. Should be LABEL_NAME.");
          }
          add(" ");
          add(first);
        }
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case DEBUGGER:
        Preconditions.checkState(childCount == 0, node);
        add("debugger");
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case BREAK:
        Preconditions.checkState(childCount <= 1, node);
        add("break");
        if (childCount == 1) {
          if (!first.isLabelName()) {
            throw new Error("Unexpected token type. Should be LABEL_NAME.");
          }
          add(" ");
          add(first);
        }
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case EXPR_RESULT:
        Preconditions.checkState(childCount == 1, node);
        add(first, Context.START_OF_EXPR);
        cc.endStatement(hasTrailingCommentOnSameLine(node));
        break;

      case NEW:
        add("new ");
        int precedence = NodeUtil.precedence(type);

        // `new void 0` is a syntax error add parenthese in this case.  This is only particularly
        // interesting for code in dead branches.
        int precedenceOfFirst = NodeUtil.precedence(first.getToken());
        if (precedenceOfFirst == precedence) {
          precedence = precedence + 1;
        }

        // If the first child contains a CALL, then claim higher precedence
        // to force parentheses. Otherwise, when parsed, NEW will bind to the
        // first viable parentheses (don't traverse into functions).
        // Also, NEW requires parentheses around an optional chain callee.
        // If the first child is an arrow function, then parentheses is needed
        if (NodeUtil.has(first, Node::isCall, NodeUtil.MATCH_NOT_FUNCTION)
            || NodeUtil.isOptChainNode(first)) {
          precedence = NodeUtil.precedence(first.getToken()) + 1;
        }
        addExpr(first, precedence, Context.OTHER);

        // '()' is optional when no arguments are present
        Node next = first.getNext();
        if (next != null) {
          add("(");
          addList(next);
          add(")");
        } else {
          if (cc.shouldPreserveExtras(node)) {
            add("(");
            add(")");
          }
        }
        break;

      case STRING_KEY:
        addStringKey(node);
        break;

      case STRINGLIT:
        Preconditions.checkState(childCount == 0, "String node %s may not have children", node);
        addJsString(node);
        break;

      case DELPROP:
        Preconditions.checkState(childCount == 1, node);
        add("delete ");
        add(first);
        break;

      case OBJECTLIT:
        {
          boolean needsParens = context == Context.START_OF_EXPR || context.atArrowFunctionBody();
          if (needsParens) {
            add("(");
          }
          add("{");
          for (Node c = first; c != null; c = c.getNext()) {
            if (c != first) {
              cc.listSeparator();
            }

            checkState(NodeUtil.isObjLitProperty(c) || c.isSpread(), c);
            add(c);
          }
          if (first != null && node.hasTrailingComma()) {
            cc.optionalListSeparator();
          }
          add("}");
          if (needsParens) {
            add(")");
          }
          break;
        }

      case COMPUTED_PROP:
        maybeAddAccessibilityModifier(node);
        if (node.getBooleanProp(Node.STATIC_MEMBER)) {
          add("static ");
        }

        if (node.getBooleanProp(Node.COMPUTED_PROP_GETTER)) {
          add("get ");
        } else if (node.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
          add("set ");
        } else if (node.getBooleanProp(Node.COMPUTED_PROP_METHOD)) {
          if (last.isAsyncFunction()) {
            add("async");
          }
          if (last.getBooleanProp(Node.GENERATOR_FN)) {
            add("*");
          }
        }
        add("[");
        // Must use addExpr() with a priority of 1, because comma expressions aren't allowed.
        // https://www.ecma-international.org/ecma-262/9.0/index.html#prod-ComputedPropertyName
        addExpr(first, 1, Context.OTHER);
        add("]");
        // TODO(martinprobst): There's currently no syntax for properties in object literals that
        // have type declarations on them (a la `{foo: number: 12}`). This comes up for, e.g.,
        // function parameters with default values. Support when figured out.
        maybeAddTypeDecl(node);
        if (node.getBooleanProp(Node.COMPUTED_PROP_METHOD)
            || node.getBooleanProp(Node.COMPUTED_PROP_GETTER)
            || node.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
          Node function = first.getNext();
          Node params = function.getSecondChild();
          Node body = function.getLastChild();

          add(params);
          add(body);
        } else {
          // This is a field or object literal property.
          boolean isInClass = node.getParent().isClassMembers();
          Node initializer = first.getNext();
          if (initializer != null) {
            // Object literal value.
            checkState(
                !isInClass, "initializers should only exist in object literals, not classes");
            cc.add(":");
            // Must use addExpr() with a priority of 1, because a comma expression here would cause
            // a syntax error within the object literal.
            addExpr(initializer, 1, Context.OTHER);
          } else {
            // Computed properties must either have an initializer or be computed member-variable
            // properties that exist for their type declaration.
            checkState(node.getBooleanProp(Node.COMPUTED_PROP_VARIABLE), node);
          }
        }
        break;

      case OBJECT_PATTERN:
        addObjectPattern(node);
        maybeAddTypeDecl(node);
        break;

      case SWITCH:
        add("switch(");
        add(first);
        add(")");
        cc.beginBlock();
        addAllSiblings(first.getNext());
        cc.endBlock(context == Context.STATEMENT);
        break;

      case CASE:
        Preconditions.checkState(childCount == 2, node);
        add("case ");
        add(first);
        addCaseBody(last);
        break;

      case DEFAULT_CASE:
        Preconditions.checkState(childCount == 1, node);
        add("default");
        addCaseBody(first);
        break;

      case LABEL:
        Preconditions.checkState(childCount == 2, node);
        if (!first.isLabelName()) {
          throw new Error("Unexpected token type. Should be LABEL_NAME.");
        }
        add(first);
        add(":");
        if (!last.isBlock()) {
          cc.maybeInsertSpace();
        }
        addNonEmptyStatement(last, getContextForNonEmptyExpression(context), true);
        break;

      case CAST:
        if (preserveTypeAnnotations) {
          add("(");
          add(first); // drop context because of added parentheses
          add(")");
        } else {
          add(first, context); // preserve context
        }
        break;

      case TAGGED_TEMPLATELIT:
        this.addInvocationTarget(node, context);
        add(first.getNext());
        break;

      case TEMPLATELIT:
        cc.beginTemplateLit();
        for (Node c = first; c != null; c = c.getNext()) {
          if (c.isTemplateLitString()) {
            add(escapeUnrecognizedCharacters(c.getRawString()));
          } else {
            cc.beginTemplateLitSub();
            add(c.getFirstChild(), Context.START_OF_EXPR);
            cc.endTemplateLitSub();
          }
        }
        cc.endTemplateLit();
        break;

        // Type Declaration ASTs.
      case STRING_TYPE:
        add("string");
        break;
      case BOOLEAN_TYPE:
        add("boolean");
        break;
      case NUMBER_TYPE:
        add("number");
        break;
      case ANY_TYPE:
        add("any");
        break;
      case VOID_TYPE:
        add("void");
        break;
      case NAMED_TYPE:
        // Children are a chain of getprop nodes.
        add(first);
        break;
      case ARRAY_TYPE:
        addExpr(first, NodeUtil.precedence(Token.ARRAY_TYPE), context);
        add("[]");
        break;
      case FUNCTION_TYPE:
        Node returnType = first;
        add("(");
        addList(first.getNext());
        add(")");
        cc.addOp("=>", true);
        add(returnType);
        break;
      case UNION_TYPE:
        addList(first, "|");
        break;
      case RECORD_TYPE:
        add("{");
        addList(first, false, Context.OTHER, ",");
        add("}");
        break;
      case PARAMETERIZED_TYPE:
        // First child is the type that's parameterized, later children are the arguments.
        add(first);
        add("<");
        addList(first.getNext());
        add(">");
        break;
        // CLASS -> NAME,EXPR|EMPTY,BLOCK
      case GENERIC_TYPE_LIST:
        add("<");
        addList(first, false, Context.STATEMENT, ",");
        add(">");
        break;
      case GENERIC_TYPE:
        addIdentifier(node.getString());
        if (node.hasChildren()) {
          add("extends");
          cc.maybeInsertSpace();
          add(node.getFirstChild());
        }
        break;
      case INTERFACE:
        {
          checkState(childCount == 3, node);
          Node name = first;
          Node superTypes = first.getNext();
          Node members = last;

          add("interface");
          add(name);
          maybeAddGenericTypes(name);
          if (!superTypes.isEmpty()) {
            add("extends");
            Node superType = superTypes.getFirstChild();
            add(superType);
            while ((superType = superType.getNext()) != null) {
              add(",");
              cc.maybeInsertSpace();
              add(superType);
            }
          }
          add(members);
        }
        break;
      case ENUM:
        {
          checkState(childCount == 2, node);
          Node name = first;
          Node members = last;
          add("enum");
          add(name);
          add(members);
          break;
        }
      case NAMESPACE:
        {
          checkState(childCount == 2, node);
          Node name = first;
          Node elements = last;
          add("namespace");
          add(name);
          add(elements);
          break;
        }
      case TYPE_ALIAS:
        add("type");
        add(node.getString());
        cc.addOp("=", true);
        add(last);
        cc.endStatement(/* needSemiColon= */ true, hasTrailingCommentOnSameLine(node));
        break;
      case DECLARE:
        add("declare");
        add(first);
        processEnd(node, context);
        break;
      case INDEX_SIGNATURE:
        add("[");
        add(first);
        add("]");
        maybeAddTypeDecl(node);
        cc.endStatement(/* needSemiColon= */ true, hasTrailingCommentOnSameLine(node));
        break;
      case CALL_SIGNATURE:
        if (node.getBooleanProp(Node.CONSTRUCT_SIGNATURE)) {
          add("new ");
        }
        maybeAddGenericTypes(node);
        add(first);
        maybeAddTypeDecl(node);
        cc.endStatement(/* needSemiColon= */ true, hasTrailingCommentOnSameLine(node));
        break;
      default:
        throw new IllegalStateException("Unknown token " + type + "\n" + node.toStringTree());
    }
    printTrailingComment(node);

    cc.endSourceMapping(node);
  }

  private void addIdentifier(String identifier) {
    cc.addIdentifier(identifierEscape(identifier));
  }

  private void addGetpropIdentifier(Node getprop) {
    cc.startSourceMapping(getprop);
    addIdentifier(getprop.getString());
    cc.endSourceMapping(getprop);
  }

  private int precedence(Node n) {
    if (n.isCast()) {
      return precedence(n.getFirstChild());
    }
    return NodeUtil.precedence(n.getToken());
  }

  /**
   * We have two special cases here:
   *
   * 
    *
  • If the left hand side of the call is a direct reference to eval, then it must have a * DIRECT_EVAL annotation. If it does not, then that means it was originally an indirect * call to eval, and that indirectness must be preserved. *
  • If the left hand side of the call is a property reference, then the call must not a * FREE_CALL annotation. If it does, then that means it was originally an call without an * explicit this and that must be preserved. */ private void addInvocationTarget(Node node, Context context) { Node first = node.getFirstChild(); boolean needsParens = NodeUtil.isOptChainNode(first); if (isIndirectEval(first) || (node.getBooleanProp(Node.FREE_CALL) && NodeUtil.isNormalOrOptChainGet(first))) { add("(0,"); addExpr(first, NodeUtil.precedence(Token.COMMA), Context.OTHER); add(")"); } else { if (needsParens) { add("("); } addExpr(first, NodeUtil.precedence(node.getToken()), context); if (needsParens) { add(")"); } } } private static boolean arrowFunctionNeedsParens(Node n) { Node parent = n.getParent(); // Once you cut through the layers of non-terminals used to define operator precedence, // you can see the following are true. // (Read => as "may expand to" and "!=>" as "may not expand to") // // 1. You can substitute an ArrowFunction into rules where an Expression or // AssignmentExpression is required, because // Expression => AssignmentExpression => ArrowFunction // // 2. However, most operators act on LeftHandSideExpression, CallExpression, or // MemberExpression. None of these expand to ArrowFunction. // // 3. CallExpression cannot expand to an ArrowFunction at all, because all of its expansions // produce multiple symbols and none can be logically equivalent to ArrowFunction. // // 4. LeftHandSideExpression and MemberExpression may be replaced with an ArrowFunction in // parentheses, because: // LeftHandSideExpression => MemberExpression => PrimaryExpression // PrimaryExpression => '(' Expression ')' => '(' ArrowFunction ')' if (parent == null) { return false; } else if (NodeUtil.isBinaryOperator(parent) || NodeUtil.isUnaryOperator(parent) || NodeUtil.isUpdateOperator(parent) || parent.isTaggedTemplateLit() || parent.isGetProp() || parent.isOptChainGetProp() || parent.isAwait() || parent.isYield()) { // LeftHandSideExpression OP LeftHandSideExpression // OP LeftHandSideExpression | LeftHandSideExpression OP // MemberExpression TemplateLiteral // MemberExpression '.' IdentifierName return true; } else if (parent.isGetElem() || parent.isCall() || parent.isHook() || parent.isOptChainGetElem() || parent.isOptChainCall() || parent.isNew()) { // MemberExpression '[' Expression ']' // MemberFunction '(' AssignmentExpressionList ')' // LeftHandSideExpression ? AssignmentExpression : AssignmentExpression return isFirstChild(n); } else { // All other cases are either illegal (e.g. because you cannot assign a value to an // ArrowFunction) or do not require parens. return false; } } private static boolean isFirstChild(Node n) { Node parent = n.getParent(); return parent != null && n == parent.getFirstChild(); } private void addArrowFunction(Node n, Node first, Node last, Context context) { checkState(first.getString().isEmpty(), first); boolean funcNeedsParens = arrowFunctionNeedsParens(n) || n.getMarkForParenthesize(); if (funcNeedsParens) { add("("); } maybeAddGenericTypes(first); if (n.isAsyncFunction()) { add("async"); } add(first.getNext()); // param list maybeAddTypeDecl(n); cc.addOp("=>", true); if (last.isBlock()) { add(last); } else { // This is a hack. Arrow functions have no token type, but // blockless arrow function bodies have lower precedence than anything other than commas. addExpr(last, NodeUtil.precedence(Token.COMMA) + 1, getContextForArrowFunctionBody(context)); } cc.endFunction(context == Context.STATEMENT); if (funcNeedsParens) { add(")"); } } private void addFunction(Node n, Node first, Node last, Context context) { boolean funcNeedsParens = (context == Context.START_OF_EXPR || n.getMarkForParenthesize()); if (funcNeedsParens) { add("("); } add(n.isAsyncFunction() ? "async function" : "function"); if (n.isGeneratorFunction()) { add("*"); if (!first.getString().isEmpty()) { cc.maybeInsertSpace(); } } add(first); maybeAddGenericTypes(first); add(first.getNext()); // param list maybeAddTypeDecl(n); add(last); cc.endFunction(context == Context.STATEMENT); if (funcNeedsParens) { add(")"); } } private void maybeAddAccessibilityModifier(Node n) { Visibility access = (Visibility) n.getProp(Node.ACCESS_MODIFIER); if (access != null) { add(Ascii.toLowerCase(access.toString()) + " "); } } private void maybeAddTypeDecl(Node n) { if (n.getDeclaredTypeExpression() != null) { add(":"); cc.maybeInsertSpace(); add(n.getDeclaredTypeExpression()); } } private void maybeAddGenericTypes(Node n) { Node generics = (Node) n.getProp(Node.GENERIC_TYPE_LIST); if (generics != null) { add(generics); } } private void maybeAddOptional(Node n) { if (n.getBooleanProp(Node.OPT_ES6_TYPED)) { add("?"); } } /** * We could use addList recursively here, but sometimes we produce very deeply nested operators * and run out of stack space, so we just unroll the recursion when possible. * *

    We assume nodes are left-recursive. */ private void unrollBinaryOperator( Node n, Token op, String opStr, Context context, Context rhsContext, int leftPrecedence, int rightPrecedence) { Node firstNonOperator = n.getFirstChild(); while (firstNonOperator.getToken() == op) { firstNonOperator = firstNonOperator.getFirstChild(); } addExpr(firstNonOperator, leftPrecedence, context); Node current = firstNonOperator; do { current = current.getParent(); cc.addOp(opStr, true); addExpr(current.getSecondChild(), rightPrecedence, rhsContext); } while (current != n); } static boolean isSimpleNumber(String s) { int len = s.length(); if (len == 0) { return false; } for (int index = 0; index < len; index++) { char c = s.charAt(index); if (c < '0' || c > '9') { return false; } } return len == 1 || s.charAt(0) != '0'; } static double getSimpleNumber(String s) { if (isSimpleNumber(s)) { try { long l = Long.parseLong(s); if (l <= NodeUtil.MAX_POSITIVE_INTEGER_NUMBER) { return l; } } catch (NumberFormatException e) { // The number was too long to parse. Fall through to NaN. } } return Double.NaN; } /** * @return Whether the name is an indirect eval. */ private static boolean isIndirectEval(Node n) { return n.isName() && "eval".equals(n.getString()) && !n.getBooleanProp(Node.DIRECT_EVAL); } /** * Adds a block or expression, substituting a VOID with an empty statement. This is used for "for * (...);" and "if (...);" type statements. * * @param n The node to print. * @param context The context to determine how the node should be printed. */ private void addNonEmptyStatement(Node n, Context context, boolean allowNonBlockChild) { Node nodeToProcess = n; if (!allowNonBlockChild && !n.isBlock()) { throw new Error("Missing BLOCK child."); } // Strip unneeded blocks, that is blocks with <2 children unless // the CodePrinter specifically wants to keep them. if (n.isBlock()) { int count = getNonEmptyChildCount(n, 2); if (count == 0) { if (cc.shouldPreserveExtras(n)) { cc.beginBlock(); printTrailingComment(n); cc.endBlock(cc.breakAfterBlockFor(n, context == Context.STATEMENT)); } else { printTrailingComment(n); cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); } return; } if (count == 1) { // Preserve the block only if needed or requested. // 'let', 'const', etc are not allowed by themselves in "if" and other // structures. Also, hack around a IE6/7 browser bug that needs a block around DOs. Node firstAndOnlyChild = getFirstNonEmptyChild(n); boolean alwaysWrapInBlock = cc.shouldPreserveExtras(n); if (alwaysWrapInBlock || isBlockDeclOrDo(firstAndOnlyChild)) { cc.beginBlock(); add(firstAndOnlyChild, Context.STATEMENT); printTrailingComment(n); cc.maybeLineBreak(); cc.endBlock(cc.breakAfterBlockFor(n, context == Context.STATEMENT)); return; } else { // Continue with the only child. nodeToProcess = firstAndOnlyChild; } } } if (nodeToProcess.isEmpty()) { printTrailingComment(n); cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); } else { add(nodeToProcess, context); printTrailingComment(n); } } /** * @return Whether the Node is a DO or a declaration that is only allowed in restricted contexts. */ private static boolean isBlockDeclOrDo(Node n) { if (n.isLabel()) { Node labeledStatement = n.getLastChild(); if (!labeledStatement.isBlock()) { return isBlockDeclOrDo(labeledStatement); } else { // For labels with block children, we need to ensure that a // labeled FUNCTION or DO isn't generated when extraneous BLOCKs // are skipped. if (getNonEmptyChildCount(n, 2) == 1) { return isBlockDeclOrDo(getFirstNonEmptyChild(n)); } else { // Either a empty statement or an block with more than one child, // way it isn't a FUNCTION or DO. return false; } } } else { switch (n.getToken()) { case LET: case CONST: case FUNCTION: case CLASS: case DO: return true; default: return false; } } } private void addExpr(Node n, int minPrecedence, Context context) { if (opRequiresParentheses(n, minPrecedence, context)) { add("("); add(n, Context.OTHER); add(")"); } else { add(n, context); } } private boolean opRequiresParentheses(Node n, int minPrecedence, Context context) { if (context.inForInInitClause() && n.isIn()) { // make sure this operator 'in' isn't confused with the for-loop 'in' return true; } else if (NodeUtil.isUnaryOperator(n) && isFirstOperandOfExponentiationExpression(n)) { // Unary operators are higher precedence than '**', but // ExponentiationExpression cannot expand to // UnaryExpression ** ExponentiationExpression return true; } else if (isLogicalANDorLogicalORChildOfNullishCoalesce(n) || isNullishCoalesceChildOfLogicalANDorLogicalOR(n)) { // precedence is not enough here since using && or || with ?? without parentheses // is a syntax error as ?? expands directly to | return true; } else { return precedence(n) < minPrecedence; } } private static boolean isLogicalANDorLogicalORChildOfNullishCoalesce(Node n) { Node parent = n.getParent(); boolean logicalANDorLogicalOR = n.isAnd() || n.isOr(); boolean childOfNullishCoalesce = parent != null && parent.isNullishCoalesce(); return logicalANDorLogicalOR && childOfNullishCoalesce; } private static boolean isNullishCoalesceChildOfLogicalANDorLogicalOR(Node n) { Node parent = n.getParent(); boolean childOfLogicalANDorLogicalOR = parent != null && (parent.isAnd() || parent.isOr()); return n.isNullishCoalesce() && childOfLogicalANDorLogicalOR; } private boolean isFirstOperandOfExponentiationExpression(Node n) { Node parent = n.getParent(); return parent != null && parent.isExponent() && parent.getFirstChild() == n; } void addList(Node firstInList) { addList(firstInList, true, Context.OTHER, ","); } void addList(Node firstInList, String separator) { addList(firstInList, true, Context.OTHER, separator); } void addList( Node firstInList, boolean isArrayOrFunctionArgument, Context lhsContext, String separator) { if (firstInList == null) { return; } for (Node n = firstInList; n != null; n = n.getNext()) { boolean isFirst = n == firstInList; int minPrecedence = isArrayOrFunctionArgument ? 1 : 0; if (isFirst) { addExpr(n, minPrecedence, lhsContext); } else { cc.addOp(separator, true); addExpr(n, minPrecedence, getContextForNoInOperator(lhsContext)); } } if (isArrayOrFunctionArgument && checkNotNull(firstInList.getParent()).hasTrailingComma()) { cc.optionalListSeparator(); } } void addStringKey(Node n) { String key = n.getString(); // Object literal property names don't have to be quoted if they are not JavaScript keywords. boolean mustBeQuoted = n.isQuotedStringKey() || (quoteKeywordProperties && TokenStream.isKeyword(key)) || !TokenStream.isJSIdentifier(key) // do not encode literally any non-literal characters that were Unicode escaped. || !NodeUtil.isLatin(key); if (!mustBeQuoted) { // Check if the property is eligible to be printed as shorthand. if (n.isShorthandProperty()) { Node child = n.getFirstChild(); if (child.matchesQualifiedName(key) || (child.isDefaultValue() && child.getFirstChild().matchesQualifiedName(key))) { add(child); return; } } add(key); } else { // Determine if the string is a simple number. double d = getSimpleNumber(key); if (!Double.isNaN(d)) { cc.addNumber(d, n); } else { addJsString(n); } } if (n.hasChildren()) { // NOTE: the only time a STRING_KEY node does *not* have children is when it's // inside a TypeScript enum. We should change these to their own ENUM_KEY token // so that the bifurcating logic can be removed from STRING_KEY. add(":"); addExpr(n.getFirstChild(), 1, Context.OTHER); } } void addObjectPattern(Node n) { add("{"); for (Node child = n.getFirstChild(); child != null; child = child.getNext()) { if (child != n.getFirstChild()) { cc.listSeparator(); } add(child); } add("}"); } /** * Adds a comma-separated list as is specified by an ARRAYLIT node. * * @param firstInList The first in the node list (chained through the next property). */ void addArrayList(@Nullable Node firstInList) { if (firstInList == null) { return; } boolean lastWasEmpty = false; for (Node n = firstInList; n != null; n = n.getNext()) { if (n != firstInList) { cc.listSeparator(); } addExpr(n, 1, Context.OTHER); lastWasEmpty = n.isEmpty(); } if (lastWasEmpty) { cc.listSeparator(); } else if (firstInList.getParent().hasTrailingComma()) { cc.optionalListSeparator(); } } void addCaseBody(Node caseBody) { checkState(caseBody.isBlock(), caseBody); cc.beginCaseBody(); addAllSiblings(caseBody.getFirstChild()); cc.endCaseBody(); } void addAllSiblings(Node n) { for (Node c = n; c != null; c = c.getNext()) { add(c); } } private void addNonJsDoc_nonTrailing(Node node, NonJSDocComment nonJSDocComment) { String content = nonJSDocComment.getCommentString(); SourcePosition commentEndPosition = nonJSDocComment.getEndPosition(); int nodeLineNumber = node.getLineno() - 1; // source lines are 1-indexed if (nonJSDocComment.isEndingAsLineComment()) { // Non trailing line comments can not be on the same line as the node. checkState( gentsMode || commentEndPosition.line < nodeLineNumber, "Non trailing line comments can not be on the same line as the node."); add(content + "\n"); } else { if (nodeLineNumber == commentEndPosition.line) { // e.g. ``` /* comment */ let x; ``` add(content + " "); } else { // e.g. // ``` // /* comment */ // let x; // ``` add(content + "\n"); } } } private void addNonJsDoctrailing(NonJSDocComment nonJSDocComment, boolean sameLine) { String content = nonJSDocComment.getCommentString(); if (nonJSDocComment.isEndingAsLineComment()) { // Trailing line comments *must* end with a `\n`. E.g.. `let x; //comment\n` add(" " + content); if (sameLine) { cc.startNewLine(); } } else { if (nonJSDocComment.isInline()) { // e.g. `foo(x /*comment*/);` is inline add(" " + content); } else { // e.g. `let x; /*comment*/` is non-inline add(" " + content); if (sameLine) { cc.startNewLine(); } } } } /** Outputs a JS string, using the optimal (single/double) quote character */ private void addJsString(Node n) { add(jsString(n.getString())); } private String jsString(String s) { int singleq = 0; int doubleq = 0; // could count the quotes and pick the optimal quote character for (int i = 0; i < s.length(); i++) { switch (s.charAt(i)) { case '"': doubleq++; break; case '\'': singleq++; break; default: // skip non-quote characters } } String doublequote; String singlequote; char quote; if (preferSingleQuotes ? (singleq <= doubleq) : (singleq < doubleq)) { // more double quotes so enclose in single quotes. quote = '\''; doublequote = "\""; singlequote = "\\\'"; } else { // more single quotes so escape the doubles quote = '\"'; doublequote = "\\\""; singlequote = "\'"; } return quote + strEscape(s, doublequote, singlequote, "`", "\\\\", "$", false) + quote; } /** Escapes regular expression */ String regexpEscape(String s) { return '/' + strEscape(s, "\"", "'", "`", "\\", "$", true) + '/'; } /** Helper to escape JavaScript string as well as regular expression */ private String strEscape( String s, String doublequoteEscape, String singlequoteEscape, String backtickEscape, String backslashEscape, String dollarEscape, boolean isRegexp) { StringBuilder sb = new StringBuilder(s.length() + 2); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '\0': sb.append("\\x00"); break; case '\u000B': if (!isRegexp) { sb.append("\\v"); } else { sb.append("\\x0B"); } break; // From the SingleEscapeCharacter grammar production. case '\b': sb.append("\\b"); break; case '\f': sb.append("\\f"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; case '\\': sb.append(backslashEscape); break; case '\"': sb.append(doublequoteEscape); break; case '\'': sb.append(singlequoteEscape); break; case '$': sb.append(dollarEscape); break; case '`': sb.append(backtickEscape); break; case '=': // '=' is a syntactically significant regexp character. if (trustedStrings || isRegexp) { sb.append(c); } else { sb.append("\\x3d"); } break; case '&': if (trustedStrings || isRegexp) { sb.append(c); } else { sb.append("\\x26"); } break; case '>': if (!trustedStrings && !isRegexp) { sb.append(GT_ESCAPED); break; } // Break --> into --\> or ]]> into ]]\> // // This is just to prevent developers from shooting themselves in the // foot, and does not provide the level of security that you get // with trustedString == false. if (i >= 2 && ((s.charAt(i - 1) == '-' && s.charAt(i - 2) == '-') || (s.charAt(i - 1) == ']' && s.charAt(i - 2) == ']'))) { sb.append(GT_ESCAPED); } else { sb.append(c); } break; case '<': if (!trustedStrings && !isRegexp) { sb.append(LT_ESCAPED); break; } // Break 0x1f && c < 0x7f)) { // If we're given an outputCharsetEncoder, then check if the character can be // represented in this character set. If no charsetEncoder provided - pass straight // Latin characters through, and escape the rest. Doing the explicit character check is // measurably faster than using the CharsetEncoder. sb.append(c); } else { // Other characters can be misinterpreted by some JS parsers, // or perhaps mangled by proxies along the way, // so we play it safe and Unicode escape them. Util.appendHexJavaScriptRepresentation(sb, c); } } } return sb.toString(); } /** * Helper to escape the characters that might be misinterpreted * * @param s the string to modify * @return the string with unrecognizable characters escaped. */ private String escapeUnrecognizedCharacters(String s) { // TODO(yitingwang) Move this method to a suitable place StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { // From the SingleEscapeCharacter grammar production. case '\b': case '\f': case '\n': case '\r': case '\t': case '\\': case '\"': case '\'': case '$': case '`': case '\u2028': case '\u2029': sb.append(c); break; default: if ((outputCharsetEncoder != null && outputCharsetEncoder.canEncode(c)) || (c > 0x1f && c < 0x7f)) { // If we're given an outputCharsetEncoder, then check if the character can be // represented in this character set. If no charsetEncoder provided - pass straight // Latin characters through, and escape the rest. Doing the explicit character check is // measurably faster than using the CharsetEncoder. sb.append(c); } else { // Other characters can be misinterpreted by some JS parsers, // or perhaps mangled by proxies along the way, // so we play it safe and Unicode escape them. Util.appendHexJavaScriptRepresentation(sb, c); } } } return sb.toString(); } static String identifierEscape(String s) { // First check if escaping is needed at all -- in most cases it isn't. if (NodeUtil.isLatin(s)) { return s; } // Now going through the string to escape non-Latin characters if needed. StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); // Identifiers should always go to Latin1/ ASCII characters because // different browser's rules for valid identifier characters are // crazy. if (c > 0x1F && c < 0x7F) { sb.append(c); } else { Util.appendHexJavaScriptRepresentation(sb, c); } } return sb.toString(); } /** * @param maxCount The maximum number of children to look for. * @return The number of children of this node that are non empty up to maxCount. */ private static int getNonEmptyChildCount(Node n, int maxCount) { int i = 0; Node c = n.getFirstChild(); for (; c != null && i < maxCount; c = c.getNext()) { if (c.isBlock()) { i += getNonEmptyChildCount(c, maxCount - i); } else if (!c.isEmpty()) { i++; } } return i; } /** Gets the first non-empty child of the given node. */ private static @Nullable Node getFirstNonEmptyChild(Node n) { for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { if (c.isBlock()) { Node result = getFirstNonEmptyChild(c); if (result != null) { return result; } } else if (!c.isEmpty()) { return c; } } return null; } /** * Information on the current context. Used for disambiguating special cases. For example, a "{" * could indicate the start of an object literal or a block, depending on the current context. */ public enum Context { STATEMENT, BEFORE_DANGLING_ELSE, // a hack to resolve the else-clause ambiguity START_OF_EXPR, // Are we inside the init clause of a for loop? If so, the containing // expression can't contain an in operator. Pass this context flag down // until we reach expressions which no longer have the limitation. IN_FOR_INIT_CLAUSE( /* inForInitClause= */ true, /** at start of arrow fn */ false), // Handle object literals at the start of a non-block arrow function body. // This is only important when the first token after the "=>" is "{". START_OF_ARROW_FN_BODY( /* inForInitClause= */ false, /** at start of arrow fn */ true), START_OF_ARROW_FN_IN_FOR_INIT( /* inForInitClause= */ true, /** atArrowFunctionBody */ true), OTHER; // nothing special to watch out for. // The following two cases are independent, unlike the other enum states, so we have separate // booleans for them. private final boolean inForInitClause; private final boolean atArrowFnBody; Context() { this(false, false); } Context(boolean inForInitClause, boolean atStartOfArrowFnBody) { this.inForInitClause = inForInitClause; this.atArrowFnBody = atStartOfArrowFnBody; } public boolean inForInInitClause() { return inForInitClause; } public boolean atArrowFunctionBody() { return atArrowFnBody; } } private static Context getContextForNonEmptyExpression(Context currentContext) { return currentContext == Context.BEFORE_DANGLING_ELSE ? Context.BEFORE_DANGLING_ELSE : Context.OTHER; } /** * If we're in a IN_FOR_INIT_CLAUSE, we can't permit in operators in the expression. Pass on the * IN_FOR_INIT_CLAUSE flag through subexpressions. */ private static Context getContextForNoInOperator(Context context) { return (context.inForInInitClause() ? context : Context.OTHER); } /** * If we're at the start of an arrow function body, we need parentheses around object literals and * object patterns. We also must also pass the IN_FOR_INIT_CLAUSE flag into subexpressions. */ private static Context getContextForArrowFunctionBody(Context context) { return context.inForInInitClause() ? Context.START_OF_ARROW_FN_IN_FOR_INIT : Context.START_OF_ARROW_FN_BODY; } private void processEnd(Node n, Context context) { switch (n.getToken()) { case CLASS: case INTERFACE: case ENUM: case NAMESPACE: cc.endClass(context == Context.STATEMENT); break; case FUNCTION: if (n.getLastChild().isEmpty()) { cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); } else { cc.endFunction(context == Context.STATEMENT); } break; case DECLARE: if (n.getParent().getToken() != Token.NAMESPACE_ELEMENTS) { processEnd(n.getFirstChild(), context); } break; case EXPORT: if (n.getParent().getToken() != Token.NAMESPACE_ELEMENTS && !n.getFirstChild().isDeclare()) { processEnd(n.getFirstChild(), context); } break; case COMPUTED_PROP: if (n.hasOneChild()) { cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); } break; case MEMBER_FUNCTION_DEF: case GETTER_DEF: case SETTER_DEF: if (n.getFirstChild().getLastChild().isEmpty()) { cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); } break; case MEMBER_VARIABLE_DEF: cc.endStatement(/* needSemiColon= */ true, /*hasTrailingCommentOnSameLine=*/ false); break; default: if (context == Context.STATEMENT) { cc.endStatement(/*hasTrailingCommentOnSameLine=*/ false); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy