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

com.google.javascript.jscomp.parsing.IRFactory 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.

The newest version!
/*
 * Copyright 2013 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.parsing;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.base.JSCompObjects.identical;
import static java.lang.Integer.parseInt;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.javascript.jscomp.base.format.SimpleFormat;
import com.google.javascript.jscomp.parsing.Config.JsDocParsing;
import com.google.javascript.jscomp.parsing.Config.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.jscomp.parsing.parser.IdentifierToken;
import com.google.javascript.jscomp.parsing.parser.LiteralToken;
import com.google.javascript.jscomp.parsing.parser.SourceFile;
import com.google.javascript.jscomp.parsing.parser.TemplateLiteralToken;
import com.google.javascript.jscomp.parsing.parser.TokenType;
import com.google.javascript.jscomp.parsing.parser.trees.ArgumentListTree;
import com.google.javascript.jscomp.parsing.parser.trees.ArrayLiteralExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ArrayPatternTree;
import com.google.javascript.jscomp.parsing.parser.trees.AwaitExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.BinaryOperatorTree;
import com.google.javascript.jscomp.parsing.parser.trees.BlockTree;
import com.google.javascript.jscomp.parsing.parser.trees.BreakStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.CallExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.CaseClauseTree;
import com.google.javascript.jscomp.parsing.parser.trees.CatchTree;
import com.google.javascript.jscomp.parsing.parser.trees.ClassDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.CommaExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
import com.google.javascript.jscomp.parsing.parser.trees.ComprehensionForTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComprehensionIfTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComprehensionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComputedPropertyDefinitionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComputedPropertyFieldTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComputedPropertyGetterTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComputedPropertyMethodTree;
import com.google.javascript.jscomp.parsing.parser.trees.ComputedPropertySetterTree;
import com.google.javascript.jscomp.parsing.parser.trees.ConditionalExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ContinueStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.DebuggerStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.DefaultClauseTree;
import com.google.javascript.jscomp.parsing.parser.trees.DefaultParameterTree;
import com.google.javascript.jscomp.parsing.parser.trees.DoWhileStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.DynamicImportTree;
import com.google.javascript.jscomp.parsing.parser.trees.EmptyStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.ExportDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.ExportSpecifierTree;
import com.google.javascript.jscomp.parsing.parser.trees.ExpressionStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.FieldDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.FinallyTree;
import com.google.javascript.jscomp.parsing.parser.trees.ForAwaitOfStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.ForInStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.ForOfStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.ForStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.FormalParameterListTree;
import com.google.javascript.jscomp.parsing.parser.trees.FunctionDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.GetAccessorTree;
import com.google.javascript.jscomp.parsing.parser.trees.IdentifierExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.IfStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.ImportDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.ImportMetaExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ImportSpecifierTree;
import com.google.javascript.jscomp.parsing.parser.trees.IterRestTree;
import com.google.javascript.jscomp.parsing.parser.trees.IterSpreadTree;
import com.google.javascript.jscomp.parsing.parser.trees.LabelledStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.LiteralExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.MemberExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.MemberLookupExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.MissingPrimaryExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.NewExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.NewTargetExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.NullTree;
import com.google.javascript.jscomp.parsing.parser.trees.ObjectLiteralExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ObjectPatternTree;
import com.google.javascript.jscomp.parsing.parser.trees.ObjectSpreadTree;
import com.google.javascript.jscomp.parsing.parser.trees.OptChainCallExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.OptionalMemberExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.OptionalMemberLookupExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ParenExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ParseTree;
import com.google.javascript.jscomp.parsing.parser.trees.ParseTreeType;
import com.google.javascript.jscomp.parsing.parser.trees.ProgramTree;
import com.google.javascript.jscomp.parsing.parser.trees.PropertyNameAssignmentTree;
import com.google.javascript.jscomp.parsing.parser.trees.ReturnStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.SetAccessorTree;
import com.google.javascript.jscomp.parsing.parser.trees.SuperExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.SwitchStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.TemplateLiteralExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.TemplateLiteralPortionTree;
import com.google.javascript.jscomp.parsing.parser.trees.TemplateSubstitutionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ThisExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.ThrowStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.TryStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.UnaryExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.UpdateExpressionTree;
import com.google.javascript.jscomp.parsing.parser.trees.UpdateExpressionTree.OperatorPosition;
import com.google.javascript.jscomp.parsing.parser.trees.VariableDeclarationListTree;
import com.google.javascript.jscomp.parsing.parser.trees.VariableDeclarationTree;
import com.google.javascript.jscomp.parsing.parser.trees.VariableStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.WhileStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.WithStatementTree;
import com.google.javascript.jscomp.parsing.parser.trees.YieldExpressionTree;
import com.google.javascript.jscomp.parsing.parser.util.SourcePosition;
import com.google.javascript.jscomp.parsing.parser.util.SourceRange;
import com.google.javascript.rhino.ErrorReporter;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.NonJSDocComment;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;
import com.google.javascript.rhino.dtoa.DToA;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.nullness.Nullable;

/** IRFactory transforms the external AST to the internal AST. */
class IRFactory {

  static final String GETTER_ERROR_MESSAGE =
      "getters are not supported in older versions of JavaScript. "
          + "If you are targeting newer versions of JavaScript, "
          + "set the appropriate language_in option.";

  static final String SETTER_ERROR_MESSAGE =
      "setters are not supported in older versions of JavaScript. "
          + "If you are targeting newer versions of JavaScript, "
          + "set the appropriate language_in option.";

  static final String INVALID_ES3_PROP_NAME =
      "Keywords and reserved words are not allowed as unquoted property "
          + "names in older versions of JavaScript. "
          + "If you are targeting newer versions of JavaScript, "
          + "set the appropriate language_in option.";

  static final String INVALID_ES5_STRICT_OCTAL =
      "Octal integer literals are not supported in strict mode.";

  static final String INVALID_OCTAL_DIGIT = "Invalid octal digit in octal literal.";

  static final String STRING_CONTINUATION_WARNING =
      "String continuations are not recommended. See"
          + " https://google.github.io/styleguide/jsguide.html#features-strings-no-line-continuations";

  static final String OCTAL_STRING_LITERAL_WARNING =
      "Octal literals in strings are not supported in this language mode.";

  static final String DUPLICATE_PARAMETER = "Duplicate parameter name \"%s\"";

  static final String DUPLICATE_LABEL = "Duplicate label \"%s\"";

  static final String UNLABELED_BREAK = "unlabelled break must be inside loop or switch";

  static final String UNEXPECTED_CONTINUE = "continue must be inside loop";

  static final String UNEXPECTED_LABELLED_CONTINUE =
      "continue can only use labels of iteration statements";

  static final String UNEXPECTED_RETURN = "return must be inside function";
  static final String UNEXPECTED_YIELD = "yield must be inside generator function";
  static final String UNEXPECTED_AWAIT = "await must be inside asynchronous function";
  static final String UNEXPECTED_NEW_DOT_TARGET = "new.target must be inside a function";
  static final String UNDEFINED_LABEL = "undefined label \"%s\"";

  private final StaticSourceFile sourceFile;
  private final @Nullable String sourceName;

  /** Source file reference that also contains the file content. */
  private final SourceFile fileWithContent;

  private final Config config;
  private final ErrorReporter errorReporter;
  private final TransformDispatcher transformDispatcher;

  private static final ImmutableSet ES5_RESERVED_KEYWORDS =
      ImmutableSet.of(
          // From Section 7.6.1.2
          "class", "const", "enum", "export", "extends", "import", "super");
  private static final ImmutableSet ES5_STRICT_RESERVED_KEYWORDS =
      ImmutableSet.of(
          // From Section 7.6.1.2
          "class",
          "const",
          "enum",
          "export",
          "extends",
          "import",
          "super",
          "implements",
          "interface",
          "let",
          "package",
          "private",
          "protected",
          "public",
          "static",
          "yield");

  /** If non-null, use this set of keywords instead of TokenStream.isKeyword(). */
  private final @Nullable ImmutableSet reservedKeywords;

  private final Set parsedComments = new LinkedHashSet<>();

  private final LinkedHashSet licenseBuilder = new LinkedHashSet<>();
  private @Nullable JSDocInfo firstFileoverview = null;

  // Use a template node for properties set on all nodes to minimize the
  // memory footprint associated with these.
  private final Node templateNode;

  private final CommentTracker jsdocTracker;
  private final CommentTracker nonJsdocTracker;
  private final JsDocInfoParser.JsDocSourceKind jsDocSourceKind;

  private boolean currentFileIsExterns = false;

  private FeatureSet features = FeatureSet.BARE_MINIMUM;
  private Node resultNode;

  private IRFactory(
      StaticSourceFile sourceFile,
      Config config,
      ErrorReporter errorReporter,
      ImmutableList comments,
      SourceFile fileWithContent,
      JsDocInfoParser.JsDocSourceKind jsDocSourceKind) {
    this.jsdocTracker = new CommentTracker(comments, (c) -> c.type == Comment.Type.JSDOC);
    this.nonJsdocTracker = new CommentTracker(comments, (c) -> c.type != Comment.Type.JSDOC);
    this.sourceFile = sourceFile;
    this.fileWithContent = fileWithContent;
    // The template node properties are applied to all nodes in this transform.
    this.templateNode = createTemplateNode();

    // Sometimes this will be null in tests.
    this.sourceName = sourceFile == null ? null : sourceFile.getName();

    this.config = config;
    this.errorReporter = errorReporter;
    this.transformDispatcher = new TransformDispatcher();

    if (config.strictMode().isStrict()) {
      reservedKeywords = ES5_STRICT_RESERVED_KEYWORDS;
    } else if (config.languageMode() == LanguageMode.ECMASCRIPT3) {
      reservedKeywords = null; // use TokenStream.isKeyword instead
    } else {
      reservedKeywords = ES5_RESERVED_KEYWORDS;
    }
    this.jsDocSourceKind = jsDocSourceKind;
  }

  private static final class CommentTracker {
    private final ImmutableList source;
    private final Predicate filter;
    private int index = -1;

    CommentTracker(ImmutableList source, Predicate filter) {
      this.source = source;
      this.filter = filter;

      this.advance();
    }

    @Nullable Comment current() {
      return (this.index >= this.source.size()) ? null : this.source.get(this.index);
    }

    void advance() {
      while (true) {
        this.index++; // Always advance at least one element.

        Comment c = this.current();
        if (c == null || this.filter.test(c)) {
          break;
        }
      }
    }

    boolean hasPendingCommentBefore(SourcePosition pos) {
      Comment c = this.current();
      // The line number matters for trailing comments
      return c != null && c.location.end.line <= pos.line && c.location.end.offset <= pos.offset;
    }
  }

  // Create a template node to use as a source of common attributes, this allows
  // the prop structure to be shared among all the node from this source file.
  // This reduces the cost of these properties to O(nodes) to O(files).
  private Node createTemplateNode() {
    // The Node type choice is arbitrary.
    Node templateNode = new Node(Token.SCRIPT);
    templateNode.setStaticSourceFile(sourceFile);
    return templateNode;
  }

  public static IRFactory transformTree(
      ProgramTree tree,
      StaticSourceFile sourceFile,
      Config config,
      ErrorReporter errorReporter,
      SourceFile file) {
    JsDocInfoParser.JsDocSourceKind jsDocSourceKind =
        sourceFile.getName().endsWith(".closure.js")
            ? JsDocInfoParser.JsDocSourceKind.TSICKLE
            : JsDocInfoParser.JsDocSourceKind.NORMAL;
    IRFactory irFactory =
        new IRFactory(
            sourceFile, config, errorReporter, tree.sourceComments, file, jsDocSourceKind);

    // don't call transform as we don't want standard jsdoc handling.
    Node n = irFactory.transformDispatcher.process(tree);
    irFactory.setSourceInfo(n, tree);

    if (tree.sourceComments != null) {
      for (Comment comment : tree.sourceComments) {
        if ((comment.type == Comment.Type.JSDOC || comment.type == Comment.Type.IMPORTANT)
            && !irFactory.parsedComments.contains(comment)) {
          irFactory.handlePossibleFileOverviewJsDoc(comment);
        }
      }

      if (n.isScript()) {
        SourcePosition endOfFilePos = tree.location.end;
        // Handle end of file comments that are still pending
        NonJSDocComment nonJSDocComment =
            irFactory.parseNonJSDocCommentAt(endOfFilePos, /* isInline= */ false);
        if (nonJSDocComment != null) {
          // pending end-of-file comment exists && parsing mode is set to INCLUDE_ALL_COMMENTS
          n.setTrailingNonJSDocComment(nonJSDocComment);
        }
      }
    }

    irFactory.setFileOverviewJsDoc(n);

    irFactory.validateAll(n);
    irFactory.resultNode = n;

    return irFactory;
  }

  Node getResultNode() {
    return resultNode;
  }

  FeatureSet getFeatures() {
    return features;
  }

  private void validateAll(Node n) {
    ArrayDeque work = new ArrayDeque<>();
    while (n != null) {
      validate(n);
      Node nextSibling = n.getNext();
      Node firstChild = n.getFirstChild();
      if (firstChild != null) {
        if (nextSibling != null) {
          // handle the siblings later
          work.push(nextSibling);
        }
        n = firstChild;
      } else if (nextSibling != null) {
        // no children, handle the next sibling
        n = nextSibling;
      } else {
        // no siblings, continue with work we have saved on the work queue
        n = work.poll();
      }
    }
    checkState(work.isEmpty());
  }

  private void validate(Node n) {
    validateParameters(n);
    validateBreakContinue(n);
    validateReturn(n);
    validateYield(n);
    validateAwait(n);
    validateNewDotTarget(n);
    validateLabel(n);
    validateBlockScopedFunctions(n);
  }

  private void validateAwait(Node n) {
    if (n.isAwait()) {
      Node parent = n;
      while ((parent = parent.getParent()) != null) {
        // The await is in a class static block.
        // e.g. `class C { static { await; } }`
        if (parent.isClassMembers()) {
          errorReporter.error(UNEXPECTED_AWAIT, sourceName, n.getLineno(), n.getCharno());
          return;
        }
        if (parent.isAsyncFunction()) {
          return;
        } else if (parent.isFunction()) {
          // The await is in a non-async function.
          // e.g. `function f() { return await 5; }`
          // nested function e.g. `async function f() { function f2() { return await 5; } }`
          errorReporter.error(UNEXPECTED_AWAIT, sourceName, n.getLineno(), n.getCharno());
          return;
        }
      }
      errorReporter.error(UNEXPECTED_AWAIT, sourceName, n.getLineno(), n.getCharno());
    }
  }

