com.google.javascript.jscomp.ReferenceCollectingCallback Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter Show documentation
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.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
/*
* Copyright 2008 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSymbolTable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A helper class for passes that want to access all information about where a variable is
* referenced and declared at once and then make a decision as to how it should be handled, possibly
* inlining, reordering, or generating warnings. Callers do this by providing {@link Behavior} and
* then calling {@link #process(Node, Node)}.
*
* @author [email protected] (Kushal Dave)
*/
public final class ReferenceCollectingCallback
implements ScopedCallback, HotSwapCompilerPass, StaticSymbolTable {
/**
* Maps a given variable to a collection of references to that name. Note that
* Var objects are not stable across multiple traversals (unlike scope root or
* name).
*/
private final Map referenceMap =
new LinkedHashMap<>();
/**
* The stack of basic blocks and scopes the current traversal is in.
*/
private List blockStack = new ArrayList<>();
/**
* Source of behavior at various points in the traversal.
*/
private final Behavior behavior;
private final ScopeCreator scopeCreator;
/**
* JavaScript compiler to use in traversing.
*/
private final AbstractCompiler compiler;
/**
* Only collect references for filtered variables.
*/
private final Predicate varFilter;
/**
* Traverse hoisted functions where they're referenced, not
* where they're declared.
*/
private final Set startedFunctionTraverse = new HashSet<>();
private final Set finishedFunctionTraverse = new HashSet<>();
private Scope narrowScope;
/**
* Constructor initializes block stack.
*/
public ReferenceCollectingCallback(AbstractCompiler compiler, Behavior behavior,
ScopeCreator creator) {
this(compiler, behavior, creator, Predicates.alwaysTrue());
}
/**
* Constructor only collects references that match the given variable.
*
* The test for Var equality uses reference equality, so it's necessary to
* inject a scope when you traverse.
*/
ReferenceCollectingCallback(AbstractCompiler compiler, Behavior behavior, ScopeCreator creator,
Predicate varFilter) {
this.compiler = compiler;
this.behavior = behavior;
this.scopeCreator = creator;
this.varFilter = varFilter;
}
/**
* Convenience method for running this pass over a tree with this
* class as a callback.
*/
@Override
public void process(Node externs, Node root) {
NodeTraversal t = new NodeTraversal(compiler, this, scopeCreator);
t.traverseRoots(externs, root);
}
public void process(Node root) {
NodeTraversal t = new NodeTraversal(compiler, this, scopeCreator);
t.traverse(root);
}
/**
* Targets reference collection to a particular scope.
*/
void processScope(Scope scope) {
boolean shouldAddToBlockStack = !scope.isHoistScope();
this.narrowScope = scope;
if (shouldAddToBlockStack) {
blockStack.add(new BasicBlock(null, scope.getRootNode()));
}
(new NodeTraversal(compiler, this, scopeCreator)).traverseAtScope(scope);
if (shouldAddToBlockStack) {
pop(blockStack);
}
this.narrowScope = null;
}
/**
* Same as process but only runs on a part of AST associated to one script.
*/
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
NodeTraversal.traverse(compiler, scriptRoot, this);
}
/**
* Gets the variables that were referenced in this callback.
*/
@Override
public Iterable getAllSymbols() {
return referenceMap.keySet();
}
@Override
public Scope getScope(Var var) {
return var.scope;
}
/**
* Gets the reference collection for the given variable.
*/
@Override
public ReferenceCollection getReferences(Var v) {
return referenceMap.get(v);
}
/**
* For each node, update the block stack and reference collection
* as appropriate.
*/
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName() || n.isImportStar() || (n.isStringKey() && !n.hasChildren())) {
if ((parent.isImportSpec() && n != parent.getLastChild())
|| (parent.isExportSpec() && n != parent.getFirstChild())) {
// The n in `import {n as x}` or `export {x as n}` are not references, even though
// they are represented in the AST as NAME nodes.
return;
}
Var v = t.getScope().getVar(n.getString());
if (v != null) {
if (varFilter.apply(v)) {
addReference(v, new Reference(n, t, peek(blockStack)));
}
if (v.getParentNode() != null
&& NodeUtil.isHoistedFunctionDeclaration(v.getParentNode())
// If we're only traversing a narrow scope, do not try to climb outside.
&& (narrowScope == null || narrowScope.getDepth() <= v.getScope().getDepth())) {
outOfBandTraversal(v);
}
}
}
if (isBlockBoundary(n, parent)) {
pop(blockStack);
}
}
private void outOfBandTraversal(Var v) {
if (startedFunctionTraverse.contains(v)) {
return;
}
startedFunctionTraverse.add(v);
Node fnNode = v.getParentNode();
// Replace the block stack with a new one. This algorithm only works
// because we know hoisted functions cannot be inside loops. It will have to
// change if we ever do general function continuations.
checkState(NodeUtil.isHoistedFunctionDeclaration(fnNode), fnNode);
Scope containingScope = v.getScope();
// This is tricky to compute because of the weird traverseAtScope call for
// AggressiveInlineAliases.
List newBlockStack = null;
if (containingScope.isGlobal()) {
newBlockStack = new ArrayList<>();
newBlockStack.add(blockStack.get(0));
} else {
for (int i = 0; i < blockStack.size(); i++) {
if (blockStack.get(i).getRoot() == containingScope.getRootNode()) {
newBlockStack = new ArrayList<>(blockStack.subList(0, i + 1));
}
}
}
checkNotNull(newBlockStack);
List oldBlockStack = blockStack;
blockStack = newBlockStack;
NodeTraversal outOfBandTraversal = new NodeTraversal(compiler, this, scopeCreator);
outOfBandTraversal.traverseFunctionOutOfBand(fnNode, containingScope);
blockStack = oldBlockStack;
finishedFunctionTraverse.add(v);
}
/**
* Updates block stack and invokes any additional behavior.
*/
@Override
public void enterScope(NodeTraversal t) {
Node n = t.getScopeRoot();
BasicBlock parent = blockStack.isEmpty() ? null : peek(blockStack);
// Don't add all ES6 scope roots to blockStack, only those that are also scopes according to
// the ES5 scoping rules. Other nodes that ought to be considered the root of a BasicBlock
// are added in shouldTraverse() or processScope() and removed in visit().
if (t.isHoistScope()) {
blockStack.add(new BasicBlock(parent, n));
}
}
/**
* Updates block stack and invokes any additional behavior.
*/
@Override
public void exitScope(NodeTraversal t) {
if (t.isHoistScope()) {
pop(blockStack);
}
behavior.afterExitScope(t, new ReferenceMapWrapper(referenceMap));
}
/**
* Updates block stack.
*/
@Override
public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
// We automatically traverse a hoisted function body when that function
// is first referenced, so that the reference lists are in the right order.
//
// TODO(nicksantos): Maybe generalize this to a continuation mechanism
// like in RemoveUnusedCode.
if (NodeUtil.isHoistedFunctionDeclaration(n)) {
Node nameNode = n.getFirstChild();
Var functionVar = nodeTraversal.getScope().getVar(nameNode.getString());
checkNotNull(functionVar);
if (finishedFunctionTraverse.contains(functionVar)) {
return false;
}
startedFunctionTraverse.add(functionVar);
}
// If node is a new basic block, put on basic block stack
if (isBlockBoundary(n, parent)) {
blockStack.add(new BasicBlock(peek(blockStack), n));
}
// Add the second x before the first one in "let [x] = x;". VariableReferenceCheck
// relies on reference order to give a warning.
if ((n.isDefaultValue() || n.isDestructuringLhs()) && n.hasTwoChildren()) {
Scope scope = nodeTraversal.getScope();
nodeTraversal.traverseInnerNode(n.getSecondChild(), n, scope);
nodeTraversal.traverseInnerNode(n.getFirstChild(), n, scope);
return false;
}
return true;
}
private static T pop(List list) {
return list.remove(list.size() - 1);
}
private static T peek(List list) {
return Iterables.getLast(list);
}
/**
* @return true if this node marks the start of a new basic block
*/
private static boolean isBlockBoundary(Node n, Node parent) {
if (parent != null) {
switch (parent.getToken()) {
case DO:
case FOR:
case FOR_IN:
case FOR_OF:
case FOR_AWAIT_OF:
case TRY:
case WHILE:
case WITH:
case CLASS:
// NOTE: TRY has up to 3 child blocks:
// TRY
// BLOCK
// BLOCK
// CATCH
// BLOCK
// Note that there is an explicit CATCH token but no explicit
// FINALLY token. For simplicity, we consider each BLOCK
// a separate basic BLOCK.
return true;
case AND:
case HOOK:
case IF:
case OR:
case SWITCH:
// The first child of a conditional is not a boundary,
// but all the rest of the children are.
return n != parent.getFirstChild();
default:
break;
}
}
return n.isCase();
}
private void addReference(Var v, Reference reference) {
// Create collection if none already
ReferenceCollection referenceInfo =
referenceMap.computeIfAbsent(v, k -> new ReferenceCollection());
// Add this particular reference
referenceInfo.add(reference);
}
static class ReferenceMapWrapper implements ReferenceMap {
private final Map referenceMap;
public ReferenceMapWrapper(Map referenceMap) {
this.referenceMap = referenceMap;
}
@Override
public ReferenceCollection getReferences(Var var) {
return referenceMap.get(var);
}
Map getRawReferenceMap() {
return referenceMap;
}
@Override
public String toString() {
return referenceMap.toString();
}
}
/**
* Way for callers to add specific behavior during traversal that
* utilizes the built-up reference information.
*/
public interface Behavior {
/**
* Called after we finish with a scope.
*/
void afterExitScope(NodeTraversal t, ReferenceMap referenceMap);
}
static final Behavior DO_NOTHING_BEHAVIOR = new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap referenceMap) {}
};
}