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

com.google.javascript.jscomp.NodeTraversal Maven / Gradle / Ivy

/*
 * Copyright 2004 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.base.JSCompObjects.identical;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.InputId;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.jspecify.nullness.Nullable;

/**
 * NodeTraversal allows an iteration through the nodes in the parse tree, and facilitates the
 * optimizations on the parse tree.
 */
public class NodeTraversal {
  private final AbstractCompiler compiler;
  private final Callback callback;
  private final @Nullable ScopedCallback scopeCallback;
  private final ScopeCreator scopeCreator;
  private final boolean obeyDestructuringAndDefaultValueExecutionOrder;

  /** Contains the current node */
  private Node currentNode;

  private @Nullable Node currentHoistScopeRoot;

  /** Contains the current FUNCTION node if there is one, otherwise null. */
  private Node currentFunction;

  /** Contains the enclosing SCRIPT node if there is one, otherwise null. */
  private Node currentScript;

  /** The change scope for the current node being visiteds */
  private Node currentChangeScope;

  /**
   * The chain scope for the currentNode being visited. Scopes are relatively expensive to build so
   * they are built lazily. The list contains instanciated `AbstractScope` or the `Node`
   * representing the root of the scope.
   *
   * 

If `AbstractScope` is seen all previous entries will also have been instanciated due to the * heirarchical nature of scopes. */ private final ArrayList scopes = new ArrayList<>(); /** The current source file name */ private @Nullable String sourceName; /** The current input */ private @Nullable InputId inputId; private @Nullable CompilerInput compilerInput; /** Callback for tree-based traversals */ public interface Callback { /** * Visits a node in preorder (before its children) and decides whether its children should be * traversed. If the children should be traversed, they will be visited by {@link * #shouldTraverse(NodeTraversal, Node, Node)} in preorder and by {@link #visit(NodeTraversal, * Node, Node)} in postorder. * *

Siblings are always visited left-to-right. * *

Implementations can have side-effects (e.g. modify the parse tree). Removing the current * node is legal, but removing or reordering nodes above the current node may cause nodes to be * visited twice or not at all. * * @param t The current traversal. * @param n The current node. * @param parent The parent of the current node. * @return whether the children of this node should be visited */ boolean shouldTraverse(NodeTraversal t, Node n, @Nullable Node parent); /** * Visits a node in postorder (after its children). A node is visited in postorder iff {@link * #shouldTraverse(NodeTraversal, Node, Node)} returned true for its parent. In particular, the * root node is never visited in postorder. * *

Siblings are always visited left-to-right. * *

Implementations can have side-effects (e.g. modify the parse tree). Removing the current * node is legal, but removing or reordering nodes above the current node may cause nodes to be * visited twice or not at all. * * @param t The current traversal. * @param n The current node. * @param parent The parent of the current node. */ void visit(NodeTraversal t, Node n, @Nullable Node parent); } /** Callback that also knows about scope changes */ public interface ScopedCallback extends Callback { /** * Called immediately after entering a new scope. The new scope can be accessed through * t.getScope() */ void enterScope(NodeTraversal t); /** * Called immediately before exiting a scope. The ending scope can be accessed through * t.getScope() */ void exitScope(NodeTraversal t); } /** * Abstract callback to visit all nodes in postorder. Note: Do not create anonymous subclasses of * this. Instead, write a lambda expression which will be interpreted as an * AbstractPostOrderCallbackInterface. */ public abstract static class AbstractPostOrderCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } } /** * Abstract callback to visit all non-extern nodes in postorder. Note: Even though type-summary * nodes are included under the externs roots, they are traversed by this callback. */ public abstract static class ExternsSkippingCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return !n.isScript() || !n.isFromExterns() || NodeUtil.isFromTypeSummary(n); } } /** Abstract callback to visit all nodes in postorder. */ @FunctionalInterface public static interface AbstractPostOrderCallbackInterface { void visit(NodeTraversal t, Node n, Node parent); } /** Abstract callback to visit all nodes in preorder. */ public abstract static class AbstractPreOrderCallback implements Callback { @Override public final void visit(NodeTraversal t, Node n, Node parent) {} } /** Abstract scoped callback to visit all nodes in postorder. */ public abstract static class AbstractScopedCallback implements ScopedCallback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void enterScope(NodeTraversal t) {} @Override public void exitScope(NodeTraversal t) {} } /** * An traversal base class that tracks and caches the ControlFlowGraph (CFG) during the traversal. * The CFGs are constructed lazily. */ public abstract static class AbstractCfgCallback implements ScopedCallback { private final ArrayDeque cfgs = new ArrayDeque<>(); /** Gets the control flow graph for the current JS scope. */ @SuppressWarnings("unchecked") // The type is always ControlFlowGraph public ControlFlowGraph getControlFlowGraph(AbstractCompiler compiler) { ControlFlowGraph result; Object o = cfgs.peek(); checkState(o != null); if (o instanceof Node) { Node cfgRoot = (Node) o; result = ControlFlowAnalysis.builder() .setCompiler(compiler) .setCfgRoot(cfgRoot) .setIncludeEdgeAnnotations(true) .computeCfg(); cfgs.pop(); cfgs.push(result); } else { result = (ControlFlowGraph) o; } return result; } @Override public final void enterScope(NodeTraversal t) { Node currentScopeRoot = t.getScopeRoot(); if (NodeUtil.isValidCfgRoot(currentScopeRoot)) { cfgs.push(currentScopeRoot); } enterScopeWithCfg(t); } @Override public final void exitScope(NodeTraversal t) { exitScopeWithCfg(t); Node currentScopeRoot = t.getScopeRoot(); if (NodeUtil.isValidCfgRoot(currentScopeRoot)) { cfgs.pop(); } } public void enterScopeWithCfg(NodeTraversal t) {} public void exitScopeWithCfg(NodeTraversal t) {} @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) {} } /** Abstract callback to visit all nodes but not traverse into function bodies. */ public abstract static class AbstractShallowCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { // We do want to traverse the name of a named function, but we don't // want to traverse the arguments or body. return parent == null || !parent.isFunction() || n == parent.getFirstChild(); } } /** * Abstract callback to visit all structure and statement nodes but doesn't traverse into * functions or expressions. */ public abstract static class AbstractShallowStatementCallback implements Callback { @Override public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return parent == null || NodeUtil.isControlStructure(parent) || NodeUtil.isStatementBlock(parent); } } /** * Abstract callback that knows when a global script, goog.provide file, goog.module, * goog.loadModule, ES module or CommonJS module is entered or exited. This includes both whole * file modules and bundled modules, as well as files in the global scope. */ public abstract static class AbstractModuleCallback implements Callback { protected final AbstractCompiler compiler; private final ModuleMetadataMap moduleMetadataMap; private @Nullable ModuleMetadata currentModule; private @Nullable Node scopeRoot; private boolean inLoadModule; AbstractModuleCallback(AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap) { this.compiler = compiler; this.moduleMetadataMap = moduleMetadataMap; } /** * Called when the traversal enters a global file or module. * * @param currentModule The entered global file or module. * @param moduleScopeRoot The root scope for the entered module or SCRIPT for global files. */ protected void enterModule(ModuleMetadata currentModule, Node moduleScopeRoot) {} /** * Called when the traversal exits a global file or module. * * @param oldModule The exited global file or module. * @param moduleScopeRoot The root scope for the exited module or SCRIPT for global files. */ protected void exitModule(ModuleMetadata oldModule, Node moduleScopeRoot) {} private static final QualifiedName GOOG_MODULE = QualifiedName.of("goog.module"); @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case SCRIPT: currentModule = moduleMetadataMap.getModulesByPath().get(t.getInput().getPath().toString()); checkNotNull(currentModule); scopeRoot = n.hasChildren() && n.getFirstChild().isModuleBody() ? n.getFirstChild() : n; enterModule(currentModule, scopeRoot); break; case BLOCK: if (NodeUtil.isBundledGoogModuleScopeRoot(n)) { scopeRoot = n; inLoadModule = true; } break; case CALL: if (inLoadModule && GOOG_MODULE.matches(n.getFirstChild())) { ModuleMetadata newModule = moduleMetadataMap.getModulesByGoogNamespace().get(n.getLastChild().getString()); checkNotNull(newModule); // In the event of multiple goog.module statements (an error), don't call enterModule // more than once. if (!identical(newModule, currentModule)) { currentModule = newModule; enterModule(currentModule, scopeRoot); } } break; default: break; } return shouldTraverse(t, n, currentModule, scopeRoot); } /** * See {@link Callback#shouldTraverse}. * * @param t The current traversal. * @param n The current node. * @param currentModule The current module, or null if not inside a module (e.g. AST root). * @param moduleScopeRoot The root scope for the current module, or null if not inside a module * (e.g. AST root). * @return whether the children of this node should be visited */ protected boolean shouldTraverse( NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, @Nullable Node moduleScopeRoot) { return true; } @Override public final void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case SCRIPT: checkNotNull(currentModule); exitModule(currentModule, scopeRoot); currentModule = null; scopeRoot = null; break; case BLOCK: if (NodeUtil.isBundledGoogModuleScopeRoot(n)) { checkNotNull(currentModule); exitModule(currentModule, scopeRoot); scopeRoot = n.getGrandparent().getGrandparent(); inLoadModule = false; currentModule = moduleMetadataMap.getModulesByPath().get(t.getInput().getPath().toString()); checkNotNull(currentModule); } break; default: break; } visit(t, n, currentModule, scopeRoot); } /** * See {@link Callback#visit}. * * @param t The current traversal. * @param n The current node. * @param currentModule The current module, or null if not inside a module (e.g. AST root). * @param moduleScopeRoot The root scope for the current module, or null if not inside a module * (e.g. AST root). */ protected void visit( NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, @Nullable Node moduleScopeRoot) {} } /** Callback that fires on changed scopes. */ public abstract static class AbstractChangedScopeCallback extends AbstractPreOrderCallback { abstract void enterChangedScopeRoot(AbstractCompiler compiler, Node root); @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isChangeScopeRoot(n) && t.getCompiler().hasScopeChanged(n)) { this.enterChangedScopeRoot(t.getCompiler(), n); } return true; } } public static Builder builder() { return new Builder(); } /** Builder */ public static final class Builder { private Callback callback; private AbstractCompiler compiler; private ScopeCreator scopeCreator; private boolean obeyDestructuringAndDefaultValueExecutionOrder = false; private Builder() {} @CanIgnoreReturnValue public Builder setCallback(Callback x) { this.callback = x; return this; } @CanIgnoreReturnValue public Builder setCallback(AbstractPostOrderCallbackInterface x) { this.callback = new AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { x.visit(t, n, parent); } }; return this; } @CanIgnoreReturnValue public Builder setCompiler(AbstractCompiler x) { this.compiler = x; return this; } @CanIgnoreReturnValue public Builder setScopeCreator(ScopeCreator x) { this.scopeCreator = x; return this; } @CanIgnoreReturnValue public Builder setObeyDestructuringAndDefaultValueExecutionOrder(boolean x) { this.obeyDestructuringAndDefaultValueExecutionOrder = x; return this; } public NodeTraversal build() { return new NodeTraversal(this); } public void traverse(Node root) { this.build().traverse(root); } void traverseAtScope(AbstractScope scope) { this.build().traverseAtScope(scope); } void traverseRoots(Node externs, Node root) { this.build().traverseRoots(externs, root); } void traverseWithScope(Node root, AbstractScope s) { this.build().traverseWithScope(root, s); } } private NodeTraversal(Builder builder) { this.compiler = checkNotNull(builder.compiler); this.callback = checkNotNull(builder.callback); this.scopeCallback = (this.callback instanceof ScopedCallback) ? (ScopedCallback) this.callback : null; this.scopeCreator = (builder.scopeCreator == null) ? new SyntacticScopeCreator(this.compiler) : builder.scopeCreator; this.obeyDestructuringAndDefaultValueExecutionOrder = builder.obeyDestructuringAndDefaultValueExecutionOrder; } private void throwUnexpectedException(Throwable unexpectedException) { // If there's an unexpected exception, try to get the // line number of the code that caused it. String message = unexpectedException.getMessage(); // TODO(user): It is possible to get more information if currentNode or // its parent is missing. We still have the scope stack in which it is still // very useful to find out at least which function caused the exception. if (currentScript != null) { message = unexpectedException.getMessage() + "\n" + formatNodeContext("Node", currentNode) + (currentNode == null ? "" : formatNodeContext("Parent", currentNode.getParent())); } compiler.throwInternalError(message, unexpectedException); } private String formatNodeContext(String label, Node n) { if (n == null) { return " " + label + ": NULL"; } return " " + label + "(" + n.toString(false, false, false) + "): " + formatNodePosition(n); } /** Traverses a parse tree recursively. */ private void traverse(Node root) { try { initTraversal(root); currentNode = root; pushScope(root); // null parent ensures that the shallow callbacks will traverse root traverseBranch(root, null); popScope(); } catch (Error | Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } /** Traverses using the SyntacticScopeCreator */ public static void traverse(AbstractCompiler compiler, Node root, Callback cb) { NodeTraversal.builder().setCompiler(compiler).setCallback(cb).traverse(root); } private void traverseRoots(Node externs, Node root) { try { Node scopeRoot = externs.getParent(); checkNotNull(scopeRoot); initTraversal(scopeRoot); currentNode = scopeRoot; pushScope(scopeRoot); traverseBranch(externs, scopeRoot); checkState(root.getParent() == scopeRoot); traverseBranch(root, scopeRoot); popScope(); } catch (Error | Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } public static void traverseRoots( AbstractCompiler compiler, Callback cb, Node externs, Node root) { NodeTraversal.builder().setCompiler(compiler).setCallback(cb).traverseRoots(externs, root); } private static final String MISSING_SOURCE = "[source unknown]"; private String formatNodePosition(Node n) { String sourceFileName = getBestSourceFileName(n); if (sourceFileName == null) { return MISSING_SOURCE + "\n"; } int lineNumber = n.getLineno(); int columnNumber = n.getCharno(); String src = compiler.getSourceLine(sourceFileName, lineNumber); if (src == null) { src = MISSING_SOURCE; } return sourceFileName + ":" + lineNumber + ":" + columnNumber + "\n" + src + "\n"; } /** * Traverses a parse tree recursively with a scope, starting with the given root. This should only * be used in the global scope or module scopes. Otherwise, use {@link #traverseAtScope}. */ private void traverseWithScope(Node root, AbstractScope s) { checkState(s.isGlobal() || s.isModuleScope(), s); try { initTraversal(root); currentNode = root; pushScope(s); traverseBranch(root, null); popScope(); } catch (Error | Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } /** * Traverses a parse tree recursively with a scope, starting at that scope's root. Omits children * of the scope root that are traversed in the outer scope (specifically, non-bleeding function * and class name nodes, class extends clauses, and computed property keys). */ private void traverseAtScope(AbstractScope s) { Node n = s.getRootNode(); initTraversal(n); currentNode = n; Deque> parentScopes = new ArrayDeque<>(); AbstractScope temp = s.getParent(); while (temp != null) { parentScopes.push(temp); temp = temp.getParent(); } while (!parentScopes.isEmpty()) { pushScope(parentScopes.pop(), true); } switch (n.getToken()) { case FUNCTION: if (callback.shouldTraverse(this, n, null)) { pushScope(s); Node fnName = n.getFirstChild(); Node args = fnName.getNext(); Node body = args.getNext(); if (!NodeUtil.isFunctionDeclaration(n)) { // Only traverse the function name if it's a bleeding function expression name. traverseBranch(fnName, n); } traverseBranch(args, n); traverseBranch(body, n); popScope(); callback.visit(this, n, null); } break; case CLASS: if (callback.shouldTraverse(this, n, null)) { pushScope(s); Node className = n.getFirstChild(); Node body = n.getLastChild(); if (NodeUtil.isClassExpression(n)) { // Only traverse the class name if it's a bleeding class expression name. traverseBranch(className, n); } // Omit the extends node, which is in the outer scope. Computed property keys are already // excluded by handleClassMembers. traverseBranch(body, n); popScope(); callback.visit(this, n, null); } break; case BLOCK: if (callback.shouldTraverse(this, n, null)) { pushScope(s); // traverseBranch is not called here to avoid re-creating the block scope. traverseChildren(n); popScope(); callback.visit(this, n, null); } break; case MEMBER_FIELD_DEF: pushScope(s); if (callback.shouldTraverse(this, n, null)) { // traverseBranch is not called here to avoid re-creating the MemberFieldDef scope. traverseChildren(n); callback.visit(this, n, null); } popScope(); break; case COMPUTED_FIELD_DEF: pushScope(s); if (callback.shouldTraverse(this, n, null)) { traverseBranch(n.getLastChild(), n); callback.visit(this, n, null); } popScope(); break; case FOR_IN: case FOR_OF: case FOR_AWAIT_OF: case FOR: if (callback.shouldTraverse(this, n, null)) { pushScope(s); Node forAssignmentParam = n.getFirstChild(); Node forIterableParam = forAssignmentParam.getNext(); Node forBodyScope = forIterableParam.getNext(); traverseBranch(forAssignmentParam, n); traverseBranch(forIterableParam, n); traverseBranch(forBodyScope, n); popScope(); callback.visit(this, n, null); } break; case SWITCH: if (callback.shouldTraverse(this, n, null)) { pushScope(s); traverseChildren(n); popScope(); callback.visit(this, n, null); } break; default: checkState( s.isGlobal() || s.isModuleScope(), "Expected global or module scope. Got: (%s)", s); traverseWithScope(n, s); } } /** * Traverses *just* the contents of provided scope nodes (and optionally scopes nested within * them) but will fall back on traversing the entire AST from root if a null scope nodes list is * provided. * * @param root If scopeNodes is null, this method will just traverse 'root' instead. If scopeNodes * is not null, this parameter is ignored. */ public static void traverseScopeRoots( AbstractCompiler compiler, @Nullable Node root, @Nullable List scopeNodes, Callback cb, boolean traverseNested) { if (scopeNodes == null) { NodeTraversal.traverse(compiler, root, cb); return; } class TraverseScopeRootsCallback implements ScopedCallback { boolean insideScopeNode = false; @Nullable Node scopeNode = null; @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (scopeNode == n) { insideScopeNode = true; } return (traverseNested || scopeNode == n || !NodeUtil.isChangeScopeRoot(n)) && cb.shouldTraverse(t, n, parent); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (scopeNode == n) { insideScopeNode = false; } cb.visit(t, n, parent); } @Override public void enterScope(NodeTraversal t) { if (insideScopeNode && cb instanceof ScopedCallback) { ((ScopedCallback) cb).enterScope(t); } } @Override public void exitScope(NodeTraversal t) { if (insideScopeNode && cb instanceof ScopedCallback) { ((ScopedCallback) cb).exitScope(t); } } } TraverseScopeRootsCallback scb = new TraverseScopeRootsCallback(); MemoizedScopeCreator scopeCreator = new MemoizedScopeCreator(new SyntacticScopeCreator(compiler)); for (final Node scopeNode : scopeNodes) { scb.scopeNode = scopeNode; NodeTraversal.builder() .setCompiler(compiler) .setCallback(scb) .setScopeCreator(scopeCreator) .build() .traverseScopeRoot(scopeNode); } } private void traverseScopeRoot(Node scopeRoot) { try { initTraversal(scopeRoot); currentNode = scopeRoot; initScopeRoots(scopeRoot.getParent()); traverseBranch(scopeRoot, scopeRoot.getParent()); } catch (Error | Exception unexpectedException) { throwUnexpectedException(unexpectedException); } } public AbstractCompiler getCompiler() { return compiler; } /** * Gets the current input source name. * * @return A string that may be empty, but not null */ public String getSourceName() { if (sourceName == null) { sourceName = currentScript != null ? currentScript.getSourceFileName() : ""; } return sourceName; } /** Gets the current input source. */ public CompilerInput getInput() { InputId inputId = getInputId(); if (compilerInput == null && inputId != null) { compilerInput = compiler.getInput(inputId); } return compilerInput; } /** Gets the current input chunk. */ public @Nullable JSChunk getChunk() { CompilerInput input = getInput(); return input == null ? null : input.getChunk(); } /** Returns the node currently being traversed. */ public Node getCurrentNode() { return currentNode; } private void handleScript(Node n, Node parent) { if (Platform.isThreadInterrupted()) { throw new RuntimeException(new InterruptedException()); } setChangeScope(n); currentNode = n; currentScript = n; clearScriptState(); if (callback.shouldTraverse(this, n, parent)) { traverseChildren(n); currentNode = n; callback.visit(this, n, parent); } setChangeScope(null); } private void handleFunction(Node n, Node parent) { Node changeScope = this.currentChangeScope; setChangeScope(n); currentNode = n; if (callback.shouldTraverse(this, n, parent)) { traverseFunction(n, parent); currentNode = n; callback.visit(this, n, parent); } setChangeScope(changeScope); } /** Traverses a module. */ private void handleModule(Node n, Node parent) { currentHoistScopeRoot = n; pushScope(n); currentNode = n; if (callback.shouldTraverse(this, n, parent)) { currentNode = n; traverseChildren(n); callback.visit(this, n, parent); } popScope(); // Module bodies don't nest currentHoistScopeRoot = null; } private void handleDestructuringOrDefaultValue(Node n, Node parent) { currentNode = n; if (callback.shouldTraverse(this, n, parent)) { Node first = n.getFirstChild(); Node second = first.getNext(); if (second != null) { checkState(second.getNext() == null, second); traverseBranch(second, n); } traverseBranch(first, n); currentNode = n; callback.visit(this, n, parent); } } /** Traverses a branch. */ private void traverseBranch(Node n, @Nullable Node parent) { switch (n.getToken()) { case SCRIPT: handleScript(n, parent); return; case FUNCTION: handleFunction(n, parent); return; case MODULE_BODY: handleModule(n, parent); return; case CLASS: handleClass(n, parent); return; case CLASS_MEMBERS: handleClassMembers(n, parent); return; case DEFAULT_VALUE: case DESTRUCTURING_LHS: // TODO(nickreid): Handle ASSIGN with destructuring target. if (this.obeyDestructuringAndDefaultValueExecutionOrder) { handleDestructuringOrDefaultValue(n, parent); return; } break; default: break; } currentNode = n; if (!callback.shouldTraverse(this, n, parent)) { return; } boolean createsBlockScope = NodeUtil.createsBlockScope(n); Node previousHoistScopeRoot = currentHoistScopeRoot; if (createsBlockScope) { pushScope(n); if (NodeUtil.isClassStaticBlock(n)) { currentHoistScopeRoot = n; } } /** * Intentiionally inlined call to traverseChildren. * *

Calling traverseChildren here would double the maximum stack depth seen during * compilation. That can cause stack overflows on some very deep ASTs (e.g. b/188616350). * Inlining side-steps the issue. */ for (Node child = n.getFirstChild(); child != null; ) { // child could be replaced, in which case our child node // would no longer point to the true next Node next = child.getNext(); traverseBranch(child, n); child = next; } if (createsBlockScope) { popScope(); currentHoistScopeRoot = previousHoistScopeRoot; } currentNode = n; callback.visit(this, n, parent); } /** Traverses a function. */ private void traverseFunction(Node n, Node parent) { final Node fnName = n.getFirstChild(); // NOTE: If a function declaration is the root of a traversal, then we will treat it as a // function expression (since 'parent' is null, even though 'n' actually has a parent node) and // traverse the function name before entering the scope, rather than afterwards. Removing the // null check for 'parent' seems safe, but causes a rare crash when traverseScopeRoots is called // by PeepholeOptimizationsPass on a function that somehow doesn't actually have a parent at all // (presumably because it's already been removed from the AST?) so that doesn't actually work. // This does not actually change anything, though, since rooting a traversal at a function node // causes the function scope to be entered twice (unless using #traverseAtScope, which doesn't // call this method), so that the name node is just always traversed inside the scope anyway. boolean isFunctionDeclaration = parent != null && NodeUtil.isFunctionDeclaration(n); if (isFunctionDeclaration) { // Function declarations are in the scope containing the declaration. traverseBranch(fnName, n); } currentNode = n; // may nest within class static blocks Node previousHoistScopeRoot = currentHoistScopeRoot; currentHoistScopeRoot = n; Node previousFunction = currentFunction; currentFunction = n; pushScope(n); if (!isFunctionDeclaration) { // Function expression names are only accessible within the function // scope. traverseBranch(fnName, n); } final Node args = fnName.getNext(); final Node body = args.getNext(); // Args traverseBranch(args, n); // Body // ES6 "arrow" function may not have a block as a body. traverseBranch(body, n); popScope(); currentFunction = previousFunction; currentHoistScopeRoot = previousHoistScopeRoot; } /** * Traverses a class. Note that we traverse some of the child nodes slightly out of order to * ensure children are visited in the correct scope. The following children are in the outer * scope: (1) the 'extends' clause, (2) any computed method keys, (3) the class name for class * declarations only (class expression names are traversed in the class scope). This requires that * we visit the extends node (second child) and any computed member keys (grandchildren of the * last, body, child) before visiting the name (first child) or body (last child). */ private void handleClass(Node n, Node parent) { this.currentNode = n; if (!callback.shouldTraverse(this, n, parent)) { return; } final Node className = n.getFirstChild(); final Node extendsClause = className.getNext(); final Node body = extendsClause.getNext(); boolean isClassExpression = NodeUtil.isClassExpression(n); traverseBranch(extendsClause, n); if (!isClassExpression) { // Class declarations are in the scope containing the declaration. traverseBranch(className, n); } currentNode = n; pushScope(n); if (isClassExpression) { // Class expression names are only accessible within the function // scope. traverseBranch(className, n); } // Body traverseBranch(body, n); popScope(); this.currentNode = n; callback.visit(this, n, parent); } /** Traverse class members, excluding keys of computed props. */ private void handleClassMembers(Node n, Node parent) { this.currentNode = n; if (!callback.shouldTraverse(this, n, parent)) { return; } for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); // see traverseChildren if (child.isComputedProp() || child.isComputedFieldDef()) { traverseBranch(child.getFirstChild(), child); } child = next; } for (Node child = n.getFirstChild(); child != null; ) { Node next = child.getNext(); // see traverseChildren switch (child.getToken()) { case COMPUTED_PROP: currentNode = n; if (callback.shouldTraverse(this, child, n)) { traverseBranch(child.getLastChild(), child); currentNode = n; callback.visit(this, child, n); } break; case COMPUTED_FIELD_DEF: currentNode = n; pushScope(child); if (callback.shouldTraverse(this, child, n)) { if (child.hasTwoChildren()) { // No RHS to traverse in `[x];` computed field case traverseBranch(child.getLastChild(), child); } currentNode = n; callback.visit(this, child, n); } popScope(); break; case MEMBER_FIELD_DEF: pushScope(child); traverseBranch(child, n); popScope(); break; case BLOCK: case MEMBER_FUNCTION_DEF: case MEMBER_VARIABLE_DEF: case GETTER_DEF: case SETTER_DEF: traverseBranch(child, n); break; default: throw new IllegalStateException("Invalid class member: " + child.getToken()); } child = next; } this.currentNode = n; callback.visit(this, n, parent); } private void traverseChildren(Node n) { for (Node child = n.getFirstChild(); child != null; ) { // child could be replaced, in which case our child node // would no longer point to the true next Node next = child.getNext(); traverseBranch(child, n); child = next; } } /** * Examines the functions stack for the last instance of a function node. When possible, prefer * this method over NodeUtil.getEnclosingFunction() because this in general looks at less nodes. */ public @Nullable Node getEnclosingFunction() { return currentFunction; } /** Creates a new scope (e.g. when entering a function). */ private void pushScope(Node node) { checkNotNull(currentNode); checkNotNull(node); scopes.add(node); if (scopeCallback != null) { scopeCallback.enterScope(this); } } /** Creates a new scope (e.g. when entering a function). */ private void pushScope(AbstractScope s) { pushScope(s, false); } /** * Creates a new scope (e.g. when entering a function). * * @param quietly Don't fire an enterScope callback. */ private void pushScope(AbstractScope s, boolean quietly) { checkNotNull(currentNode); scopes.add(s); if (!quietly && scopeCallback != null) { scopeCallback.enterScope(this); } } private void popScope() { popScope(false); } /** * Pops back to the previous scope (e.g. when leaving a function). * * @param quietly Don't fire the exitScope callback. */ private void popScope(boolean quietly) { if (!quietly && scopeCallback != null) { scopeCallback.exitScope(this); } scopes.remove(scopes.size() - 1); } /** * Scopes are stored in a list of objects. These can either be instanciated `AbstractScope` or the * `Node` representing the root of the scope. */ private Node getNodeRootFromScopeObj(Object root) { return root instanceof Node ? (Node) root : ((AbstractScope) root).getRootNode(); } /** Returns the current scope's root. */ public @Nullable Node getScopeRoot() { int roots = scopes.size(); if (roots > 0) { return getNodeRootFromScopeObj(scopes.get(roots - 1)); } else { return null; } } int getScopeDepth() { int depth = scopes.size(); checkState(depth > 0); return depth - 1; // Use 0-based scope depth to be consistent within the compiler } public Scope getScope() { return getAbstractScope().untyped(); } public TypedScope getTypedScope() { return getAbstractScope().typed(); } /** Gets the current scope. */ public AbstractScope getAbstractScope() { return getAbstractScope(scopes.size() - 1); } /** * Recusively visit uninstanciated scopes and instanciate and store any necessary scopes to return * the requested scope. */ private AbstractScope getAbstractScope(int rootDepth) { Object o = scopes.get(rootDepth); if (o instanceof Node) { // The root scope has a null parent. AbstractScope parentScope = (rootDepth > 0) ? getAbstractScope(rootDepth - 1) : null; AbstractScope scope = scopeCreator.createScope((Node) o, parentScope); scopes.set(rootDepth, scope); return scope; } else { return (AbstractScope) o; } } public boolean isHoistScope() { return isHoistScopeRootNode(getScopeRoot()); } public @Nullable Node getClosestHoistScopeRoot() { for (int i = scopes.size() - 1; i >= 0; i--) { Node rootNode = getNodeRootFromScopeObj(scopes.get(i)); if (isHoistScopeRootNode(rootNode)) { return rootNode; } } return null; } public @Nullable AbstractScope getClosestContainerScope() { for (int i = scopes.size() - 1; i >= 0; i--) { Node rootNode = getNodeRootFromScopeObj(scopes.get(i)); if (!NodeUtil.createsBlockScope(rootNode)) { return getAbstractScope(i); } } return null; } public @Nullable AbstractScope getClosestHoistScope() { for (int i = scopes.size() - 1; i >= 0; i--) { Node rootNode = getNodeRootFromScopeObj(scopes.get(i)); if (isHoistScopeRootNode(rootNode)) { return getAbstractScope(i); } } return null; } private static boolean isHoistScopeRootNode(Node n) { switch (n.getToken()) { case FUNCTION: case MODULE_BODY: case ROOT: case SCRIPT: return true; default: return NodeUtil.isFunctionBlock(n); } } public ScopeCreator getScopeCreator() { return scopeCreator; } /** * Determines whether the traversal is currently in the global scope. Note that this returns false * in a global block scope. */ public boolean inGlobalScope() { return getScopeDepth() == 0; } /** Determines whether the traversal is currently in a module scope. */ public boolean inModuleScope() { return NodeUtil.isModuleScopeRoot(getScopeRoot()); } public boolean inGlobalOrModuleScope() { return this.inGlobalScope() || inModuleScope(); } /** Determines whether the traversal is currently in the scope of the block of a function. */ public boolean inFunctionBlockScope() { return NodeUtil.isFunctionBlock(getScopeRoot()); } /** Determines whether the hoist scope of the current traversal is global. */ public boolean inGlobalHoistScope() { return currentHoistScopeRoot == null; } /** Determines whether the hoist scope of the current traversal is global. */ public boolean inModuleHoistScope() { Node moduleRoot = currentHoistScopeRoot; if (moduleRoot == null) { // in global hoist scope return false; } if (moduleRoot.isFunction()) { // For wrapped modules, the function block is the module scope root. moduleRoot = moduleRoot.getLastChild(); } return NodeUtil.isModuleScopeRoot(moduleRoot); } /** Reports a diagnostic (error or warning) */ public void report(Node n, DiagnosticType diagnosticType, String... arguments) { JSError error = JSError.make(n, diagnosticType, arguments); compiler.report(error); } public void reportCodeChange() { Node changeScope = this.currentChangeScope; checkNotNull(changeScope); checkState(NodeUtil.isChangeScopeRoot(changeScope), changeScope); compiler.reportChangeToChangeScope(changeScope); } public void reportCodeChange(Node n) { compiler.reportChangeToEnclosingScope(n); } /** * Returns the SCRIPT node enclosing the current scope, or `null` if unknown * *

e.g. returns null if {@link #traverseInnerNode(Node, Node, AbstractScope)} was used */ @Nullable Node getCurrentScript() { return currentScript; } /** * @param n The current change scope, should be null when the traversal is complete. */ private void setChangeScope(@Nullable Node n) { this.currentChangeScope = n; } private Node getEnclosingScript(Node n) { while (n != null && !n.isScript()) { n = n.getParent(); } return n; } private void initTraversal(Node traversalRoot) { if (Platform.isThreadInterrupted()) { throw new RuntimeException(new InterruptedException()); } Node hoistScopeRoot = NodeUtil.getEnclosingHoistScopeRoot(traversalRoot); this.currentHoistScopeRoot = hoistScopeRoot; // "null" is the global hoist scope root, but we want the current script if any // for the "change scope" Node changeScope = NodeUtil.getEnclosingChangeScopeRoot( hoistScopeRoot != null ? hoistScopeRoot : traversalRoot); setChangeScope(changeScope); Node enclosingFunction = hoistScopeRoot != null ? NodeUtil.getEnclosingFunction(hoistScopeRoot) : null; this.currentFunction = enclosingFunction; Node script = getEnclosingScript(changeScope); currentScript = script; clearScriptState(); } /** * Prefills the scopes stack up to a given spot in the AST. Allows for starting traversal at any * spot while still having correct scope state. */ private void initScopeRoots(Node n) { Deque queuedScopeRoots = new ArrayDeque<>(); while (n != null) { if (isScopeRoot(n)) { queuedScopeRoots.addFirst(n); } n = n.getParent(); } for (Node queuedScopeRoot : queuedScopeRoots) { pushScope(queuedScopeRoot); } } private boolean isScopeRoot(Node n) { if (n.isRoot() && n.getParent() == null) { return true; } else if (n.isFunction()) { return true; } else if (NodeUtil.createsBlockScope(n)) { return true; } return false; } /** * This is used to clear any cached state with regard to the current script and should be called * *

before traversing a SCRIPT rootedß subtree. */ private void clearScriptState() { inputId = null; sourceName = null; compilerInput = null; } InputId getInputId() { if (currentScript != null && inputId == null) { inputId = currentScript.getInputId(); } return inputId; } private String getBestSourceFileName(Node n) { return n == null ? getSourceName() : n.getSourceFileName(); } }