  private void validateYield(Node n) {
    if (n.isYield()) {
      Node parent = n;
      while ((parent = parent.getParent()) != null) {
        // The yield is in a class static block.
        // e.g. `class C { static { yield; } }`
        if (parent.isClassMembers()) {
          errorReporter.error(UNEXPECTED_YIELD, sourceName, n.getLineno(), n.getCharno());
          return;
        }
        if (parent.isGeneratorFunction()) {
          return;
        }
      }
    }
  }

  private void validateReturn(Node n) {
    if (n.isReturn()) {
      Node parent = n;
      while ((parent = parent.getParent()) != null) {
        // The return is in a class static block.
        // e.g. `class C { static { return; } }`
        if (parent.isClassMembers()) {
          errorReporter.error(UNEXPECTED_RETURN, sourceName, n.getLineno(), n.getCharno());
          return;
        }
        if (parent.isFunction()) {
          return;
        }
      }
      errorReporter.error(UNEXPECTED_RETURN, sourceName, n.getLineno(), n.getCharno());
    }
  }

  private void validateNewDotTarget(Node n) {
    if (n.getToken() == Token.NEW_TARGET) {
      Node parent = n;
      while ((parent = parent.getParent()) != null) {
        if (parent.isFunction()) {
          return;
        }
      }
      errorReporter.error(UNEXPECTED_NEW_DOT_TARGET, sourceName, n.getLineno(), n.getCharno());
    }
  }

  private void validateBreakContinue(Node n) {
    if (n.isBreak() || n.isContinue()) {
      Node labelName = n.getFirstChild();
      if (labelName != null) {
        Node parent = n.getParent();
        while (!parent.isLabel() || !labelsMatch(parent, labelName)) {
          if (parent.isFunction() || parent.isScript() || parent.isClassMembers()) {
            // report missing label
            errorReporter.error(
                SimpleFormat.format(UNDEFINED_LABEL, labelName.getString()),
                sourceName,
                n.getLineno(),
                n.getCharno());
            break;
          }
          parent = parent.getParent();
        }
        if (parent.isLabel() && labelsMatch(parent, labelName)) {
          if (n.isContinue() && !isContinueTarget(parent.getLastChild())) {
            // report invalid continue target
            errorReporter.error(
                UNEXPECTED_LABELLED_CONTINUE, sourceName, n.getLineno(), n.getCharno());
          }
        }
      } else {
        if (n.isContinue()) {
          Node parent = n.getParent();
          while (!isContinueTarget(parent)) {
            if (parent.isFunction() || parent.isScript() || parent.isClassMembers()) {
              // report invalid continue
              errorReporter.error(UNEXPECTED_CONTINUE, sourceName, n.getLineno(), n.getCharno());
              break;
            }
            parent = parent.getParent();
          }
        } else {
          Node parent = n.getParent();
          while (!isBreakTarget(parent)) {
            if (parent.isFunction() || parent.isScript() || parent.isClassMembers()) {
              // report invalid break
              errorReporter.error(UNLABELED_BREAK, sourceName, n.getLineno(), n.getCharno());
              break;
            }
            parent = parent.getParent();
          }
        }
      }
    }
  }

  private static boolean isBreakTarget(Node n) {
    switch (n.getToken()) {
      case FOR:
      case FOR_IN:
      case FOR_OF:
      case FOR_AWAIT_OF:
      case WHILE:
      case DO:
      case SWITCH:
        return true;
      default:
        return false;
    }
  }

  private static boolean isContinueTarget(Node n) {
    switch (n.getToken()) {
      case FOR:
      case FOR_IN:
      case FOR_OF:
      case FOR_AWAIT_OF:
      case WHILE:
      case DO:
        return true;
      default:
        return false;
    }
  }

  private static boolean labelsMatch(Node label, Node labelName) {
    return label.getFirstChild().getString().equals(labelName.getString());
  }

  private void validateLabel(Node n) {
    if (n.isLabel()) {
      Node labelName = n.getFirstChild();
      for (Node parent = n.getParent();
          parent != null && !parent.isFunction();
          parent = parent.getParent()) {
        if (parent.isLabel() && labelsMatch(parent, labelName)) {
          errorReporter.error(
              SimpleFormat.format(DUPLICATE_LABEL, labelName.getString()),
              sourceName,
              n.getLineno(),
              n.getCharno());
          break;
        }
      }
    }
  }

