Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.nullToEmpty;
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 java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import javax.annotation.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;
/** Contains the current node*/
private Node curNode;
/** Contains the enclosing SCRIPT node if there is one, otherwise null. */
private Node curScript;
/** The change scope for the current node being visiteds */
private Node currentChangeScope;
/**
* Stack containing the Scopes that have been created. The Scope objects
* are lazily created; so the {@code scopeRoots} stack contains the
* Nodes for all Scopes that have not been created yet.
*/
private final Deque> scopes = new ArrayDeque<>();
/**
* A stack of scope roots. See #scopes.
*/
private final List scopeRoots = new ArrayList<>();
/**
* Stack containing the control flow graphs (CFG) that have been created. There are fewer CFGs
* than scopes, since block-level scopes are not valid CFG roots. The CFG objects are lazily
* populated: elements are simply the CFG root node until requested by {@link
* #getControlFlowGraph()}.
*/
private final Deque cfgs = new ArrayDeque<>();
/** The current source file name */
private String sourceName;
/** The current input */
private InputId inputId;
private CompilerInput compilerInput;
/** The scope creator */
private final ScopeCreator scopeCreator;
/** Possible callback for scope entry and exist **/
private ScopedCallback scopeCallback;
/** Callback for passes that iterate over a list of change scope roots (FUNCTIONs and SCRIPTs) */
public interface ChangeScopeRootCallback {
void enterChangeScopeRoot(AbstractCompiler compiler, Node root);
}
/**
* 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, 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, 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);
}
private static Callback makePostOrderCallback(AbstractPostOrderCallbackInterface lambda) {
return new Callback() {
@Override
public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
return true;
}
@Override
public final void visit(NodeTraversal t, Node n, Node parent) {
lambda.visit(t, n, 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) {}
}
/**
* 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;
@Nullable private ModuleMetadata currentModule;
@Nullable private 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) {}
@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 && n.getFirstChild().matchesQualifiedName("goog.module")) {
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 (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) {}
}
/**
* Creates a node traversal using the specified callback interface
* and the scope creator.
*/
public NodeTraversal(AbstractCompiler compiler, Callback cb, ScopeCreator scopeCreator) {
this.callback = cb;
if (cb instanceof ScopedCallback) {
this.scopeCallback = (ScopedCallback) cb;
}
this.compiler = compiler;
this.scopeCreator = scopeCreator;
}
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 curNode 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 (inputId != null) {
message =
unexpectedException.getMessage() + "\n"
+ formatNodeContext("Node", curNode)
+ (curNode == null ? "" : formatNodeContext("Parent", curNode.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.
*/
public void traverse(Node root) {
try {
initTraversal(root);
curNode = 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 t = new NodeTraversal(compiler, cb, new SyntacticScopeCreator(compiler));
t.traverse(root);
}
/** Traverses in post order. */
public static void traversePostOrder(
AbstractCompiler compiler, Node root, AbstractPostOrderCallbackInterface cb) {
traverse(compiler, root, makePostOrderCallback(cb));
}
void traverseRoots(Node externs, Node root) {
try {
Node scopeRoot = externs.getParent();
checkNotNull(scopeRoot);
initTraversal(scopeRoot);
curNode = 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 t = new NodeTraversal(compiler, cb, new SyntacticScopeCreator(compiler));
t.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}.
*/
void traverseWithScope(Node root, AbstractScope, ?> s) {
checkState(s.isGlobal() || s.isModuleScope(), s);
try {
initTraversal(root);
curNode = 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).
*/
void traverseAtScope(AbstractScope, ?> s) {
Node n = s.getRootNode();
initTraversal(n);
curNode = 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);
}
if (n.isFunction()) {
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);
}
} else if (n.isClass()) {
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);
}
} else if (n.isBlock()) {
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);
}
} else if (NodeUtil.isAnyFor(n)) {
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);
}
} else if (n.isSwitch()) {
if (callback.shouldTraverse(this, n, null)) {
pushScope(s);
traverseChildren(n);
popScope();
callback.visit(this, n, null);
}
} else {
checkState(s.isGlobal() || s.isModuleScope(), "Expected global or module scope. Got:", s);
traverseWithScope(n, s);
}
}
private void traverseScopeRoot(Node scopeRoot) {
try {
initTraversal(scopeRoot);
curNode = scopeRoot;
initScopeRoots(scopeRoot.getParent());
traverseBranch(scopeRoot, scopeRoot.getParent());
} catch (Error | Exception unexpectedException) {
throwUnexpectedException(unexpectedException);
}
}
/**
* 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,
final Callback cb,
final boolean traverseNested) {
traverseScopeRoots(compiler, root, scopeNodes, cb, null, traverseNested);
}
/**
* 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. Also allows for a callback to notify when starting on one of the provided scope
* nodes.
* @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,
final Callback cb,
@Nullable final ChangeScopeRootCallback changeCallback,
final boolean traverseNested) {
if (scopeNodes == null) {
NodeTraversal.traverse(compiler, root, cb);
} else {
MemoizedScopeCreator scopeCreator =
new MemoizedScopeCreator(new SyntacticScopeCreator(compiler));
for (final Node scopeNode : scopeNodes) {
traverseSingleScopeRoot(
compiler, cb, changeCallback, traverseNested, scopeCreator, scopeNode);
}
}
}
private static void traverseSingleScopeRoot(
AbstractCompiler compiler,
final Callback cb,
@Nullable ChangeScopeRootCallback changeCallback,
final boolean traverseNested,
MemoizedScopeCreator scopeCreator,
final Node scopeNode) {
if (changeCallback != null) {
changeCallback.enterChangeScopeRoot(compiler, scopeNode);
}
ScopedCallback scb = new ScopedCallback() {
boolean insideScopeNode = false;
@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);
}
}
};
NodeTraversal t = new NodeTraversal(compiler, scb, scopeCreator);
t.traverseScopeRoot(scopeNode);
}
/**
* Traverse a function out-of-band of normal traversal.
*
* @param node The function node.
* @param scope The scope the function is contained in. Does not fire enter/exit
* callback events for this scope.
*/
public void traverseFunctionOutOfBand(Node node, AbstractScope, ?> scope) {
checkNotNull(scope);
checkState(node.isFunction(), node);
checkNotNull(scope.getRootNode());
initTraversal(node);
curNode = node.getParent();
pushScope(scope, true /* quietly */);
traverseBranch(node, curNode);
popScope(true /* quietly */);
}
/**
* Traverses an inner node recursively with a refined scope. An inner node may
* be any node with a non {@code null} parent (i.e. all nodes except the
* root).
*
* @param node the node to traverse
* @param parent the node's parent, it may not be {@code null}
* @param refinedScope the refined scope of the scope currently at the top of
* the scope stack or in trivial cases that very scope or {@code null}
*/
void traverseInnerNode(Node node, Node parent, AbstractScope, ?> refinedScope) {
checkNotNull(parent);
initTraversal(node);
if (refinedScope != null && getAbstractScope() != refinedScope) {
curNode = node;
pushScope(refinedScope);
traverseBranch(node, parent);
popScope();
} else {
traverseBranch(node, parent);
}
}
public AbstractCompiler getCompiler() {
return compiler;
}
/**
* Gets the current line number, or zero if it cannot be determined. The line
* number is retrieved lazily as a running time optimization.
*/
public int getLineNumber() {
Node cur = curNode;
while (cur != null) {
int line = cur.getLineno();
if (line >= 0) {
return line;
}
cur = cur.getParent();
}
return 0;
}
/**
* Gets the current char number, or zero if it cannot be determined. The line
* number is retrieved lazily as a running time optimization.
*/
public int getCharno() {
Node cur = curNode;
while (cur != null) {
int line = cur.getCharno();
if (line >= 0) {
return line;
}
cur = cur.getParent();
}
return 0;
}
/**
* Gets the current input source name.
*
* @return A string that may be empty, but not null
*/
public String getSourceName() {
return sourceName;
}
/**
* Gets the current input source.
*/
public CompilerInput getInput() {
if (compilerInput == null && inputId != null) {
compilerInput = compiler.getInput(inputId);
}
return compilerInput;
}
/**
* Gets the current input module.
*/
public JSModule getModule() {
CompilerInput input = getInput();
return input == null ? null : input.getModule();
}
/** Returns the node currently being traversed. */
public Node getCurrentNode() {
return curNode;
}
/**
* Traversal for passes that work only on changed functions.
* Suppose a loopable pass P1 uses this traversal.
* Then, if a function doesn't change between two runs of P1, it won't look at
* the function the second time.
* (We're assuming that P1 runs to a fixpoint, o/w we may miss optimizations.)
*
* Most changes are reported with calls to Compiler.reportCodeChange(), which
* doesn't know which scope changed. We keep track of the current scope by
* calling Compiler.setScope inside pushScope and popScope.
* The automatic tracking can be wrong in rare cases when a pass changes scope
* w/out causing a call to pushScope or popScope.
*
* Passes that do cross-scope modifications call
* Compiler.reportChangeToEnclosingScope(Node n).
*/
public static void traverseChangedFunctions(
final AbstractCompiler compiler, final ChangeScopeRootCallback callback) {
final Node jsRoot = compiler.getJsRoot();
NodeTraversal.traverse(compiler, jsRoot,
new AbstractPreOrderCallback() {
@Override
public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (NodeUtil.isChangeScopeRoot(n) && compiler.hasScopeChanged(n)) {
callback.enterChangeScopeRoot(compiler, n);
}
return true;
}
});
}
private void handleScript(Node n, Node parent) {
if (Thread.interrupted()) {
throw new RuntimeException(new InterruptedException());
}
setChangeScope(n);
setInputId(n.getInputId(), getSourceName(n));
curNode = n;
curScript = n;
if (callback.shouldTraverse(this, n, parent)) {
traverseChildren(n);
curNode = n;
callback.visit(this, n, parent);
}
setChangeScope(null);
}
private void handleFunction(Node n, Node parent) {
Node changeScope = this.currentChangeScope;
setChangeScope(n);
curNode = n;
if (callback.shouldTraverse(this, n, parent)) {
traverseFunction(n, parent);
curNode = n;
callback.visit(this, n, parent);
}
setChangeScope(changeScope);
}
/** Traverses a module. */
private void handleModule(Node n, Node parent) {
pushScope(n);
curNode = n;
if (callback.shouldTraverse(this, n, parent)) {
curNode = n;
traverseChildren(n);
callback.visit(this, n, parent);
}
popScope();
}
/** Traverses a branch. */
private void traverseBranch(Node n, 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;
default:
break;
}
curNode = n;
if (!callback.shouldTraverse(this, n, parent)) {
return;
}
if (NodeUtil.createsBlockScope(n)) {
pushScope(n);
traverseChildren(n);
popScope();
} else {
traverseChildren(n);
}
curNode = 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);
}
curNode = 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();
}
/**
* 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.curNode = 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);
for (Node child = body.getFirstChild(); child != null;) {
Node next = child.getNext(); // see traverseChildren
if (child.isComputedProp()) {
traverseBranch(child.getFirstChild(), child);
}
child = next;
}
if (!isClassExpression) {
// Class declarations are in the scope containing the declaration.
traverseBranch(className, n);
}
curNode = n;
pushScope(n);
if (isClassExpression) {
// Class expression names are only accessible within the function
// scope.
traverseBranch(className, n);
}
// Body
traverseBranch(body, n);
popScope();
this.curNode = n;
callback.visit(this, n, parent);
}
/** Traverse class members, excluding keys of computed props. */
private void handleClassMembers(Node n, Node parent) {
this.curNode = n;
if (!callback.shouldTraverse(this, n, parent)) {
return;
}
for (Node child = n.getFirstChild(); child != null;) {
Node next = child.getNext(); // see traverseChildren
if (child.isComputedProp()) {
curNode = n;
if (callback.shouldTraverse(this, child, n)) {
traverseBranch(child.getLastChild(), child);
curNode = n;
callback.visit(this, child, n);
}
} else {
traverseBranch(child, n);
}
child = next;
}
this.curNode = 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 Node getEnclosingFunction() {
Node root = getCfgRoot();
return root.isFunction() ? root : null;
}
/** Sets the given node as the current scope and pushes the relevant frames on the CFG stacks. */
private void recordScopeRoot(Node node) {
if (NodeUtil.isValidCfgRoot(node)) {
cfgs.push(node);
}
}
/** Creates a new scope (e.g. when entering a function). */
private void pushScope(Node node) {
checkNotNull(curNode);
checkNotNull(node);
scopeRoots.add(node);
recordScopeRoot(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(curNode);
scopes.push(s);
recordScopeRoot(s.getRootNode());
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);
}
Node scopeRoot;
int roots = scopeRoots.size();
if (roots > 0) {
scopeRoot = scopeRoots.remove(roots - 1);
} else {
scopeRoot = scopes.pop().getRootNode();
}
if (NodeUtil.isValidCfgRoot(scopeRoot)) {
cfgs.pop();
}
}
/** Gets the current scope. */
public AbstractScope, ?> getAbstractScope() {
AbstractScope, ?> scope = scopes.peek();
// NOTE(dylandavidson): Use for-each loop to avoid slow ArrayList#get performance.
for (Node scopeRoot : scopeRoots) {
scope = scopeCreator.createScope(scopeRoot, scope);
scopes.push(scope);
}
scopeRoots.clear();
return scope;
}
/**
* Instantiate some, but not necessarily all, scopes from stored roots.
*
*
NodeTraversal instantiates scopes lazily when getScope() or similar is called, by iterating
* over a stored list of not-yet-instantiated scopeRoots. When a not-yet-instantiated parent
* scope is requested, it doesn't make sense to instantiate all pending scopes. Instead,
* we count the number that are needed to ensure the requested parent is instantiated and call
* this function to instantiate only as many scopes as are needed, shifting their roots off the
* queue, and returning the deepest scope actually created.
*/
private AbstractScope, ?> instantiateScopes(int count) {
checkArgument(count <= scopeRoots.size());
AbstractScope, ?> scope = scopes.peek();
for (int i = 0; i < count; i++) {
scope = scopeCreator.createScope(scopeRoots.get(i), scope);
scopes.push(scope);
}
scopeRoots.subList(0, count).clear();
return scope;
}
public boolean isHoistScope() {
return isHoistScopeRootNode(getScopeRoot());
}
public Node getClosestHoistScopeRoot() {
int roots = scopeRoots.size();
for (int i = roots; i > 0; i--) {
Node rootNode = scopeRoots.get(i - 1);
if (isHoistScopeRootNode(rootNode)) {
return rootNode;
}
}
return scopes.peek().getClosestHoistScope().getRootNode();
}
public AbstractScope, ?> getClosestContainerScope() {
for (int i = scopeRoots.size(); i > 0; i--) {
if (!NodeUtil.createsBlockScope(scopeRoots.get(i - 1))) {
return instantiateScopes(i);
}
}
return scopes.peek().getClosestContainerScope();
}
public AbstractScope, ?> getClosestHoistScope() {
for (int i = scopeRoots.size(); i > 0; i--) {
if (isHoistScopeRootNode(scopeRoots.get(i - 1))) {
return instantiateScopes(i);
}
}
return scopes.peek().getClosestHoistScope();
}
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 Scope getScope() {
return getAbstractScope().untyped();
}
public TypedScope getTypedScope() {
return getAbstractScope().typed();
}
/** Gets the control flow graph for the current JS scope. */
@SuppressWarnings("unchecked") // The type is always ControlFlowGraph
public ControlFlowGraph getControlFlowGraph() {
ControlFlowGraph result;
Object o = cfgs.peek();
if (o instanceof Node) {
Node cfgRoot = (Node) o;
ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, true);
cfa.process(null, cfgRoot);
result = cfa.getCfg();
cfgs.pop();
cfgs.push(result);
} else {
result = (ControlFlowGraph) o;
}
return result;
}
/** Returns the current scope's root. */
public Node getScopeRoot() {
int roots = scopeRoots.size();
if (roots > 0) {
return scopeRoots.get(roots - 1);
} else {
AbstractScope, ?> s = scopes.peek();
return s != null ? s.getRootNode() : null;
}
}
@SuppressWarnings("unchecked") // The type is always ControlFlowGraph
private Node getCfgRoot() {
Node result;
Object o = cfgs.peek();
if (o instanceof Node) {
result = (Node) o;
} else {
result = ((ControlFlowGraph) o).getEntry().getValue();
}
return result;
}
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 the global scope. Note that this returns false
* in a global block 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() {
Node cfgRoot = getCfgRoot();
checkState(
cfgRoot.isScript()
|| cfgRoot.isRoot()
|| cfgRoot.isBlock()
|| cfgRoot.isFunction()
|| cfgRoot.isModuleBody(),
cfgRoot);
return cfgRoot.isScript() || cfgRoot.isRoot() || cfgRoot.isBlock();
}
/**
* Determines whether the hoist scope of the current traversal is global.
*/
public boolean inModuleHoistScope() {
Node moduleRoot = getCfgRoot();
if (moduleRoot.isFunction()) {
// For wrapped modules, the function block is the module scope root.
moduleRoot = moduleRoot.getLastChild();
}
return NodeUtil.isModuleScopeRoot(moduleRoot);
}
int getScopeDepth() {
int sum = scopes.size() + scopeRoots.size();
checkState(sum > 0);
return sum - 1; // Use 0-based scope depth to be consistent within the compiler
}
/** 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);
}
private static String getSourceName(Node n) {
String name = n.getSourceFileName();
return nullToEmpty(name);
}
/**
* 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 curScript;
}
/**
* @param n The current change scope, should be null when the traversal is complete.
*/
private void setChangeScope(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 (Thread.interrupted()) {
throw new RuntimeException(new InterruptedException());
}
Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(traversalRoot);
setChangeScope(changeScope);
Node script = getEnclosingScript(changeScope);
if (script != null) {
setInputId(script.getInputId(), script.getSourceFileName());
} else {
setInputId(null, "");
}
curScript = script;
}
/**
* Prefills the scopeRoots 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;
}
private void setInputId(InputId id, String sourceName) {
inputId = id;
this.sourceName = sourceName;
compilerInput = null;
}
InputId getInputId() {
return inputId;
}
private String getBestSourceFileName(Node n) {
return n == null ? sourceName : n.getSourceFileName();
}
}