Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.NonJSDocComment;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.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";
// A memoizer for formatting strings as JS strings.
private final Map escapedJsStrings = new HashMap<>();
private final CodeConsumer cc;
private final OutputCharsetEncoder outputCharsetEncoder;
private final boolean preferSingleQuotes;
private final boolean preserveTypeAnnotations;
private final boolean printNonJSDocComments;
private final boolean trustedStrings;
private final boolean quoteKeywordProperties;
private final boolean useOriginalName;
private final boolean prettyPrint;
private final FeatureSet outputFeatureSet;
private final JSDocInfoPrinter jsDocInfoPrinter;
private CodeGenerator(CodeConsumer consumer) {
cc = consumer;
outputCharsetEncoder = null;
preferSingleQuotes = false;
trustedStrings = true;
preserveTypeAnnotations = false;
printNonJSDocComments = false;
quoteKeywordProperties = false;
useOriginalName = false;
prettyPrint = 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.quoteKeywordProperties = options.shouldQuoteKeywordProperties();
this.useOriginalName = options.getUseOriginalNamesInOutput();
this.outputFeatureSet = options.getOutputFeatureSet();
this.prettyPrint = options.isPrettyPrint();
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();
}
protected void add(String str) {
cc.add(str);
}
protected void add(Node n) {
add(n, Context.OTHER);
}
protected void add(Node node, Context context) {
if (!cc.continueProcessing()) {
return;
}
if (preserveTypeAnnotations && node.getJSDocInfo() != null) {
String jsdocAsString = jsDocInfoPrinter.print(node.getJSDocInfo());
// Don't print an empty jsdoc
if (!jsdocAsString.equals("/** */ ")) {
add(jsdocAsString);
}
}
// print any non-trailing non-JSDoc comment attached to this node
if (printNonJSDocComments) {
NonJSDocComment nonJSDocComment = node.getNonJSDocComment();
if (nonJSDocComment != null && !nonJSDocComment.isTrailing()) {
String nonJSDocCommentString = node.getNonJSDocCommentString();
if (!nonJSDocCommentString.isEmpty()) {
addNonJsDoc_nonTrailing(node, nonJSDocComment);
}
}
}
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(true);
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();
break;
case VAR:
add("var ");
addList(first, false, getContextForNoInOperator(context), ",");
if (node.getParent() == null || NodeUtil.isStatement(node)) {
cc.endStatement();
}
break;
case CONST:
add("const ");
addList(first, false, getContextForNoInOperator(context), ",");
if (node.getParent() == null || NodeUtil.isStatement(node)) {
cc.endStatement();
}
break;
case LET:
add("let ");
addList(first, false, getContextForNoInOperator(context), ",");
if (node.getParent() == null || NodeUtil.isStatement(node)) {
cc.endStatement();
}
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);
add(last);
}
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);
if (first.isComma() || (first.isCast() && first.getFirstChild().isComma())) {
addExpr(first, NodeUtil.precedence(Token.ASSIGN), Context.OTHER);
} else {
// Add expression, consider nearby code at lowest level of
// precedence.
addExpr(first, 0, 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()
&& !outputFeatureSet.has(Feature.TYPE_ANNOTATION)) {
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:
{
// 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 NEG:
{
checkState(childCount == 1, node);
// It's important to our validity checker that the code we print produces the same AST as
// the code we parse back. NEG is a weird case because Rhino parses "- -2" as "2".
if (node.getFirstChild().isNumber()) {
cc.addNumber(-node.getFirstChild().getDouble(), node.getFirstChild());
} else {
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.isString() || !last.isString()) {
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("...");
add(node.getFirstChild());
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();
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");
add(superClass);
}
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.isQuotedString()
&& 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 SCRIPT:
case MODULE_BODY:
case BLOCK:
case ROOT:
{
if (node.getClass() != Node.class) {
throw new Error("Unexpected Node subclass.");
}
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();
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:
{
checkState(
childCount == 2, "Bad OPTCHAIN_GETPROP: expected 2 children, but got %s", childCount);
checkState(last.isString(), "Bad OPTCHAIN_GETPROP: RHS should be STRING");
addExpr(first, NodeUtil.precedence(type), context);
add(node.isOptionalChainStart() ? "?." : ".");
addIdentifier(last);
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 (node.getFirstChild().matchesQualifiedName("$jscomp.scope")
&& node.getParent().isAssign()) {
add("var ");
}
addIdentifier(node.getOriginalName());
break;
}
checkState(childCount == 2, "Bad GETPROP: expected 2 children, but got %s", childCount);
checkState(last.isString(), "Bad GETPROP: RHS should be STRING");
// 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(last.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.
add("[");
add(last);
add("]");
} else {
add(".");
addIdentifier(last);
}
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:
// 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.
{
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(type), context);
if (needsParens) {
add(")");
}
}
Node args = first.getNext();
add("(");
addList(args);
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();
break;
case DEBUGGER:
Preconditions.checkState(childCount == 0, node);
add("debugger");
cc.endStatement();
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();
break;
case EXPR_RESULT:
Preconditions.checkState(childCount == 1, node);
add(first, Context.START_OF_EXPR);
cc.endStatement();
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 (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(")");
}
break;
case STRING_KEY:
addStringKey(node);
break;
case STRING:
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 && prettyPrint && node.hasTrailingComma()) {
cc.listSeparator();
}
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:
add(first, Context.START_OF_EXPR);
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(true);
break;
case DECLARE:
add("declare");
add(first);
processEnd(node, context);
break;
case INDEX_SIGNATURE:
add("[");
add(first);
add("]");
maybeAddTypeDecl(node);
cc.endStatement(true);
break;
case CALL_SIGNATURE:
if (node.getBooleanProp(Node.CONSTRUCT_SIGNATURE)) {
add("new ");
}
maybeAddGenericTypes(node);
add(first);
maybeAddTypeDecl(node);
cc.endStatement(true);
break;
default:
throw new IllegalStateException("Unknown token " + type + "\n" + node.toStringTree());
}
// print any trailing nonJSDoc comment attached to this node
if (printNonJSDocComments) {
NonJSDocComment nonJSDocComment = node.getNonJSDocComment();
if (nonJSDocComment != null && nonJSDocComment.isTrailing()) {
String nonJSDocCommentString = node.getNonJSDocCommentString();
if (!nonJSDocCommentString.isEmpty()) {
addNonJsDoc_trailing(node, nonJSDocComment);
}
}
}
cc.endSourceMapping(node);
}
private void addIdentifier(String identifier) {
cc.addIdentifier(identifierEscape(identifier));
}
private void addIdentifier(Node identifier) {
cc.startSourceMapping(identifier);
addIdentifier(identifier.getString());
cc.endSourceMapping(identifier);
}
private int precedence(Node n) {
if (n.isCast()) {
return precedence(n.getFirstChild());
}
return NodeUtil.precedence(n.getToken());
}
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()) {
// LeftHandSideExpression OP LeftHandSideExpression
// OP LeftHandSideExpression | LeftHandSideExpression OP
// MemberExpression TemplateLiteral
// MemberExpression '.' IdentifierName
return true;
} else if (parent.isGetElem() || parent.isCall() || parent.isHook()) {
// 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);
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);
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.shouldPreserveExtraBlocks()) {
cc.beginBlock();
cc.endBlock(cc.breakAfterBlockFor(n, context == Context.STATEMENT));
} else {
cc.endStatement(true);
}
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.shouldPreserveExtraBlocks();
if (alwaysWrapInBlock || isBlockDeclOrDo(firstAndOnlyChild)) {
cc.beginBlock();
add(firstAndOnlyChild, Context.STATEMENT);
cc.maybeLineBreak();
cc.endBlock(cc.breakAfterBlockFor(n, context == Context.STATEMENT));
return;
} else {
// Continue with the only child.
nodeToProcess = firstAndOnlyChild;
}
}
}
if (nodeToProcess.isEmpty()) {
cc.endStatement(true);
} else {
add(nodeToProcess, context);
}
}
/**
* @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.getToken() == Token.EXPONENT && 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;
if (isFirst) {
addExpr(n, isArrayOrFunctionArgument ? 1 : 0, lhsContext);
} else {
cc.addOp(separator, true);
addExpr(n, isArrayOrFunctionArgument ? 1 : 0,
getContextForNoInOperator(lhsContext));
}
}
if (prettyPrint
&& isArrayOrFunctionArgument
&& checkNotNull(firstInList.getParent()).hasTrailingComma()) {
cc.listSeparator();
}
}
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.isQuotedString()
|| (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("}");
}
/**
* This function adds a comma-separated list as is specified by an ARRAYLIT node with the
* associated skipIndexes array. This is a space optimization since we avoid creating a whole Node
* object for each empty array literal slot.
*
* @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 || (prettyPrint && checkNotNull(firstInList.getParent()).hasTrailingComma())) {
cc.listSeparator();
}
}
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(
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 addNonJsDoc_trailing(Node n, NonJSDocComment nonJSDocComment) {
String content = nonJSDocComment.getCommentString();
SourcePosition commentStartPosition = nonJSDocComment.getStartPosition();
int nodeLineNumber = n.getLineno() - 1; // source lines are 1-indexed
if (nonJSDocComment.isEndingAsLineComment()) {
// Trailing line comments *must* be on the same line as the node. e.g. `let a; // some`
checkState(
commentStartPosition.line == nodeLineNumber,
"Trailing line comments *must* be on the same line as the node.");
// Trailing line comments *must* end with a `\n`. E.g.. `let x; //comment\n`
add(" " + content + "\n");
} else {
if (nonJSDocComment.isInline()) {
// e.g. `foo(x /*comment*/);` is inline
add(" " + content);
} else {
// e.g. `let x; /*comment*/` is non-inline
add(" " + content + "\n");
}
}
}
/** Outputs a JS string, using the optimal (single/double) quote character */
private void addJsString(Node n) {
String s = n.getString();
boolean useSlashV = n.getBooleanProp(Node.SLASH_V);
if (useSlashV) {
add(jsString(n.getString(), useSlashV));
} else {
String cached =
escapedJsStrings.computeIfAbsent(s, (String k) -> jsString(n.getString(), useSlashV));
add(cached);
}
}
private String jsString(String s, boolean useSlashV) {
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, "`", "\\\\", "$", useSlashV, false)
+ quote;
}
/** Escapes regular expression */
String regexpEscape(String s) {
return '/' + strEscape(s, "\"", "'", "`", "\\", "$", false, 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 useSlashV,
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 (useSlashV) {
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 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(true);
} 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().getToken() != Token.DECLARE) {
processEnd(n.getFirstChild(), context);
}
break;
case COMPUTED_PROP:
if (n.hasOneChild()) {
cc.endStatement(true);
}
break;
case MEMBER_FUNCTION_DEF:
case GETTER_DEF:
case SETTER_DEF:
if (n.getFirstChild().getLastChild().isEmpty()) {
cc.endStatement(true);
}
break;
case MEMBER_VARIABLE_DEF:
cc.endStatement(true);
break;
default:
if (context == Context.STATEMENT) {
cc.endStatement();
}
}
}
}