  private void validateParameters(Node n) {
    if (n.isParamList()) {
      Set seenNames = new LinkedHashSet<>();
      for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
        ParsingUtil.getParamOrPatternNames(
            c,
            (Node param) -> {
              String paramName = param.getString();
              if (!seenNames.add(paramName)) {
                errorReporter.warning(
                    SimpleFormat.format(DUPLICATE_PARAMETER, paramName),
                    sourceName,
                    param.getLineno(),
                    param.getCharno());
              }
            });
      }
    }
  }

  private void validateBlockScopedFunctions(Node n) {
    if (n.isFunction() && n.getParent().isBlock() && !n.getGrandparent().isFunction()) {
      maybeWarnForFeature(n, Feature.BLOCK_SCOPED_FUNCTION_DECLARATION);
    }
  }

  private void setFileOverviewJsDoc(Node irNode) {
    JSDocInfo.Builder fileoverview =
        (this.firstFileoverview == null) ? JSDocInfo.builder() : this.firstFileoverview.toBuilder();

    if (!this.licenseBuilder.isEmpty()) {
      fileoverview.recordLicense(String.join("", this.licenseBuilder));
    }

    irNode.setJSDocInfo(fileoverview.build(false));
  }

  Node transformBlock(ParseTree node) {
    Node irNode = transform(node);
    if (irNode.isBlock()) {
      return irNode;
    }

    Node newBlock = irNode.isEmpty() ? newNode(Token.BLOCK) : newNode(Token.BLOCK, irNode);
    setSourceInfo(newBlock, irNode);
    newBlock.setIsAddedBlock(true);

    return newBlock;
  }

  /**
   * @return true if the jsDocParser represents a fileoverview.
   */
  private boolean handlePossibleFileOverviewJsDoc(JsDocInfoParser jsDocParser) {
    if (jsDocParser.getLicenseText() != null) {
      this.licenseBuilder.add(jsDocParser.getLicenseText());
    }

    JSDocInfo newFileoverview = jsDocParser.getFileOverviewJSDocInfo();
    if (identical(newFileoverview, this.firstFileoverview)) {
      return false;
    }

    if (this.firstFileoverview == null) {
      this.firstFileoverview = newFileoverview;
      this.currentFileIsExterns = newFileoverview.isExterns();
    } else {
      JSDocInfo.Builder merged = this.firstFileoverview.toBuilder();
      if (!newFileoverview.getSuppressions().isEmpty()) {
        merged.recordSuppressions(newFileoverview.getSuppressions());
      }
      if (newFileoverview.isExterns()) {
        this.currentFileIsExterns = true;
        merged.recordExterns();
      }
      if (newFileoverview.isNoCoverage()) {
        merged.recordNoCoverage();
      }
      this.firstFileoverview = merged.build();
    }

    return true;
  }

  private void handlePossibleFileOverviewJsDoc(Comment comment) {
    JsDocInfoParser jsDocParser = createJsDocInfoParser(comment);
    parsedComments.add(comment);
    handlePossibleFileOverviewJsDoc(jsDocParser);
  }

  private Comment getJSDocCommentAt(SourcePosition pos) {
    Comment closestPreviousComment = null;
    while (this.jsdocTracker.hasPendingCommentBefore(pos)) {
      closestPreviousComment = this.jsdocTracker.current();
      this.jsdocTracker.advance();
    }

    return closestPreviousComment;
  }

  private @Nullable JSDocInfo parseJSDocInfoFrom(Comment comment) {
    if (comment != null) {
      JsDocInfoParser jsDocParser = createJsDocInfoParser(comment);
      parsedComments.add(comment);
      if (!handlePossibleFileOverviewJsDoc(jsDocParser)) {
        return jsDocParser.retrieveAndResetParsedJSDocInfo();
      }
    }
    return null;
  }

  private @Nullable JSDocInfo parseJSDocInfoOnTree(ParseTree tree) {
    switch (tree.type) {
      case EXPRESSION_STATEMENT:
      case LABELLED_STATEMENT:
      case EXPORT_DECLARATION:
      case TEMPLATE_SUBSTITUTION:
        return null;

      case CALL_EXPRESSION:
      case CONDITIONAL_EXPRESSION:
      case BINARY_OPERATOR:
      case MEMBER_EXPRESSION:
      case MEMBER_LOOKUP_EXPRESSION:
      case UPDATE_EXPRESSION:
        {
          ParseTree nearest = findNearestNode(tree);
          if (nearest.type == ParseTreeType.PAREN_EXPRESSION) {
            return null;
          }
        }
        break;

      default:
        break;
    }

    return parseJSDocInfoFrom(getJSDocCommentAt(tree.getStart()));
  }

  JSDocInfo parseJSDocInfoOnToken(com.google.javascript.jscomp.parsing.parser.Token token) {
    return parseJSDocInfoFrom(getJSDocCommentAt(token.getStart()));
  }

  JSDocInfo parseInlineJSDocAt(SourcePosition pos) {
    Comment comment = getJSDocCommentAt(pos);
    return (comment != null && !comment.value.contains("@"))
        ? parseInlineTypeDoc(comment)
        : parseJSDocInfoFrom(comment);
  }

  /**
   * Creates a single NonJSDocComment from every comment associated with this node; or null if there
   * are no such comments.
   *
   * 

It would be legal to replace all comments associated with this node with that one string. */ private @Nullable NonJSDocComment parseNonJSDocCommentAt(SourcePosition pos, boolean isInline) { if (config.jsDocParsingMode() != JsDocParsing.INCLUDE_ALL_COMMENTS) { return null; } if (!this.nonJsdocTracker.hasPendingCommentBefore(pos)) { return null; } StringBuilder result = new StringBuilder(); Comment firstComment = this.nonJsdocTracker.current(); Comment lastComment = null; while (this.nonJsdocTracker.hasPendingCommentBefore(pos)) { Comment currentComment = this.nonJsdocTracker.current(); if (lastComment != null) { for (int blankCount = currentComment.location.start.line - lastComment.location.end.line; blankCount > 0; blankCount--) { result.append("\n"); } } result.append(currentComment.value); lastComment = currentComment; this.nonJsdocTracker.advance(); } NonJSDocComment nonJSDocComment = new NonJSDocComment( firstComment.location.start, lastComment.location.end, result.toString()); nonJSDocComment.setEndsAsLineComment(lastComment.type == Comment.Type.LINE); nonJSDocComment.setIsInline(isInline); return nonJSDocComment; } /** * Creates a single NonJSDocComment from the comment immediately following this node; or null if * there is no such comment. * * @param tokenEnd The end position after which we're looking for a trailing comment * @param possibleNextTokenStart last source position that we're interested in when looking for a * trailing comment */ private @Nullable NonJSDocComment parseTrailingNonJSDocCommentAt( SourcePosition tokenEnd, SourcePosition possibleNextTokenStart) { if (config.jsDocParsingMode() != JsDocParsing.INCLUDE_ALL_COMMENTS) { return null; } if (!this.nonJsdocTracker.hasPendingCommentBefore(possibleNextTokenStart)) { return null; } StringBuilder result = new StringBuilder(); Comment comment = this.nonJsdocTracker.current(); // Disregard comments that don't start on the same line if (tokenEnd.line != comment.location.start.line) { return null; } // Disregard comments that may be within the current statement if (comment.location.start.offset <= tokenEnd.offset) { return null; } // Check if there are only whitespace characters between the current token and the start of the // comment. String preCommentText = this.fileWithContent.contents.substring(tokenEnd.offset + 1, comment.location.start.offset); if (!preCommentText.trim().isEmpty()) { return null; } // TODO(user): handle multiple trailing comments like /* c1 */ /* c2 */ // c3 result.append(comment.value); this.nonJsdocTracker.advance(); NonJSDocComment nonJSDocComment = new NonJSDocComment(comment.location.start, comment.location.end, result.toString()); nonJSDocComment.setEndsAsLineComment(comment.type == Comment.Type.LINE); return nonJSDocComment; } private static ParseTree findNearestNode(ParseTree tree) { while (true) { switch (tree.type) { case EXPRESSION_STATEMENT: tree = tree.asExpressionStatement().expression; continue; case CALL_EXPRESSION: tree = tree.asCallExpression().operand; continue; case BINARY_OPERATOR: tree = tree.asBinaryOperator().left; continue; case CONDITIONAL_EXPRESSION: tree = tree.asConditionalExpression().condition; continue; case MEMBER_EXPRESSION: tree = tree.asMemberExpression().operand; continue; case MEMBER_LOOKUP_EXPRESSION: tree = tree.asMemberLookupExpression().operand; continue; case UPDATE_EXPRESSION: tree = tree.asUpdateExpression().operand; continue; default: return tree; } } } Node transform(ParseTree tree) { JSDocInfo info = parseJSDocInfoOnTree(tree); NonJSDocComment comment = parseNonJSDocCommentAt(tree.getStart(), false); Node node = transformDispatcher.process(tree); if (info != null) { node = maybeInjectCastNode(tree, info, node); node.setJSDocInfo(info); } if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, tree); return node; } private Node maybeInjectCastNode(ParseTree node, JSDocInfo info, Node irNode) { if (node.type == ParseTreeType.PAREN_EXPRESSION && info.hasType()) { irNode = newNode(Token.CAST, irNode); } return irNode; } /** * Names and destructuring patterns, in parameters or variable declarations are special, because * they can have inline type docs attached. * *

function f(/** string */ x) {}
* * annotates 'x' as a string. * * @see Using Inline Doc * Comments */ Node transformNodeWithInlineComments(ParseTree tree) { JSDocInfo info = parseInlineJSDocAt(tree.getStart()); NonJSDocComment comment = parseNonJSDocCommentAt(tree.getStart(), true); Node node = transformDispatcher.process(tree); if (info != null) { node.setJSDocInfo(info); } if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, tree); return node; } static int lineno(ParseTree node) { return lineno(node.location.start); } static int lineno(com.google.javascript.jscomp.parsing.parser.Token token) { return lineno(token.location.start); } static int lineno(SourcePosition location) { // location lines start at zero, our AST starts at 1. return location.line + 1; } static int charno(ParseTree node) { return charno(node.location.start); } static int charno(com.google.javascript.jscomp.parsing.parser.Token token) { return charno(token.location.start); } static int charno(SourcePosition location) { return location.column; } static String languageFeatureWarningMessage(Feature feature) { LanguageMode forFeature = LanguageMode.minimumRequiredFor(feature); if (forFeature == LanguageMode.UNSUPPORTED) { return "This language feature is not currently supported by the compiler: " + feature; } else { return "This language feature is only supported for " + LanguageMode.minimumRequiredFor(feature) + " mode or better: " + feature; } } void maybeWarnForFeature(ParseTree node, Feature feature) { features = features.with(feature); if (!isSupportedForInputLanguageMode(feature)) { errorReporter.warning( languageFeatureWarningMessage(feature), sourceName, lineno(node), charno(node)); } } void maybeWarnForFeature( com.google.javascript.jscomp.parsing.parser.Token token, Feature feature) { features = features.with(feature); if (!isSupportedForInputLanguageMode(feature)) { errorReporter.warning( languageFeatureWarningMessage(feature), sourceName, lineno(token), charno(token)); } } void maybeWarnForFeature(Node node, Feature feature) { features = features.with(feature); if (!isSupportedForInputLanguageMode(feature)) { errorReporter.warning( languageFeatureWarningMessage(feature), sourceName, node.getLineno(), node.getCharno()); } } void setSourceInfo(Node node, Node ref) { node.setLinenoCharno(ref.getLineno(), ref.getCharno()); setLengthFrom(node, ref); } void setSourceInfo(Node irNode, ParseTree node) { if (irNode.getLineno() == -1) { setSourceInfo(irNode, node.location.start, node.location.end); } } void setSourceInfo(Node irNode, com.google.javascript.jscomp.parsing.parser.Token token) { setSourceInfo(irNode, token.location.start, token.location.end); } void setSourceInfo(Node node, SourcePosition start, SourcePosition end) { if (node.getLineno() == -1) { // If we didn't already set the line, then set it now. This avoids // cases like ParenthesizedExpression where we just return a previous // node, but don't want the new node to get its parent's line number. node.setLinenoCharno(lineno(start), charno(start)); setLength(node, start, end); } } /** * Creates a JsDocInfoParser and parses the JsDoc string. * *

Used both for handling individual JSDoc comments and for handling file-level JSDoc comments * (@fileoverview and @license). * * @param node The JsDoc Comment node to parse. * @return A JsDocInfoParser. Will contain either fileoverview JsDoc, or normal JsDoc, or no JsDoc * (if the method parses to the wrong level). */ private JsDocInfoParser createJsDocInfoParser(Comment node) { String comment = node.value; int lineno = lineno(node.location.start); int charno = charno(node.location.start); int position = node.location.start.offset; // The JsDocInfoParser expects the comment without the initial '/**'. int numOpeningChars = 3; JsDocInfoParser jsdocParser = new JsDocInfoParser( new JsDocTokenStream( comment.substring(numOpeningChars), lineno, charno + numOpeningChars), comment, position, templateNode, config, jsDocSourceKind, errorReporter); jsdocParser.setFileOverviewJSDocInfo(this.firstFileoverview); if (node.type == Comment.Type.IMPORTANT && node.value.length() > 0) { jsdocParser.parseImportantComment(); } else { jsdocParser.parse(); } return jsdocParser; } /** Parses inline type info. */ private JSDocInfo parseInlineTypeDoc(Comment node) { String comment = node.value; int lineno = lineno(node.location.start); int charno = charno(node.location.start); // The JsDocInfoParser expects the comment without the initial '/**'. int numOpeningChars = 3; JsDocInfoParser parser = new JsDocInfoParser( new JsDocTokenStream( comment.substring(numOpeningChars), lineno, charno + numOpeningChars), comment, node.location.start.offset, templateNode, config, jsDocSourceKind, errorReporter); return parser.parseInlineTypeDoc(); } // Set the length on the node if we're in IDE mode. void setLength(Node node, SourcePosition start, SourcePosition end) { node.setLength(end.offset - start.offset); } void setLengthFrom(Node node, Node ref) { node.setLength(ref.getLength()); } private static final QualifiedName GOOG_MODULE = QualifiedName.of("goog.module"); private class TransformDispatcher { /** * Transforms the given object key `input` into a Node with token `output`. * *

Depending on `input`, this may add quoting, such as for numerical values. For example, in * `{2: null}`, `2` will be transformed into a quoted string. This adjustment loses some * accuracy about the source code, but simplifies the AST. */ private Node processObjectLitKey( com.google.javascript.jscomp.parsing.parser.Token input, Token output) { if (input == null) { return createMissingExpressionNode(); } if (input.type == TokenType.IDENTIFIER) { return processName(input.asIdentifier(), output); } LiteralToken literal = input.asLiteral(); JSDocInfo jsDocInfo = parseJSDocInfoOnToken(literal); NonJSDocComment comment = parseNonJSDocCommentAt(literal.getStart(), true); final Node node; switch (input.type) { case NUMBER: node = newStringNode(output, DToA.numberToString(normalizeNumber(literal))); break; case BIGINT: node = newStringNode(output, normalizeBigInt(literal).toString()); break; default: node = newStringNode(output, normalizeString(literal, false)); break; } if (jsDocInfo != null) { node.setJSDocInfo(jsDocInfo); } if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, literal); node.putBooleanProp(Node.QUOTED_PROP, true); return node; } Node processComprehension(ComprehensionTree tree) { return unsupportedLanguageFeature(tree, "array/generator comprehensions"); } Node processComprehensionFor(ComprehensionForTree tree) { return unsupportedLanguageFeature(tree, "array/generator comprehensions"); } Node processComprehensionIf(ComprehensionIfTree tree) { return unsupportedLanguageFeature(tree, "array/generator comprehensions"); } Node processArrayLiteral(ArrayLiteralExpressionTree tree) { Node node = newNode(Token.ARRAYLIT); node.setTrailingComma(tree.hasTrailingComma); for (ParseTree child : tree.elements) { Node c = transform(child); node.addChildToBack(c); } return node; } Node processArrayPattern(ArrayPatternTree tree) { maybeWarnForFeature(tree, Feature.ARRAY_DESTRUCTURING); Node node = newNode(Token.ARRAY_PATTERN); for (ParseTree child : tree.elements) { final Node elementNode; switch (child.type) { case DEFAULT_PARAMETER: // processDefaultParameter() knows how to find and apply inline JSDoc to the right node elementNode = processDefaultParameter(child.asDefaultParameter()); break; case ITER_REST: maybeWarnForFeature(child, Feature.ARRAY_PATTERN_REST); elementNode = transformNodeWithInlineComments(child); break; default: elementNode = transformNodeWithInlineComments(child); break; } node.addChildToBack(elementNode); } return node; } Node processObjectPattern(ObjectPatternTree tree) { maybeWarnForFeature(tree, Feature.OBJECT_DESTRUCTURING); Node node = newNode(Token.OBJECT_PATTERN); for (ParseTree child : tree.fields) { Node childNode = processObjectPatternElement(child); node.addChildToBack(childNode); } return node; } private Node processObjectPatternElement(ParseTree child) { switch (child.type) { case DEFAULT_PARAMETER: // shorthand with a default value // let { /** inlineType */ name = default } = something; return processObjectPatternShorthandWithDefault(child.asDefaultParameter()); case PROPERTY_NAME_ASSIGNMENT: return processObjectPatternPropertyNameAssignment(child.asPropertyNameAssignment()); case COMPUTED_PROPERTY_DEFINITION: // let {[expression]: /** inlineType */ name} = something; ComputedPropertyDefinitionTree computedPropertyDefinition = child.asComputedPropertyDefinition(); return processObjectPatternComputedPropertyDefinition(computedPropertyDefinition); case OBJECT_REST: // let {...restObject} = someObject; maybeWarnForFeature(child, Feature.OBJECT_PATTERN_REST); Node target = transformNodeWithInlineComments(child.asObjectRest().assignmentTarget); Node rest = newNode(Token.OBJECT_REST, target); setSourceInfo(rest, child); return rest; default: throw new IllegalStateException("Unexpected object pattern element: " + child); } } /** * Process an object pattern element using shorthand and a default value. * *

e.g. the `/** inlineType * / name = default` part of this code * *


     * let { /** inlineType * / name = default } = something;
     * 
*/ private Node processObjectPatternShorthandWithDefault(DefaultParameterTree defaultParameter) { Node defaultValueNode = processDefaultParameter(defaultParameter); // Store in AST as non-shorthand form & just note it was originally shorthand // {name: /**inlineType */ name = default } Node nameNode = defaultValueNode.getFirstChild(); Node stringKeyNode = newStringNodeWithNonJSDocComment( Token.STRING_KEY, nameNode.getString(), defaultParameter.getStart()); setSourceInfo(stringKeyNode, nameNode); stringKeyNode.setShorthandProperty(true); stringKeyNode.addChildToBack(defaultValueNode); return stringKeyNode; } /** * Processes property name assignments in an object pattern. * *

Covers these cases. * *


     *   let {name} = someObject;
     *   let {key: name} = someObject;
     *   let {key: name = something} = someObject;
     * 
*/ private Node processObjectPatternPropertyNameAssignment( PropertyNameAssignmentTree propertyNameAssignment) { Node key = processObjectLitKey(propertyNameAssignment.name, Token.STRING_KEY); ParseTree targetTree = propertyNameAssignment.value; final Node valueNode; if (targetTree == null) { // `let { /** inlineType */ key } = something;` // The key is also the target name. valueNode = processNameWithInlineComments(propertyNameAssignment.name.asIdentifier()); key.setShorthandProperty(true); } else { valueNode = processDestructuringElementTarget(targetTree); } key.addChildToFront(valueNode); return key; } private Node processDestructuringElementTarget(ParseTree targetTree) { final Node valueNode; if (targetTree.type == ParseTreeType.DEFAULT_PARAMETER) { // let {key: /** inlineType */ name = default} = something; // let [/** inlineType */ name = default] = something; // processDefaultParameter() knows how to apply the inline JSDoc, if any, to the right node valueNode = processDefaultParameter(targetTree.asDefaultParameter()); } else if (targetTree.type == ParseTreeType.IDENTIFIER_EXPRESSION) { // let {key: /** inlineType */ name} = something // let [/** inlineType */ name] = something // Allow inline JSDoc on the name, since we may well be declaring it here. valueNode = processNameWithInlineComments(targetTree.asIdentifierExpression()); } else { // ({prop: /** string */ ns.a.b} = someObject); // NOTE: CheckJSDoc will report an error for this case, since we want qualified names to be // declared with individual statements, like `/** @type {string} */ ns.a.b;` valueNode = transformNodeWithInlineComments(targetTree); } return valueNode; } /** * Processes a computed property in an object pattern. * *

Covers these cases: * *


     *   let {[expression]: name} = someObject;
     *   let {[expression]: name = defaultValue} = someObject;
     * 
*/ private Node processObjectPatternComputedPropertyDefinition( ComputedPropertyDefinitionTree computedPropertyDefinition) { maybeWarnForFeature(computedPropertyDefinition, Feature.COMPUTED_PROPERTIES); Node expressionNode = transform(computedPropertyDefinition.property); ParseTree valueTree = computedPropertyDefinition.value; Node valueNode = processDestructuringElementTarget(valueTree); Node computedPropertyNode = newNode(Token.COMPUTED_PROP, expressionNode, valueNode); setSourceInfo(computedPropertyNode, computedPropertyDefinition); return computedPropertyNode; } Node processAstRoot(ProgramTree rootNode) { Node scriptNode = newNode(Token.SCRIPT); for (ParseTree child : rootNode.sourceElements) { scriptNode.addChildToBack(transform(child)); } parseDirectives(scriptNode); boolean isGoogModule = isGoogModuleFile(scriptNode); if (isGoogModule || features.has(Feature.MODULES)) { Node moduleNode = newNode(Token.MODULE_BODY); setSourceInfo(moduleNode, rootNode); moduleNode.addChildrenToBack(scriptNode.removeChildren()); scriptNode.addChildToBack(moduleNode); if (isGoogModule) { scriptNode.putBooleanProp(Node.GOOG_MODULE, true); } else { scriptNode.putBooleanProp(Node.ES6_MODULE, true); } } return scriptNode; } private boolean isGoogModuleFile(Node scriptNode) { checkArgument(scriptNode.isScript()); if (!scriptNode.hasChildren()) { return false; } Node exprResult = scriptNode.getFirstChild(); if (!exprResult.isExprResult()) { return false; } Node call = exprResult.getFirstChild(); if (!call.isCall()) { return false; } return GOOG_MODULE.matches(call.getFirstChild()) && call.hasTwoChildren() && call.getSecondChild().isStringLit(); } /** * Parse the directives, encode them in the AST, and remove their nodes. * *

The only suported directive is "use strict". * *

For information on ES5 directives, see section 14.1 of ECMA-262, Edition 5. */ private void parseDirectives(Node node) { boolean useStrict = false; while (true) { Node statement = node.getFirstChild(); if (statement == null || !statement.isExprResult()) { break; } Node directive = statement.getFirstChild(); if (!directive.isStringLit() || !directive.getString().equals("use strict")) { break; } useStrict = true; // TODO(nickreid): Should we also remove other directives that aren't "use strict"? statement.detach(); } if (useStrict) { node.setUseStrict(true); } } Node processBlock(BlockTree blockNode) { Node node = newNode(Token.BLOCK); for (ParseTree child : blockNode.statements) { Node childNode = transform(child); node.addChildToBack(childNode); attachPossibleTrailingComment(childNode, child.getEnd()); } NonJSDocComment lastComment = parseNonJSDocCommentAt(blockNode.getEnd(), false); return addExtraTrailingComment(node, lastComment); } Node processBreakStatement(BreakStatementTree statementNode) { Node node = newNode(Token.BREAK); if (statementNode.getLabel() != null) { Node labelName = transformLabelName(statementNode.name); node.addChildToBack(labelName); } return node; } Node transformLabelName(IdentifierToken token) { Node label = newStringNodeWithNonJSDocComment(Token.LABEL_NAME, token.value, token.getStart()); setSourceInfo(label, token); return label; } Node processConditionalExpression(ConditionalExpressionTree exprNode) { return newNode( Token.HOOK, transform(exprNode.condition), transform(exprNode.left), transform(exprNode.right)); } Node processContinueStatement(ContinueStatementTree statementNode) { Node node = newNode(Token.CONTINUE); if (statementNode.getLabel() != null) { Node labelName = transformLabelName(statementNode.name); node.addChildToBack(labelName); } return node; } Node processDoLoop(DoWhileStatementTree loopNode) { return newNode(Token.DO, transformBlock(loopNode.body), transform(loopNode.condition)); } Node processElementGet(MemberLookupExpressionTree getNode) { return newNode( Token.GETELEM, transform(getNode.operand), transform(getNode.memberExpression)); } Node processOptChainElementGet(OptionalMemberLookupExpressionTree getNode) { maybeWarnForFeature(getNode, Feature.OPTIONAL_CHAINING); Node getElem = newNode( Token.OPTCHAIN_GETELEM, transform(getNode.operand), transform(getNode.memberExpression)); getElem.setIsOptionalChainStart(getNode.isStartOfOptionalChain); return getElem; } /** * @param exprNode unused */ Node processEmptyStatement(EmptyStatementTree exprNode) { return newNode(Token.EMPTY); } Node processExpressionStatement(ExpressionStatementTree statementNode) { Node node = newNode(Token.EXPR_RESULT); node.addChildToBack(transform(statementNode.expression)); return node; } Node processForInLoop(ForInStatementTree loopNode) { // TODO(bradfordcsmith): Rename initializer to something more intuitive like "lhs" Node initializer = transform(loopNode.initializer); return newNode( Token.FOR_IN, initializer, transform(loopNode.collection), transformBlock(loopNode.body)); } Node processForOf(ForOfStatementTree loopNode) { maybeWarnForFeature(loopNode, Feature.FOR_OF); Node initializer = transform(loopNode.initializer); return newNode( Token.FOR_OF, initializer, transform(loopNode.collection), transformBlock(loopNode.body)); } Node processForAwaitOf(ForAwaitOfStatementTree loopNode) { maybeWarnForFeature(loopNode, Feature.FOR_AWAIT_OF); Node initializer = transform(loopNode.initializer); return newNode( Token.FOR_AWAIT_OF, initializer, transform(loopNode.collection), transformBlock(loopNode.body)); } Node processForLoop(ForStatementTree loopNode) { Node node = newNode( Token.FOR, transformOrEmpty(loopNode.initializer, loopNode), transformOrEmpty(loopNode.condition, loopNode), transformOrEmpty(loopNode.increment, loopNode)); node.addChildToBack(transformBlock(loopNode.body)); return node; } Node transformOrEmpty(ParseTree tree, ParseTree parent) { if (tree == null) { Node n = newNode(Token.EMPTY); setSourceInfo(n, parent); return n; } return transform(tree); } Node transformOrEmpty(IdentifierToken token, ParseTree parent) { if (token == null) { Node n = newNode(Token.EMPTY); setSourceInfo(n, parent); return n; } return processName(token, Token.NAME); } Node processFunctionCall(CallExpressionTree callNode) { Node node = newNode(Token.CALL, transform(callNode.operand)); node.setTrailingComma(callNode.arguments.hasTrailingComma); ArgumentListTree argumentsTree = callNode.arguments; // For each arg, represents a location (end SourcePosition) such that all // trailing comments before this location correspond to that arg. List zones = getEndOfArgCommentZones( argumentsTree.arguments, argumentsTree.commaPositions, argumentsTree.location.end); int argCount = 0; for (ParseTree child : callNode.arguments.arguments) { Node childNode = transform(child); node.addChildToBack(childNode); // The non-trailing comments are already attached to `childNode` in `transform(child)` // call. Now we must attach possible trailing comments to `childNode`. attachPossibleTrailingCommentsForArg(childNode, zones.get(argCount)); } annotateCalls(node); return node; } /** * There are two types of calls we are interested in calls without explicit "this" values (what * we are call "free" calls) and direct call to eval. */ private void annotateCalls(Node n) { checkState(n.isCall() || n.isOptChainCall() || n.isTaggedTemplateLit(), n); // Keep track of of the "this" context of a call. A call without an // explicit "this" is a free call. Node callee = n.getFirstChild(); // ignore cast nodes. while (callee.isCast()) { callee = callee.getFirstChild(); } if (!isNormalOrOptChainGet(callee)) { // This call originally was not passed a `this` value. // Inlining could change the callee into a property reference of some kind. // The code printer will recognize the `FREE_CALL` property and wrap the real callee with // `(0, real.callee)(args)` when necessary to avoid changing the calling behavior. n.putBooleanProp(Node.FREE_CALL, true); if (callee.isName() && "eval".equals(callee.getString())) { // Keep track of the context in which eval is called. It is important // to distinguish between "(0, eval)()" and "eval()". callee.putBooleanProp(Node.DIRECT_EVAL, true); } else if (callee.isComma() && callee.getFirstChild().isNumber()) { // The input code may actually already contain calls of the form // `(0, real.callee)(arg1, arg2)`. For example, the TypeScript compiler outputs code like // this in some cases. // This makes it hard for us to connect calls with the called functions, though, so we // will simplify these cases. The `FREE_CALL` property we've just applied will tell the // code printer to put this wrapping back later. final Node realCallee = callee.getSecondChild(); callee.replaceWith(realCallee.detach()); // TODO(bradfordcsmith): Why do I get an NPE if I try to report this code change? } } } /** Is this a GETPROP, OPTCHAIN_GETPROP, GETELEM, or OPTCHAIN_GETELEM? */ public boolean isNormalOrOptChainGet(Node n) { return n.isGetProp() || n.isGetElem() || n.isOptChainGetProp() || n.isOptChainGetElem(); } /** * Calculates, for each arg, a location (end SourcePosition) such that all trailing comments * before this location correspond to that arg. Can be used while processing both function calls * (ArgsList) as well as declarations (ParamList). * * @param args list of arguments or formal parameters (ParseTree nodes) * @param commaPositions list of SourcePositions corresponding to commas in the argsList or * formal parameter list * @param argListEndPosition SourcePosition of the end of argsList or paramList */ List getEndOfArgCommentZones( ImmutableList args, ImmutableList commaPositions, SourcePosition argListEndPosition) { ImmutableList.Builder zones = ImmutableList.builder(); int commaCount = 0; for (ParseTree arg : args) { if (args.size() > commaCount + 1) { // there is a next arg after this arg ParseTree nextParam = args.get(commaCount + 1); if (nextParam.location.start.line > arg.location.end.line) { // Next arg is on a new line; all trailing comments on this line belong to this arg // create a source position to represent the end of current line SourcePosition tempSourcePos = new SourcePosition( null, /* offset= */ Integer.MAX_VALUE, arg.location.end.line, Integer.MAX_VALUE /*col */); zones.add(tempSourcePos); } else { // Next arg is on the same line; trailing comments before the comma belong to this arg SourcePosition commaPosition = commaPositions.get(commaCount); zones.add(commaPosition); } } else { // last arg; trailing comments till the end of argList belong to this arg zones.add(argListEndPosition); } commaCount++; } return zones.build(); } /** * Attaches trailing comments associated with this arg or formal param to it. * * @param paramNode The node to which we're attaching trailing comment * @param endZone The end location until which we fetch pending comments for attachment */ void attachPossibleTrailingCommentsForArg(Node paramNode, SourcePosition endZone) { NonJSDocComment trailingComment = parseNonJSDocCommentAt(endZone, true); if (trailingComment == null) { return; } paramNode.setTrailingNonJSDocComment(trailingComment); } /** * Attaches trailing comments associated with this node. * * @param node The node to which we're attaching trailing comment * @param tokenEnd The end location after which we're looking for a trailing comment */ void attachPossibleTrailingComment(Node node, SourcePosition tokenEnd) { // Check for pending comments on the rest of the current line. SourcePosition nextLine = new SourcePosition(null, Integer.MAX_VALUE, tokenEnd.line + 1, 0); NonJSDocComment trailingComment = parseTrailingNonJSDocCommentAt(tokenEnd, nextLine); if (trailingComment == null) { return; } node.setTrailingNonJSDocComment(trailingComment); } Node addExtraTrailingComment(Node node, NonJSDocComment lastComment) { if (lastComment == null) { return node; } if (!node.hasChildren()) { node.setTrailingNonJSDocComment(lastComment); return node; } Node lastChild = node.getLastChild(); NonJSDocComment currentComment = lastChild.getTrailingNonJSDocComment(); if (currentComment == null) { // We add a line break here as this is cannot be a trailing comment on the same line as the // last child. // TODO(user): pass in proper child tree end position to get the number of line breaks // right. SourcePosition newStart = new SourcePosition( /* file */ null, lastComment.getEndPosition().offset - 1, lastComment.getEndPosition().line - 1, 0); NonJSDocComment newlineComment = new NonJSDocComment( newStart, lastComment.getEndPosition(), "\n" + lastComment.getCommentString()); node.getLastChild().setTrailingNonJSDocComment(newlineComment); return node; } int blankLines = lastComment.getStartPosition().line - currentComment.getEndPosition().line; int numWhiteSpace = 0; if (blankLines == 0) { numWhiteSpace = lastComment.getStartPosition().column - currentComment.getEndPosition().column - 1; } StringBuilder comment = new StringBuilder().append(currentComment.getCommentString()); for (int i = 0; i < numWhiteSpace; i++) { comment.append(" "); } for (int i = 0; i < blankLines; i++) { comment.append("\n"); } comment.append(lastComment.getCommentString()); NonJSDocComment allComments = new NonJSDocComment( currentComment.getStartPosition(), lastComment.getEndPosition(), comment.toString()); allComments.setEndsAsLineComment(lastComment.isEndingAsLineComment()); allComments.setIsInline(true); node.getLastChild().setTrailingNonJSDocComment(allComments); return node; } Node processOptChainFunctionCall(OptChainCallExpressionTree callNode) { maybeWarnForFeature(callNode, Feature.OPTIONAL_CHAINING); Node node = newNode(Token.OPTCHAIN_CALL, transform(callNode.operand)); node.setTrailingComma(callNode.hasTrailingComma); for (ParseTree child : callNode.arguments.arguments) { node.addChildToBack(transform(child)); } node.setIsOptionalChainStart(callNode.isStartOfOptionalChain); annotateCalls(node); return node; } Node processFunction(FunctionDeclarationTree functionTree) { boolean isDeclaration = (functionTree.kind == FunctionDeclarationTree.Kind.DECLARATION); boolean isMember = (functionTree.kind == FunctionDeclarationTree.Kind.MEMBER); boolean isArrow = (functionTree.kind == FunctionDeclarationTree.Kind.ARROW); boolean isAsync = functionTree.isAsync; boolean isGenerator = functionTree.isGenerator; boolean isSignature = (functionTree.functionBody.type == ParseTreeType.EMPTY_STATEMENT); if (isGenerator) { maybeWarnForFeature(functionTree, Feature.GENERATORS); } if (isMember) { maybeWarnForFeature(functionTree, Feature.MEMBER_DECLARATIONS); } if (isArrow) { maybeWarnForFeature(functionTree, Feature.ARROW_FUNCTIONS); } if (isAsync) { maybeWarnForFeature(functionTree, Feature.ASYNC_FUNCTIONS); } if (isGenerator && isAsync) { maybeWarnForFeature(functionTree, Feature.ASYNC_GENERATORS); } IdentifierToken name = functionTree.name; Node newName; if (name != null) { newName = processNameWithInlineComments(name); } else { if (isDeclaration || isMember) { errorReporter.error( "unnamed function statement", sourceName, lineno(functionTree), charno(functionTree)); // Return the bare minimum to put the AST in a valid state. newName = createMissingNameNode(); } else { newName = newStringNode(Token.NAME, ""); } // Old Rhino tagged the empty name node with the line number of the // declaration. setSourceInfo(newName, functionTree); } Node node = newNode(Token.FUNCTION); if (isMember) { newName.setString(""); } node.addChildToBack(newName); node.addChildToBack(transform(functionTree.formalParameterList)); Node bodyNode = transform(functionTree.functionBody); if (!isArrow && !isSignature && !bodyNode.isBlock()) { // When in "keep going" mode the parser tries to parse some constructs the // compiler doesn't support, repair it here. checkState(config.runMode() == Config.RunMode.KEEP_GOING); bodyNode = IR.block(); } parseDirectives(bodyNode); node.addChildToBack(bodyNode); node.setIsGeneratorFunction(isGenerator); node.setIsArrowFunction(isArrow); node.setIsAsyncFunction(isAsync); node.putBooleanProp(Node.OPT_ES6_TYPED, functionTree.isOptional); attachPossibleTrailingComment(node, functionTree.getEnd()); Node result; if (isMember) { setSourceInfo(node, functionTree); Node member = newStringNode(Token.MEMBER_FUNCTION_DEF, name.value); member.addChildToBack(node); member.setStaticMember(functionTree.isStatic); // The source info should only include the identifier, not the entire function expression setSourceInfo(member, name); result = member; } else { result = node; } return result; } // class fields: general case Node processField(FieldDeclarationTree tree) { maybeWarnForFeature(tree, Feature.PUBLIC_CLASS_FIELDS); Node node = newStringNodeWithNonJSDocComment( Token.MEMBER_FIELD_DEF, tree.name.value, tree.getStart()); if (tree.initializer != null) { Node initializer = transform(tree.initializer); node.addChildToBack(initializer); setLength(node, tree.location.start, tree.location.end); } node.putBooleanProp(Node.STATIC_MEMBER, tree.isStatic); return node; } // class fields: computed property fields Node processComputedPropertyField(ComputedPropertyFieldTree tree) { maybeWarnForFeature(tree, Feature.PUBLIC_CLASS_FIELDS); Node key = transform(tree.property); Node node = (tree.initializer != null) ? newNode(Token.COMPUTED_FIELD_DEF, key, transform(tree.initializer)) : newNode(Token.COMPUTED_FIELD_DEF, key); node.putBooleanProp(Node.STATIC_MEMBER, tree.isStatic); return node; } Node processFormalParameterList(FormalParameterListTree tree) { Node params = newNode(Token.PARAM_LIST); params.setTrailingComma(tree.hasTrailingComma); if (!checkParameters(tree)) { return params; } ImmutableList parameters = tree.parameters; List zones = getEndOfArgCommentZones(parameters, tree.commaPositions, tree.location.end); int argCount = 0; for (ParseTree param : tree.parameters) { final Node paramNode; switch (param.type) { case DEFAULT_PARAMETER: // processDefaultParameter() knows how to find and apply inline JSDoc to the right node paramNode = processDefaultParameter(param.asDefaultParameter()); break; case ITER_REST: maybeWarnForFeature(param, Feature.REST_PARAMETERS); paramNode = transformNodeWithInlineComments(param); break; default: paramNode = transformNodeWithInlineComments(param); // Reusing the logic to attach trailing comments used from call-site argsList attachPossibleTrailingCommentsForArg(paramNode, zones.get(argCount)); break; } // Children must be simple names, default parameters, rest // parameters, or destructuring patterns. checkState( paramNode.isName() || paramNode.isRest() || paramNode.isArrayPattern() || paramNode.isObjectPattern() || paramNode.isDefaultValue()); params.addChildToBack(paramNode); argCount++; } return params; } Node processDefaultParameter(DefaultParameterTree tree) { maybeWarnForFeature(tree, Feature.DEFAULT_PARAMETERS); ParseTree targetTree = tree.lhs; Node targetNode; if (targetTree.type == ParseTreeType.IDENTIFIER_EXPRESSION) { // allow inline JSDoc on an identifier // let { /** inlineType */ x = defaultValue } = someObject; // TODO(bradfordcsmith): Do we need to allow inline JSDoc for qualified names, too? targetNode = processNameWithInlineComments(targetTree.asIdentifierExpression()); } else { // ({prop: /** string */ ns.a.b = 'foo'} = someObject); // NOTE: CheckJSDoc will report an error for this case, since we want qualified names to be // declared with individual statements, like `/** @type {string} */ ns.a.b;` targetNode = transformNodeWithInlineComments(targetTree); } final Node defaultValueExpression = transform(tree.defaultValue); Node defaultValueNode = newNode(Token.DEFAULT_VALUE, targetNode, defaultValueExpression); reportErrorIfYieldOrAwaitInDefaultValue(defaultValueNode); setSourceInfo(defaultValueNode, tree); return defaultValueNode; } Node processIterRest(IterRestTree tree) { Node target = transformNodeWithInlineComments(tree.assignmentTarget); return newNode(Token.ITER_REST, target); } Node processIterSpread(IterSpreadTree tree) { maybeWarnForFeature(tree, Feature.SPREAD_EXPRESSIONS); return newNode(Token.ITER_SPREAD, transform(tree.expression)); } Node processObjectSpread(ObjectSpreadTree tree) { maybeWarnForFeature(tree, Feature.OBJECT_LITERALS_WITH_SPREAD); return newNode(Token.OBJECT_SPREAD, transform(tree.expression)); } Node processIfStatement(IfStatementTree statementNode) { Node node = newNode(Token.IF); node.addChildToBack(transform(statementNode.condition)); node.addChildToBack(transformBlock(statementNode.ifClause)); if (statementNode.elseClause != null) { node.addChildToBack(transformBlock(statementNode.elseClause)); } return node; } private void markBinaryExpressionFeatures(BinaryOperatorTree exprNode) { switch (exprNode.operator.type) { case STAR_STAR: case STAR_STAR_EQUAL: maybeWarnForFeature(exprNode, Feature.EXPONENT_OP); break; case QUESTION_QUESTION: maybeWarnForFeature(exprNode, Feature.NULL_COALESCE_OP); break; case QUESTION_QUESTION_EQUAL: maybeWarnForFeature(exprNode, Feature.NULL_COALESCE_OP); // fall through case OR_EQUAL: case AND_EQUAL: maybeWarnForFeature(exprNode, Feature.LOGICAL_ASSIGNMENT); break; default: break; } } Node processBinaryExpression(BinaryOperatorTree exprNode) { if (jsdocTracker.hasPendingCommentBefore(exprNode.right.location.start) || nonJsdocTracker.hasPendingCommentBefore(exprNode.right.location.start)) { markBinaryExpressionFeatures(exprNode); return newNode( transformBinaryTokenType(exprNode.operator.type), transform(exprNode.left), transform(exprNode.right)); } else { // No pending comments, we can traverse out of order. return processBinaryExpressionHelper(exprNode); } } // Deep binary ops (typical string concatentations) can cause stack overflows, // avoid recursing in this case and loop instead. private Node processBinaryExpressionHelper(BinaryOperatorTree exprTree) { Node root = null; Node current = null; Node previous = null; while (exprTree != null) { markBinaryExpressionFeatures(exprTree); previous = current; // Skip the first child but recurse normally into the right operand as typically this isn't // deep and because we have already checked that there isn't any JSDoc we can traverse // out of order. current = newNode(transformBinaryTokenType(exprTree.operator.type), transform(exprTree.right)); // We have inlined "transform" here. Normally, we would need to handle the JSDoc here but we // know there is no JSDoc to attach, which simplifies things. setSourceInfo(current, exprTree); // As the iteration continues add the left operand. if (previous != null) { previous.addChildToFront(current); } if (exprTree.left instanceof BinaryOperatorTree) { // continue with the left hand child exprTree = (BinaryOperatorTree) exprTree.left; } else { // Finish things off, add the left operand to the current node. Node leftNode = transform(exprTree.left); current.addChildToFront(leftNode); // Nothing left to do. exprTree = null; } // Save the top binary op, this is the result to return. if (root == null) { root = current; } } return root; } /** * @param node unused. */ Node processDebuggerStatement(DebuggerStatementTree node) { return newNode(Token.DEBUGGER); } /** * @param node unused. */ Node processThisExpression(ThisExpressionTree node) { return newNode(Token.THIS); } Node processLabeledStatement(LabelledStatementTree labelTree) { Node statement = transform(labelTree.statement); if (statement.isFunction() || statement.isClass() || statement.isLet() || statement.isConst()) { errorReporter.error( "Lexical declarations are only allowed at top level or inside a block.", sourceName, lineno(labelTree), charno(labelTree)); return statement; // drop the LABEL node so that the resulting AST is valid } return newNode(Token.LABEL, transformLabelName(labelTree.name), statement); } Node processName(IdentifierExpressionTree nameNode) { return processName(nameNode.identifierToken, Token.NAME); } Node processName(IdentifierToken identifierToken, Token output) { NonJSDocComment comment = parseNonJSDocCommentAt(identifierToken.getStart(), true); Node node = newStringNode(output, identifierToken.value); if (output == Token.NAME) { maybeWarnReservedKeyword(identifierToken); JSDocInfo info = parseJSDocInfoOnToken(identifierToken); if (info != null) { node.setJSDocInfo(info); } } if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, identifierToken); return node; } Node processString(LiteralToken token) { checkArgument(token.type == TokenType.STRING); NonJSDocComment comment = parseNonJSDocCommentAt(token.getStart(), true); Node node = newStringNode(Token.STRINGLIT, normalizeString(token, false)); if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, token); return node; } Node processTemplateLiteralToken(TemplateLiteralToken token) { checkArgument( token.type == TokenType.NO_SUBSTITUTION_TEMPLATE || token.type == TokenType.TEMPLATE_HEAD || token.type == TokenType.TEMPLATE_MIDDLE || token.type == TokenType.TEMPLATE_TAIL); Node node; if (token.hasError()) { node = newTemplateLitStringNode(null, token.value); } else { node = newTemplateLitStringNode(normalizeString(token, true), token.value); } setSourceInfo(node, token); return node; } private Node processNameWithInlineComments(IdentifierExpressionTree identifierExpression) { return processNameWithInlineComments(identifierExpression.identifierToken); } Node processNameWithInlineComments(IdentifierToken identifierToken) { JSDocInfo info = parseInlineJSDocAt(identifierToken.getStart()); NonJSDocComment comment = parseNonJSDocCommentAt(identifierToken.getStart(), false); maybeWarnReservedKeyword(identifierToken); Node node = newStringNode(Token.NAME, identifierToken.value); if (info != null) { node.setJSDocInfo(info); } if (comment != null) { node.setNonJSDocComment(comment); } setSourceInfo(node, identifierToken); return node; } private void maybeWarnKeywordProperty(Node node) { if (currentFileIsExterns) { return; } if (!TokenStream.isKeyword(node.getString())) { return; } features = features.with(Feature.KEYWORDS_AS_PROPERTIES); if (config.languageMode() == LanguageMode.ECMASCRIPT3) { errorReporter.warning( INVALID_ES3_PROP_NAME, sourceName, node.getLineno(), node.getCharno()); } } private void maybeWarnReservedKeyword(IdentifierToken token) { String identifier = token.value; boolean isIdentifier = false; if (TokenStream.isKeyword(identifier)) { features = features.with(Feature.ES3_KEYWORDS_AS_IDENTIFIERS); isIdentifier = config.languageMode() == LanguageMode.ECMASCRIPT3; } if (reservedKeywords != null && reservedKeywords.contains(identifier)) { features = features.with(Feature.KEYWORDS_AS_PROPERTIES); isIdentifier = config.languageMode() == LanguageMode.ECMASCRIPT3; } if (isIdentifier) { errorReporter.error( "identifier is a reserved word", sourceName, lineno(token.location.start), charno(token.location.start)); } } Node processNewExpression(NewExpressionTree exprNode) { Node node = newNode(Token.NEW, transform(exprNode.operand)); node.setTrailingComma(exprNode.hasTrailingComma); if (exprNode.arguments != null) { for (ParseTree arg : exprNode.arguments.arguments) { node.addChildToBack(transform(arg)); } } return node; } Node processNumberLiteral(LiteralExpressionTree literalNode) { double value = normalizeNumber(literalNode.literalToken.asLiteral()); Node number = newNumberNode(value); setSourceInfo(number, literalNode); return number; } Node processBigIntLiteral(LiteralExpressionTree literalNode) { maybeWarnForFeature(literalNode, Feature.BIGINT); BigInteger value = normalizeBigInt(literalNode.literalToken.asLiteral()); Node bigint = newBigIntNode(value); setSourceInfo(bigint, literalNode); return bigint; } Node processObjectLiteral(ObjectLiteralExpressionTree objTree) { Node node = newNode(Token.OBJECTLIT); node.setTrailingComma(objTree.hasTrailingComma); boolean maybeWarn = false; for (ParseTree el : objTree.propertyNameAndValues) { if (el.type == ParseTreeType.DEFAULT_PARAMETER) { // (e.g. var o = { x=4 };) This is only parsed for compatibility with object patterns. errorReporter.error( "Default value cannot appear at top level of an object literal.", sourceName, lineno(el), 0); continue; } else if (el.type == ParseTreeType.GET_ACCESSOR && maybeReportGetter(el)) { continue; } else if (el.type == ParseTreeType.SET_ACCESSOR && maybeReportSetter(el)) { continue; } Node key = transform(el); if (!key.isComputedProp() && !key.isQuotedStringKey() && !key.isSpread() && !currentFileIsExterns) { maybeWarnKeywordProperty(key); } if (key.isShorthandProperty()) { maybeWarn = true; } node.addChildToBack(key); } if (maybeWarn) { maybeWarnForFeature(objTree, Feature.EXTENDED_OBJECT_LITERALS); } return node; } Node processComputedPropertyDefinition(ComputedPropertyDefinitionTree tree) { maybeWarnForFeature(tree, Feature.COMPUTED_PROPERTIES); return newNode(Token.COMPUTED_PROP, transform(tree.property), transform(tree.value)); } Node processComputedPropertyMethod(ComputedPropertyMethodTree tree) { maybeWarnForFeature(tree, Feature.COMPUTED_PROPERTIES); Node n = newNode(Token.COMPUTED_PROP, transform(tree.property), transform(tree.method)); n.putBooleanProp(Node.COMPUTED_PROP_METHOD, true); if (tree.method.asFunctionDeclaration().isStatic) { n.setStaticMember(true); } return n; } Node processComputedPropertyGetter(ComputedPropertyGetterTree tree) { maybeWarnForFeature(tree, Feature.COMPUTED_PROPERTIES); Node key = transform(tree.property); Node body = transform(tree.body); Node function = IR.function(IR.name(""), IR.paramList(), body); function.srcrefTreeIfMissing(body); Node n = newNode(Token.COMPUTED_PROP, key, function); n.putBooleanProp(Node.COMPUTED_PROP_GETTER, true); n.putBooleanProp(Node.STATIC_MEMBER, tree.isStatic); return n; } Node processComputedPropertySetter(ComputedPropertySetterTree tree) { maybeWarnForFeature(tree, Feature.COMPUTED_PROPERTIES); Node key = transform(tree.property); Node paramList = processFormalParameterList(tree.parameter); setSourceInfo(paramList, tree.parameter); Node body = transform(tree.body); Node function = IR.function(IR.name(""), paramList, body); function.srcrefTreeIfMissing(body); Node n = newNode(Token.COMPUTED_PROP, key, function); n.putBooleanProp(Node.COMPUTED_PROP_SETTER, true); n.putBooleanProp(Node.STATIC_MEMBER, tree.isStatic); return n; } Node processGetAccessor(GetAccessorTree tree) { Node key = processObjectLitKey(tree.propertyName, Token.GETTER_DEF); Node body = transform(tree.body); Node dummyName = newStringNode(Token.NAME, ""); setSourceInfo(dummyName, tree.body); Node paramList = newNode(Token.PARAM_LIST); setSourceInfo(paramList, tree.body); Node value = newNode(Token.FUNCTION, dummyName, paramList, body); setSourceInfo(value, tree.body); key.addChildToFront(value); key.setStaticMember(tree.isStatic); return key; } Node processSetAccessor(SetAccessorTree tree) { Node key = processObjectLitKey(tree.propertyName, Token.SETTER_DEF); Node paramList = processFormalParameterList(tree.parameter); setSourceInfo(paramList, tree.parameter); Node body = transform(tree.body); Node dummyName = newStringNode(Token.NAME, ""); setSourceInfo(dummyName, tree.propertyName); Node value = newNode(Token.FUNCTION, dummyName, paramList, body); setSourceInfo(value, tree.body); key.addChildToFront(value); key.setStaticMember(tree.isStatic); return key; } Node processPropertyNameAssignment(PropertyNameAssignmentTree tree) { Node key = processObjectLitKey(tree.name, Token.STRING_KEY); if (tree.value != null) { key.addChildToFront(transform(tree.value)); } else { Node value = newStringNodeWithNonJSDocComment(Token.NAME, key.getString(), tree.name.getStart()) .srcref(key); key.setShorthandProperty(true); key.addChildToFront(value); } return key; } private void checkParenthesizedExpression(ParenExpressionTree exprNode) { if (exprNode.expression.type == ParseTreeType.COMMA_EXPRESSION) { ImmutableList commaNodes = exprNode.expression.asCommaExpression().expressions; ParseTree lastChild = Iterables.getLast(commaNodes); if (lastChild.type == ParseTreeType.ITER_REST) { errorReporter.error( "A rest parameter must be in a parameter list.", sourceName, lineno(lastChild), charno(lastChild)); } } } Node processParenthesizedExpression(ParenExpressionTree exprNode) { checkParenthesizedExpression(exprNode); Node expr = transform(exprNode.expression); expr.setIsParenthesized(true); return expr; } Node processPropertyGet(MemberExpressionTree getNode) { Node leftChild = transform(getNode.operand); IdentifierToken propName = getNode.memberName; if (propName == null) { return leftChild; } Node getProp = newStringNodeWithNonJSDocComment( Token.GETPROP, propName.value, getNode.memberName.getStart()); getProp.addChildToBack(leftChild); setSourceInfo(getProp, propName); maybeWarnKeywordProperty(getProp); return getProp; } Node processOptChainPropertyGet(OptionalMemberExpressionTree getNode) { maybeWarnForFeature(getNode, Feature.OPTIONAL_CHAINING); Node leftChild = transform(getNode.operand); IdentifierToken propName = getNode.memberName; if (propName == null) { return leftChild; } Node getProp = newStringNodeWithNonJSDocComment( Token.OPTCHAIN_GETPROP, propName.value, getNode.memberName.getStart()); getProp.addChildToBack(leftChild); getProp.setIsOptionalChainStart(getNode.isStartOfOptionalChain); setSourceInfo(getProp, propName); maybeWarnKeywordProperty(getProp); return getProp; } Node processRegExpLiteral(LiteralExpressionTree literalTree) { LiteralToken token = literalTree.literalToken.asLiteral(); Node literalStringNode = newStringNode(normalizeRegex(token)); // TODO(johnlenz): fix the source location. setSourceInfo(literalStringNode, token); Node node = newNode(Token.REGEXP, literalStringNode); String rawRegex = token.value; int lastSlash = rawRegex.lastIndexOf('/'); String flags = ""; if (lastSlash < rawRegex.length()) { flags = rawRegex.substring(lastSlash + 1); } validateRegExpFlags(literalTree, flags); if (!flags.isEmpty()) { Node flagsNode = newStringNode(flags); // TODO(johnlenz): fix the source location. setSourceInfo(flagsNode, token); node.addChildToBack(flagsNode); } return node; } private void validateRegExpFlags(LiteralExpressionTree tree, String flags) { for (char flag : Lists.charactersOf(flags)) { switch (flag) { case 'g': case 'i': case 'm': break; case 'u': case 'y': Feature feature = flag == 'u' ? Feature.REGEXP_FLAG_U : Feature.REGEXP_FLAG_Y; maybeWarnForFeature(tree, feature); break; case 's': maybeWarnForFeature(tree, Feature.REGEXP_FLAG_S); break; case 'd': maybeWarnForFeature(tree, Feature.REGEXP_FLAG_D); break; default: errorReporter.error( "Invalid RegExp flag '" + flag + "'", sourceName, lineno(tree), charno(tree)); } } } Node processReturnStatement(ReturnStatementTree statementNode) { Node node = newNode(Token.RETURN); if (statementNode.expression != null) { node.addChildToBack(transform(statementNode.expression)); } return node; } Node processStringLiteral(LiteralExpressionTree literalTree) { LiteralToken token = literalTree.literalToken.asLiteral(); return processString(token); } Node processTemplateLiteral(TemplateLiteralExpressionTree tree) { maybeWarnForFeature(tree, Feature.TEMPLATE_LITERALS); Node templateLitNode = newNode(Token.TEMPLATELIT); setSourceInfo(templateLitNode, tree); Node node = tree.operand == null ? templateLitNode : newNode(Token.TAGGED_TEMPLATELIT, transform(tree.operand), templateLitNode); for (ParseTree child : tree.elements) { templateLitNode.addChildToBack(transform(child)); } if (node.isTaggedTemplateLit()) { annotateCalls(node); } return node; } Node processTemplateLiteralPortion(TemplateLiteralPortionTree tree) { return processTemplateLiteralToken(tree.value.asTemplateLiteral()); } Node processTemplateSubstitution(TemplateSubstitutionTree tree) { return newNode(Token.TEMPLATELIT_SUB, transform(tree.expression)); } Node processSwitchCase(CaseClauseTree caseNode) { ParseTree expr = caseNode.expression; Node node = newNode(Token.CASE, transform(expr)); Node block = newNode(Token.BLOCK); block.setIsAddedBlock(true); setSourceInfo(block, caseNode); if (caseNode.statements != null) { for (ParseTree child : caseNode.statements) { block.addChildToBack(transform(child)); } } node.addChildToBack(block); return node; } Node processSwitchDefault(DefaultClauseTree caseNode) { Node node = newNode(Token.DEFAULT_CASE); Node block = newNode(Token.BLOCK); block.setIsAddedBlock(true); setSourceInfo(block, caseNode); if (caseNode.statements != null) { for (ParseTree child : caseNode.statements) { block.addChildToBack(transform(child)); } } node.addChildToBack(block); return node; } Node processSwitchStatement(SwitchStatementTree statementNode) { Node node = newNode(Token.SWITCH, transform(statementNode.expression)); for (ParseTree child : statementNode.caseClauses) { node.addChildToBack(transform(child)); } return node; } Node processThrowStatement(ThrowStatementTree statementNode) { return newNode(Token.THROW, transform(statementNode.value)); } Node processTryStatement(TryStatementTree statementNode) { Node node = newNode(Token.TRY, transformBlock(statementNode.body)); Node block = newNode(Token.BLOCK); boolean lineSet = false; ParseTree cc = statementNode.catchBlock; if (cc != null) { // Mark the enclosing block at the same line as the first catch // clause. setSourceInfo(block, cc); lineSet = true; block.addChildToBack(transform(cc)); } node.addChildToBack(block); ParseTree finallyBlock = statementNode.finallyBlock; if (finallyBlock != null) { node.addChildToBack(transformBlock(finallyBlock)); } // If we didn't set the line on the catch clause, then // we've got an empty catch clause. Set its line to be the same // as the finally block (to match Old Rhino's behavior.) if (!lineSet && (finallyBlock != null)) { setSourceInfo(block, finallyBlock); } return node; } Node processCatchClause(CatchTree clauseNode) { if (clauseNode.exception.type == ParseTreeType.EMPTY_STATEMENT) { maybeWarnForFeature(clauseNode, Feature.OPTIONAL_CATCH_BINDING); } return newNode( Token.CATCH, transform(clauseNode.exception), transformBlock(clauseNode.catchBody)); } Node processFinally(FinallyTree finallyNode) { return transformBlock(finallyNode.block); } Node processUnaryExpression(UnaryExpressionTree exprNode) { Token type = transformUnaryTokenType(exprNode.operator.type); Node operand = transform(exprNode.operand); switch (type) { case DELPROP: if (!(operand.isGetProp() || operand.isGetElem() || operand.isName() || operand.isOptChainGetProp() || operand.isOptChainGetElem())) { String msg = "Invalid delete operand. Only properties can be deleted."; errorReporter.error(msg, sourceName, operand.getLineno(), 0); } break; case POS: if (operand.isBigInt()) { errorReporter.error( "Cannot convert a BigInt value to a number", sourceName, operand.getLineno(), 0); } break; default: break; } return newNode(type, operand); } Node processUpdateExpression(UpdateExpressionTree updateExpr) { Token type = transformUpdateTokenType(updateExpr.operator.type); Node operand = transform(updateExpr.operand); return createUpdateNode( type, updateExpr.operatorPosition == OperatorPosition.POSTFIX, operand); } private Node createUpdateNode(Token type, boolean postfix, Node operand) { Node assignTarget = operand.isCast() ? operand.getFirstChild() : operand; if (!assignTarget.isValidAssignmentTarget()) { errorReporter.error( SimpleFormat.format( "Invalid %s %s operand.", (postfix ? "postfix" : "prefix"), (type == Token.INC ? "increment" : "decrement")), sourceName, operand.getLineno(), operand.getCharno()); } Node node = newNode(type, operand); node.putBooleanProp(Node.INCRDECR_PROP, postfix); return node; } Node processVariableStatement(VariableStatementTree stmt) { // TODO(moz): Figure out why we still need the special handling return transformDispatcher.process(stmt.declarations); } Node processVariableDeclarationList(VariableDeclarationListTree decl) { Token declType; switch (decl.declarationType) { case CONST: maybeWarnForFeature(decl, Feature.CONST_DECLARATIONS); declType = Token.CONST; break; case LET: maybeWarnForFeature(decl, Feature.LET_DECLARATIONS); declType = Token.LET; break; case VAR: declType = Token.VAR; break; default: throw new IllegalStateException(); } Node node = newNode(declType); for (VariableDeclarationTree child : decl.declarations) { node.addChildToBack(transformNodeWithInlineComments(child)); } attachPossibleTrailingComment(node, decl.getEnd()); return node; } Node processVariableDeclaration(VariableDeclarationTree decl) { Node node = transformNodeWithInlineComments(decl.lvalue); Node lhs = node.isDestructuringPattern() ? newNode(Token.DESTRUCTURING_LHS, node) : node; if (decl.initializer != null) { Node initializer = transform(decl.initializer); lhs.addChildToBack(initializer); setLength(lhs, decl.location.start, decl.location.end); } return lhs; } Node processWhileLoop(WhileStatementTree stmt) { return newNode(Token.WHILE, transform(stmt.condition), transformBlock(stmt.body)); } Node processWithStatement(WithStatementTree stmt) { return newNode(Token.WITH, transform(stmt.expression), transformBlock(stmt.body)); } /** * @param tree unused */ Node processMissingExpression(MissingPrimaryExpressionTree tree) { // This will already have been reported as an error by the parser. // Try to create something valid that ide mode might be able to // continue with. return createMissingExpressionNode(); } private Node createMissingNameNode() { return newStringNode(Token.NAME, "__missing_name__"); } private Node createMissingExpressionNode() { return newStringNode(Token.NAME, "__missing_expression__"); } Node processIllegalToken(ParseTree node) { errorReporter.error("Unsupported syntax: " + node.type, sourceName, lineno(node), 0); return newNode(Token.EMPTY); } /** Reports an illegal getter and returns true if the language mode is too low. */ boolean maybeReportGetter(ParseTree node) { features = features.with(Feature.GETTER); if (config.languageMode() == LanguageMode.ECMASCRIPT3) { errorReporter.error(GETTER_ERROR_MESSAGE, sourceName, lineno(node), 0); return true; } return false; } /** Reports an illegal setter and returns true if the language mode is too low. */ boolean maybeReportSetter(ParseTree node) { features = features.with(Feature.SETTER); if (config.languageMode() == LanguageMode.ECMASCRIPT3) { errorReporter.error(SETTER_ERROR_MESSAGE, sourceName, lineno(node), 0); return true; } return false; } Node processBooleanLiteral(LiteralExpressionTree literal) { return newNode(transformBooleanTokenType(literal.literalToken.type)); } /** * @param literal unused */ Node processNullLiteral(LiteralExpressionTree literal) { return newNode(Token.NULL); } /** * @param literal unused */ Node processNull(NullTree literal) { // NOTE: This is not a NULL literal but a placeholder node such as in // an array with "holes". return newNode(Token.EMPTY); } Node processCommaExpression(CommaExpressionTree tree) { Node root = newNode(Token.COMMA); SourcePosition start = tree.expressions.get(0).location.start; SourcePosition end = tree.expressions.get(1).location.end; setSourceInfo(root, start, end); for (ParseTree expr : tree.expressions) { int count = root.getChildCount(); if (count < 2) { root.addChildToBack(transform(expr)); } else { end = expr.location.end; root = newNode(Token.COMMA, root, transform(expr)); setSourceInfo(root, start, end); } } return root; } Node processClassDeclaration(ClassDeclarationTree tree) { maybeWarnForFeature(tree, Feature.CLASSES); Node name = transformOrEmpty(tree.name, tree); Node superClass = transformOrEmpty(tree.superClass, tree); if (!superClass.isEmpty()) { features = features.with(Feature.CLASS_EXTENDS); } Node body = newNode(Token.CLASS_MEMBERS); setSourceInfo(body, tree); boolean hasConstructor = false; for (ParseTree child : tree.elements) { switch (child.type) { case COMPUTED_PROPERTY_GETTER: case COMPUTED_PROPERTY_SETTER: case GET_ACCESSOR: case SET_ACCESSOR: features = features.with(Feature.CLASS_GETTER_SETTER); break; case BLOCK: features = features.with(Feature.CLASS_STATIC_BLOCK); break; default: break; } boolean childIsCtor = validateClassConstructorMember(child); // Has side-effects. if (childIsCtor) { if (hasConstructor) { errorReporter.error( "Class may have only one constructor.", // sourceName, lineno(child), charno(child)); } hasConstructor = true; } body.addChildToBack(transform(child)); } Node classNode = newNode(Token.CLASS, name, superClass, body); attachPossibleTrailingComment(classNode, tree.getEnd()); return classNode; } /** Returns {@code true} iff this member is a legal class constructor. */ private boolean validateClassConstructorMember(ParseTree member) { final com.google.javascript.jscomp.parsing.parser.Token memberName; final boolean isStatic; final boolean hasIllegalModifier; switch (member.type) { case GET_ACCESSOR: GetAccessorTree getter = member.asGetAccessor(); memberName = getter.propertyName; isStatic = getter.isStatic; hasIllegalModifier = true; break; case SET_ACCESSOR: SetAccessorTree setter = member.asSetAccessor(); memberName = setter.propertyName; isStatic = setter.isStatic; hasIllegalModifier = true; break; case FUNCTION_DECLARATION: FunctionDeclarationTree method = member.asFunctionDeclaration(); memberName = method.name; isStatic = method.isStatic; hasIllegalModifier = method.isGenerator || method.isAsync; break; default: // Computed properties aren't an issue here because they aren't used as the class // constructor, regardless of their name. return false; } if (isStatic) { // Statics are fine because they're never the class constructor. return false; } if (!memberName.type.equals(TokenType.IDENTIFIER) || !memberName.asIdentifier().value.equals("constructor")) { // There's only a potential issue if the member is named "constructor". // TODO(b/123769080): Also check for quoted string literals with the value "constructor". return false; } if (hasIllegalModifier) { errorReporter.error( "Class constructor may not be getter, setter, async, or generator.", sourceName, lineno(member), charno(member)); return false; } return true; } Node processSuper(SuperExpressionTree tree) { maybeWarnForFeature(tree, Feature.SUPER); return newNode(Token.SUPER); } Node processNewTarget(NewTargetExpressionTree tree) { maybeWarnForFeature(tree, Feature.NEW_TARGET); return newNode(Token.NEW_TARGET); } Node processYield(YieldExpressionTree tree) { Node yield = newNode(Token.YIELD); if (tree.expression != null) { yield.addChildToBack(transform(tree.expression)); } yield.setYieldAll(tree.isYieldAll); return yield; } Node processAwait(AwaitExpressionTree tree) { maybeWarnForFeature(tree, Feature.ASYNC_FUNCTIONS); Node await = newNode(Token.AWAIT); await.addChildToBack(transform(tree.expression)); return await; } Node processExportDecl(ExportDeclarationTree tree) { maybeWarnForFeature(tree, Feature.MODULES); Node decls = null; if (tree.isExportAll) { checkState(tree.declaration == null && tree.exportSpecifierList == null); } else if (tree.declaration != null) { checkState(tree.exportSpecifierList == null); decls = transform(tree.declaration); } else { decls = transformList(Token.EXPORT_SPECS, tree.exportSpecifierList); } if (decls == null) { decls = newNode(Token.EMPTY); } setSourceInfo(decls, tree); Node export = newNode(Token.EXPORT, decls); if (tree.from != null) { Node from = processString(tree.from); export.addChildToBack(from); } export.putBooleanProp(Node.EXPORT_ALL_FROM, tree.isExportAll); export.putBooleanProp(Node.EXPORT_DEFAULT, tree.isDefault); return export; } Node processExportSpec(ExportSpecifierTree tree) { Node importedName = processName(tree.importedName, Token.NAME); Node exportSpec = newNode(Token.EXPORT_SPEC, importedName); if (tree.destinationName == null) { exportSpec.setShorthandProperty(true); exportSpec.addChildToBack(importedName.cloneTree()); } else { Node destinationName = processName(tree.destinationName, Token.NAME); exportSpec.addChildToBack(destinationName); } return exportSpec; } Node processImportDecl(ImportDeclarationTree tree) { maybeWarnForFeature(tree, Feature.MODULES); Node firstChild = transformOrEmpty(tree.defaultBindingIdentifier, tree); Node secondChild; if (tree.nameSpaceImportIdentifier == null) { secondChild = transformListOrEmpty(Token.IMPORT_SPECS, tree.importSpecifierList); // Currently source info is "import {foo} from '...';" expression. If needed this should be // changed to use only "{foo}" part. setSourceInfo(secondChild, tree); } else { secondChild = newStringNode(Token.IMPORT_STAR, tree.nameSpaceImportIdentifier.value); setSourceInfo(secondChild, tree.nameSpaceImportIdentifier); } Node thirdChild = processString(tree.moduleSpecifier); return newNode(Token.IMPORT, firstChild, secondChild, thirdChild); } Node processImportSpec(ImportSpecifierTree tree) { Node importedName = processName(tree.importedName, Token.NAME); Node importSpec = newNode(Token.IMPORT_SPEC, importedName); if (tree.destinationName == null) { importSpec.setShorthandProperty(true); importSpec.addChildToBack(importedName.cloneTree()); } else { importSpec.addChildToBack(processName(tree.destinationName, Token.NAME)); } return importSpec; } Node processDynamicImport(DynamicImportTree dynamicImportNode) { maybeWarnForFeature(dynamicImportNode, Feature.DYNAMIC_IMPORT); Node argument = transform(dynamicImportNode.argument); return newNode(Token.DYNAMIC_IMPORT, argument); } Node processImportMeta(ImportMetaExpressionTree tree) { maybeWarnForFeature(tree, Feature.MODULES); maybeWarnForFeature(tree, Feature.IMPORT_META); return newNode(Token.IMPORT_META); } private boolean checkParameters(FormalParameterListTree tree) { ImmutableList params = tree.parameters; boolean good = true; for (int i = 0; i < params.size(); i++) { ParseTree param = params.get(i); if (param.type == ParseTreeType.ITER_REST) { if (i != params.size() - 1) { errorReporter.error( "A rest parameter must be last in a parameter list.", sourceName, lineno(param), charno(param)); good = false; } else if (tree.hasTrailingComma) { errorReporter.error( "A trailing comma must not follow a rest parameter.", sourceName, lineno(param), charno(param)); good = false; } } } return good; } private Node transformList(Token type, ImmutableList list) { Node n = newNode(type); for (ParseTree tree : list) { n.addChildToBack(transform(tree)); } return n; } private Node transformListOrEmpty(Token type, ImmutableList list) { if (list == null || list.isEmpty()) { return newNode(Token.EMPTY); } else { return transformList(type, list); } } Node unsupportedLanguageFeature(ParseTree node, String feature) { errorReporter.error( "unsupported language feature: " + feature, sourceName, lineno(node), charno(node)); return createMissingExpressionNode(); } Node processLiteralExpression(LiteralExpressionTree expr) { switch (expr.literalToken.type) { case NUMBER: return processNumberLiteral(expr); case STRING: return processStringLiteral(expr); case BIGINT: return processBigIntLiteral(expr); case FALSE: case TRUE: return processBooleanLiteral(expr); case NULL: return processNullLiteral(expr); case REGULAR_EXPRESSION: return processRegExpLiteral(expr); default: throw new IllegalStateException( "Unexpected literal type: " + expr.literalToken.getClass() + " type: " + expr.literalToken.type); } } public Node process(ParseTree node) { switch (node.type) { case BINARY_OPERATOR: return processBinaryExpression(node.asBinaryOperator()); case ARRAY_LITERAL_EXPRESSION: return processArrayLiteral(node.asArrayLiteralExpression()); case TEMPLATE_LITERAL_EXPRESSION: return processTemplateLiteral(node.asTemplateLiteralExpression()); case TEMPLATE_LITERAL_PORTION: return processTemplateLiteralPortion(node.asTemplateLiteralPortion()); case TEMPLATE_SUBSTITUTION: return processTemplateSubstitution(node.asTemplateSubstitution()); case UNARY_EXPRESSION: return processUnaryExpression(node.asUnaryExpression()); case BLOCK: return processBlock(node.asBlock()); case BREAK_STATEMENT: return processBreakStatement(node.asBreakStatement()); case CALL_EXPRESSION: return processFunctionCall(node.asCallExpression()); case OPT_CHAIN_CALL_EXPRESSION: return processOptChainFunctionCall(node.asOptChainCallExpression()); case CASE_CLAUSE: return processSwitchCase(node.asCaseClause()); case DEFAULT_CLAUSE: return processSwitchDefault(node.asDefaultClause()); case CATCH: return processCatchClause(node.asCatch()); case CONTINUE_STATEMENT: return processContinueStatement(node.asContinueStatement()); case DO_WHILE_STATEMENT: return processDoLoop(node.asDoWhileStatement()); case EMPTY_STATEMENT: return processEmptyStatement(node.asEmptyStatement()); case EXPRESSION_STATEMENT: return processExpressionStatement(node.asExpressionStatement()); case DEBUGGER_STATEMENT: return processDebuggerStatement(node.asDebuggerStatement()); case THIS_EXPRESSION: return processThisExpression(node.asThisExpression()); case FOR_STATEMENT: return processForLoop(node.asForStatement()); case FOR_IN_STATEMENT: return processForInLoop(node.asForInStatement()); case FUNCTION_DECLARATION: return processFunction(node.asFunctionDeclaration()); case MEMBER_LOOKUP_EXPRESSION: return processElementGet(node.asMemberLookupExpression()); case OPT_CHAIN_MEMBER_LOOKUP_EXPRESSION: return processOptChainElementGet(node.asOptionalMemberLookupExpression()); case MEMBER_EXPRESSION: return processPropertyGet(node.asMemberExpression()); case OPT_CHAIN_MEMBER_EXPRESSION: return processOptChainPropertyGet(node.asOptionalMemberExpression()); case CONDITIONAL_EXPRESSION: return processConditionalExpression(node.asConditionalExpression()); case IF_STATEMENT: return processIfStatement(node.asIfStatement()); case LABELLED_STATEMENT: return processLabeledStatement(node.asLabelledStatement()); case PAREN_EXPRESSION: return processParenthesizedExpression(node.asParenExpression()); case IDENTIFIER_EXPRESSION: return processName(node.asIdentifierExpression()); case NEW_EXPRESSION: return processNewExpression(node.asNewExpression()); case OBJECT_LITERAL_EXPRESSION: return processObjectLiteral(node.asObjectLiteralExpression()); case COMPUTED_PROPERTY_DEFINITION: return processComputedPropertyDefinition(node.asComputedPropertyDefinition()); case COMPUTED_PROPERTY_GETTER: return processComputedPropertyGetter(node.asComputedPropertyGetter()); case COMPUTED_PROPERTY_METHOD: return processComputedPropertyMethod(node.asComputedPropertyMethod()); case COMPUTED_PROPERTY_SETTER: return processComputedPropertySetter(node.asComputedPropertySetter()); case RETURN_STATEMENT: return processReturnStatement(node.asReturnStatement()); case UPDATE_EXPRESSION: return processUpdateExpression(node.asUpdateExpression()); case PROGRAM: return processAstRoot(node.asProgram()); case LITERAL_EXPRESSION: // STRING, NUMBER, TRUE, FALSE, NULL, REGEXP return processLiteralExpression(node.asLiteralExpression()); case SWITCH_STATEMENT: return processSwitchStatement(node.asSwitchStatement()); case THROW_STATEMENT: return processThrowStatement(node.asThrowStatement()); case TRY_STATEMENT: return processTryStatement(node.asTryStatement()); case VARIABLE_STATEMENT: // var const let return processVariableStatement(node.asVariableStatement()); case VARIABLE_DECLARATION_LIST: return processVariableDeclarationList(node.asVariableDeclarationList()); case VARIABLE_DECLARATION: return processVariableDeclaration(node.asVariableDeclaration()); case WHILE_STATEMENT: return processWhileLoop(node.asWhileStatement()); case WITH_STATEMENT: return processWithStatement(node.asWithStatement()); case COMMA_EXPRESSION: return processCommaExpression(node.asCommaExpression()); case NULL: // this is not the null literal return processNull(node.asNull()); case FINALLY: return processFinally(node.asFinally()); case MISSING_PRIMARY_EXPRESSION: return processMissingExpression(node.asMissingPrimaryExpression()); case PROPERTY_NAME_ASSIGNMENT: return processPropertyNameAssignment(node.asPropertyNameAssignment()); case GET_ACCESSOR: return processGetAccessor(node.asGetAccessor()); case SET_ACCESSOR: return processSetAccessor(node.asSetAccessor()); case FORMAL_PARAMETER_LIST: return processFormalParameterList(node.asFormalParameterList()); case CLASS_DECLARATION: return processClassDeclaration(node.asClassDeclaration()); case SUPER_EXPRESSION: return processSuper(node.asSuperExpression()); case NEW_TARGET_EXPRESSION: return processNewTarget(node.asNewTargetExpression()); case YIELD_EXPRESSION: return processYield(node.asYieldStatement()); case AWAIT_EXPRESSION: return processAwait(node.asAwaitExpression()); case FOR_OF_STATEMENT: return processForOf(node.asForOfStatement()); case FOR_AWAIT_OF_STATEMENT: return processForAwaitOf(node.asForAwaitOfStatement()); case EXPORT_DECLARATION: return processExportDecl(node.asExportDeclaration()); case EXPORT_SPECIFIER: return processExportSpec(node.asExportSpecifier()); case IMPORT_DECLARATION: return processImportDecl(node.asImportDeclaration()); case IMPORT_SPECIFIER: return processImportSpec(node.asImportSpecifier()); case DYNAMIC_IMPORT_EXPRESSION: return processDynamicImport(node.asDynamicImportExpression()); case IMPORT_META_EXPRESSION: return processImportMeta(node.asImportMetaExpression()); case ARRAY_PATTERN: return processArrayPattern(node.asArrayPattern()); case OBJECT_PATTERN: return processObjectPattern(node.asObjectPattern()); case COMPREHENSION: return processComprehension(node.asComprehension()); case COMPREHENSION_FOR: return processComprehensionFor(node.asComprehensionFor()); case COMPREHENSION_IF: return processComprehensionIf(node.asComprehensionIf()); case DEFAULT_PARAMETER: return processDefaultParameter(node.asDefaultParameter()); case ITER_REST: return processIterRest(node.asIterRest()); case ITER_SPREAD: return processIterSpread(node.asIterSpread()); // ES2019 case OBJECT_REST: return processObjectPatternElement(node.asObjectRest()); case OBJECT_SPREAD: return processObjectSpread(node.asObjectSpread()); // ES2022 case FIELD_DECLARATION: return processField(node.asFieldDeclaration()); case COMPUTED_PROPERTY_FIELD: return processComputedPropertyField(node.asComputedPropertyField()); case ARGUMENT_LIST: // TODO(johnlenz): handle these or remove parser support break; default: break; } return processIllegalToken(node); } } private void reportErrorIfYieldOrAwaitInDefaultValue(Node defaultValueNode) { Node yieldNode = findNodeTypeInExpression(defaultValueNode, Token.YIELD); if (yieldNode != null) { errorReporter.error( "`yield` is illegal in parameter default value.", yieldNode.getSourceFileName(), yieldNode.getLineno(), yieldNode.getCharno()); } Node awaitNode = findNodeTypeInExpression(defaultValueNode, Token.AWAIT); if (awaitNode != null) { errorReporter.error( "`await` is illegal in parameter default value.", awaitNode.getSourceFileName(), awaitNode.getLineno(), awaitNode.getCharno()); } } /** * Tries to find a node with the given token in the given expression. Returns the first one found * in pre-order traversal or `null` if none found. Will not traverse into function or class * expressions. */ private static @Nullable Node findNodeTypeInExpression(Node expressionNode, Token token) { Deque worklist = new ArrayDeque<>(); worklist.add(expressionNode); while (!worklist.isEmpty()) { Node node = worklist.remove(); if (node.getToken() == token) { return node; } else if (!node.isFunction() && !node.isClass()) { for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { worklist.add(child); } } } return null; } String normalizeRegex(LiteralToken token) { String value = token.value; final int lastSlash = value.lastIndexOf('/'); int cur = value.indexOf('\\'); if (cur == -1) { return value.substring(1, lastSlash); } StringBuilder result = new StringBuilder(); int start = 1; while (cur != -1) { result.append(value, start, cur); cur++; // skip the escape char. char c = value.charAt(cur); switch (c) { // Characters for which the backslash is semantically important. case '^': case '$': case '\\': case '/': case '.': case '*': case '+': case '?': case '(': case ')': case '[': case ']': case '{': case '}': case '|': case '-': case 'b': case 'B': case 'c': case 'd': case 'D': case 'f': case 'n': case 'p': // 2018 unicode property escapes case 'P': // 2018 unicode property escapes case 'r': case 's': case 'S': case 't': case 'u': case 'v': case 'w': case 'W': case 'x': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': result.append('\\'); // fallthrough default: // For all other characters, the backslash has no effect, so just append the next char. result.append(c); } start = cur + 1; cur = value.indexOf('\\', start); } result.append(value, start, lastSlash); return result.toString(); } String normalizeString(LiteralToken token, boolean templateLiteral) { String value = token.value; // and are normalized as . For raw template literal string values: this is the // spec behaviour. For regular string literals: they can only be part of a line continuation, // which we want to scrub. if (value.indexOf('\r') >= 0) { value = value.replaceAll("\r\n?", "\n"); } int start = templateLiteral ? 0 : 1; // skip the leading quote int cur = value.indexOf('\\'); if (cur == -1) { // short circuit no escapes. return templateLiteral ? value : value.substring(1, value.length() - 1); } StringBuilder result = new StringBuilder(); while (cur != -1) { result.append(value, start, cur); cur += 1; // skip the escape char. char c = value.charAt(cur); switch (c) { case 'b': result.append('\b'); break; case 'f': result.append('\f'); break; case 'n': result.append('\n'); break; case 'r': result.append('\r'); break; case 't': result.append('\t'); break; case 'v': result.append('\u000B'); break; case '\n': // line continuation, skip the line break maybeWarnForFeature(token, Feature.STRING_CONTINUATION); errorReporter.warning( STRING_CONTINUATION_WARNING, sourceName, lineno(token.location.start), charno(token.location.start)); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': int numDigits; if (cur + 1 < value.length() && isOctalDigit(value.charAt(cur + 1))) { if (cur + 2 < value.length() && isOctalDigit(value.charAt(cur + 2))) { numDigits = 3; } else { numDigits = 2; } } else { numDigits = 1; } if (inStrictContext() || templateLiteral) { if (c == '0' && numDigits == 1) { // No warning: "\0" followed by a character which is not an octal digit // is allowed in strict mode. } else { errorReporter.warning( OCTAL_STRING_LITERAL_WARNING, sourceName, lineno(token.location.start), charno(token.location.start)); } } result.append((char) parseInt(value.substring(cur, cur + numDigits), 8)); cur += numDigits - 1; break; case 'x': result.append( (char) (hexdigit(value.charAt(cur + 1)) * 0x10 + hexdigit(value.charAt(cur + 2)))); cur += 2; break; case 'u': int escapeEnd; String hexDigits; if (value.charAt(cur + 1) != '{') { // Simple escape with exactly four hex digits: \\uXXXX escapeEnd = cur + 5; hexDigits = value.substring(cur + 1, escapeEnd); } else { // Escape with braces can have any number of hex digits: \\u{XXXXXXX} escapeEnd = cur + 2; while (Character.digit(value.charAt(escapeEnd), 0x10) >= 0) { escapeEnd++; } hexDigits = value.substring(cur + 2, escapeEnd); escapeEnd++; } int codePointValue = parseInt(hexDigits, 0x10); if (codePointValue > 0x10ffff) { errorReporter.error( "Undefined Unicode code-point", sourceName, lineno(token.location.start), charno(token.location.start)); // Compilation should stop, but we should finish the string and find more errors. // These appends are just to have a placeholder for the errored normalization. result.append("\\u{"); result.append(hexDigits); result.append("}"); } else { result.append(Character.toChars(codePointValue)); } cur = escapeEnd - 1; break; case '\'': case '"': case '\\': default: result.append(c); break; } start = cur + 1; cur = value.indexOf('\\', start); } // skip the trailing quote. result.append(value, start, templateLiteral ? value.length() : value.length() - 1); return result.toString(); } boolean isSupportedForInputLanguageMode(Feature feature) { return config.languageMode().featureSet.has(feature); } private boolean inStrictContext() { // TODO(johnlenz): in ECMASCRIPT5/6 is a "mixed" mode and we should track the context // that we are in, if we want to support it. return config.strictMode().isStrict(); } double normalizeNumber(LiteralToken token) { String value = token.value; if (value.contains("_")) { value = removeNumericSeparators(value, token); } SourceRange location = token.location; int length = value.length(); checkState(length > 0); checkState(value.charAt(0) != '-' && value.charAt(0) != '+'); if (value.charAt(0) == '.') { return Double.parseDouble('0' + value); } else if (value.charAt(0) == '0' && length > 1) { switch (value.charAt(1)) { case '.': case 'e': case 'E': return Double.parseDouble(value); case 'b': case 'B': { maybeWarnForFeature(token, Feature.BINARY_LITERALS); double v = 0; int c = 1; while (++c < length) { v = (v * 2) + binarydigit(value.charAt(c)); } return v; } case 'o': case 'O': { maybeWarnForFeature(token, Feature.OCTAL_LITERALS); double v = 0; int c = 1; while (++c < length) { v = (v * 8) + octaldigit(value.charAt(c)); } return v; } case 'x': case 'X': { double v = 0; int c = 1; while (++c < length) { v = (v * 0x10) + hexdigit(value.charAt(c)); } return v; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': double v = 0; int c = 0; while (++c < length) { char digit = value.charAt(c); if (isOctalDigit(digit)) { v = (v * 8) + octaldigit(digit); } else { errorReporter.error( INVALID_OCTAL_DIGIT, sourceName, lineno(location.start), charno(location.start)); return 0; } } if (inStrictContext()) { errorReporter.error( INVALID_ES5_STRICT_OCTAL, sourceName, lineno(location.start), charno(location.start)); } else { errorReporter.warning( INVALID_ES5_STRICT_OCTAL, sourceName, lineno(location.start), charno(location.start)); } return v; case '8': case '9': errorReporter.error( INVALID_OCTAL_DIGIT, sourceName, lineno(location.start), charno(location.start)); return 0; default: throw new IllegalStateException( "Unexpected character in number literal: " + value.charAt(1)); } } else { return Double.parseDouble(value); } } BigInteger normalizeBigInt(LiteralToken token) { String value = token.value; value = value.substring(0, value.indexOf('n')); if (value.contains("_")) { value = removeNumericSeparators(value, token); } int length = value.length(); checkState(length > 0); checkState(value.charAt(0) != '-' && value.charAt(0) != '+'); if (value.charAt(0) == '0' && length > 1) { switch (value.charAt(1)) { case 'b': case 'B': maybeWarnForFeature(token, Feature.BINARY_LITERALS); return new BigInteger(value.substring(2), 2); case 'o': case 'O': maybeWarnForFeature(token, Feature.OCTAL_LITERALS); return new BigInteger(value.substring(2), 8); case 'x': case 'X': return new BigInteger(value.substring(2), 16); case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': throw new IllegalStateException("Nonzero BigInts can't have a leading zero"); default: throw new IllegalStateException( "Unexpected character in bigint literal: " + value.charAt(1)); } } else { return new BigInteger(value); } } private String removeNumericSeparators(String value, LiteralToken token) { maybeWarnForFeature(token, Feature.NUMERIC_SEPARATOR); return value.replace("_", ""); } private static int binarydigit(char c) { if (c >= '0' && c <= '1') { return (c - '0'); } throw new IllegalStateException("unexpected: " + c); } private static boolean isOctalDigit(char c) { return c >= '0' && c <= '7'; } private static int octaldigit(char c) { if (isOctalDigit(c)) { return (c - '0'); } throw new IllegalStateException("unexpected: " + c); } private static int hexdigit(char c) { switch (c) { case '0': return 0; case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; case '6': return 6; case '7': return 7; case '8': return 8; case '9': return 9; case 'a': case 'A': return 10; case 'b': case 'B': return 11; case 'c': case 'C': return 12; case 'd': case 'D': return 13; case 'e': case 'E': return 14; case 'f': case 'F': return 15; default: throw new IllegalStateException("unexpected: " + c); } } static Token transformBooleanTokenType(TokenType token) { switch (token) { case TRUE: return Token.TRUE; case FALSE: return Token.FALSE; default: throw new IllegalStateException(String.valueOf(token)); } } static Token transformUpdateTokenType(TokenType token) { switch (token) { case PLUS_PLUS: return Token.INC; case MINUS_MINUS: return Token.DEC; default: throw new IllegalStateException(String.valueOf(token)); } } static Token transformUnaryTokenType(TokenType token) { switch (token) { case BANG: return Token.NOT; case TILDE: return Token.BITNOT; case PLUS: return Token.POS; case MINUS: return Token.NEG; case DELETE: return Token.DELPROP; case TYPEOF: return Token.TYPEOF; case VOID: return Token.VOID; default: throw new IllegalStateException(String.valueOf(token)); } } static Token transformBinaryTokenType(TokenType token) { switch (token) { case BAR: return Token.BITOR; case CARET: return Token.BITXOR; case AMPERSAND: return Token.BITAND; case EQUAL_EQUAL: return Token.EQ; case NOT_EQUAL: return Token.NE; case OPEN_ANGLE: return Token.LT; case LESS_EQUAL: return Token.LE; case CLOSE_ANGLE: return Token.GT; case GREATER_EQUAL: return Token.GE; case LEFT_SHIFT: return Token.LSH; case RIGHT_SHIFT: return Token.RSH; case UNSIGNED_RIGHT_SHIFT: return Token.URSH; case PLUS: return Token.ADD; case MINUS: return Token.SUB; case STAR: return Token.MUL; case SLASH: return Token.DIV; case PERCENT: return Token.MOD; case STAR_STAR: return Token.EXPONENT; case EQUAL_EQUAL_EQUAL: return Token.SHEQ; case NOT_EQUAL_EQUAL: return Token.SHNE; case IN: return Token.IN; case INSTANCEOF: return Token.INSTANCEOF; case COMMA: return Token.COMMA; case EQUAL: return Token.ASSIGN; case BAR_EQUAL: return Token.ASSIGN_BITOR; case CARET_EQUAL: return Token.ASSIGN_BITXOR; case AMPERSAND_EQUAL: return Token.ASSIGN_BITAND; case LEFT_SHIFT_EQUAL: return Token.ASSIGN_LSH; case RIGHT_SHIFT_EQUAL: return Token.ASSIGN_RSH; case UNSIGNED_RIGHT_SHIFT_EQUAL: return Token.ASSIGN_URSH; case PLUS_EQUAL: return Token.ASSIGN_ADD; case MINUS_EQUAL: return Token.ASSIGN_SUB; case STAR_EQUAL: return Token.ASSIGN_MUL; case STAR_STAR_EQUAL: return Token.ASSIGN_EXPONENT; case SLASH_EQUAL: return Token.ASSIGN_DIV; case PERCENT_EQUAL: return Token.ASSIGN_MOD; case OR: return Token.OR; case AND: return Token.AND; case QUESTION_QUESTION: return Token.COALESCE; case OR_EQUAL: return Token.ASSIGN_OR; case AND_EQUAL: return Token.ASSIGN_AND; case QUESTION_QUESTION_EQUAL: return Token.ASSIGN_COALESCE; default: throw new IllegalStateException(String.valueOf(token)); } } // Simple helper to create nodes and set the initial node properties. Node newNode(Token type) { return new Node(type).clonePropsFrom(templateNode); } Node newNode(Token type, Node child1) { return new Node(type, child1).clonePropsFrom(templateNode); } Node newNode(Token type, Node child1, Node child2) { return new Node(type, child1, child2).clonePropsFrom(templateNode); } Node newNode(Token type, Node child1, Node child2, Node child3) { return new Node(type, child1, child2, child3).clonePropsFrom(templateNode); } Node newStringNode(String value) { return IR.string(value).clonePropsFrom(templateNode); } Node newStringNode(Token type, String value) { return Node.newString(type, value).clonePropsFrom(templateNode); } /** Creates a new string node and attaches any pending JSDoc comments for it. */ Node newStringNodeWithNonJSDocComment(Token type, String value, SourcePosition start) { Node node = newStringNode(type, value); NonJSDocComment comment = parseNonJSDocCommentAt(start, false); if (comment != null) { node.setNonJSDocComment(comment); } return node; } Node newTemplateLitStringNode(@Nullable String cooked, String raw) { return Node.newTemplateLitString(cooked, raw).clonePropsFrom(templateNode); } Node newNumberNode(Double value) { return IR.number(value).clonePropsFrom(templateNode); } Node newBigIntNode(BigInteger value) { return Node.newBigInt(value).clonePropsFrom(templateNode); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy