com.google.javascript.jscomp.NameAnalyzer Maven / Gradle / Ivy
Show all versions of closure-compiler Show documentation
/*
* Copyright 2006 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 java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Ordering;
import com.google.common.io.Files;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.GatherSideEffectSubexpressionsCallback.GetReplacementSideEffectSubexpressions;
import com.google.javascript.jscomp.GatherSideEffectSubexpressionsCallback.SideEffectAccumulator;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.jscomp.graph.LinkedDirectedGraph;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.io.File;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* This pass identifies all global names, simple (e.g. a
) or
* qualified (e.g. a.b.c
), and the dependencies between them, then
* removes code associated with unreferenced names. It starts by assuming that
* only externally accessible names (e.g. window
) are referenced,
* then iteratively marks additional names as referenced (e.g. Foo
* in window['foo'] = new Foo();
). This makes it possible to
* eliminate code containing circular references.
*
* Qualified names can be defined using dotted or object literal syntax
* (a.b.c = x;
or a.b = {c: x};
, respectively).
*
*
Removal of prototype classes is currently all or nothing. In other words,
* prototype properties and methods are never individually removed.
*
*
Optionally generates pretty HTML output of data so that it is easy to
* analyze dependencies.
*
*
Only operates on names defined in the global scope, but it would be easy
* to extend the pass to names defined in local scopes.
*
* TODO(nicksantos): In the initial implementation of this pass, it was
* important to understand namespaced names (e.g., that a.b is distinct from
* a.b.c). Now that this pass comes after CollapseProperties, this is no longer
* necessary. For now, I've changed so that {@code referenceParentNames}
* creates a two-way reference between a.b and a.b.c, so that they're
* effectively the same name. When someone has the time, we should completely
* rip out all the logic that understands namespaces.
*
*/
final class NameAnalyzer implements CompilerPass {
/** Reference to the JS compiler */
private final AbstractCompiler compiler;
/** Map of all JS names found */
private final Map allNames = new HashMap<>();
/** Reference dependency graph */
private LinkedDirectedGraph referenceGraph =
LinkedDirectedGraph.createWithoutAnnotations();
/**
* Map of name scopes - all children of the Node key have a dependency on the
* name value.
*
* If scopes.get(node).equals(name) && node2 is a child of node, then node2
* will not get executed unless name is referenced via a get operation
*/
private final ListMultimap scopes =
LinkedListMultimap.create();
/** Used to parse prototype names */
private static final String PROTOTYPE_SUBSTRING = ".prototype.";
private static final int PROTOTYPE_SUBSTRING_LEN =
PROTOTYPE_SUBSTRING.length();
private static final int PROTOTYPE_SUFFIX_LEN = ".prototype".length();
/** Window root */
private static final String WINDOW = "window";
/** Function class name */
private static final String FUNCTION = "Function";
/** All of these refer to global scope. These can be moved to config */
static final Set DEFAULT_GLOBAL_NAMES = ImmutableSet.of(
"window", "goog.global");
/** Whether to remove unreferenced variables in main pass */
private final boolean removeUnreferenced;
/** The path of the report file */
private final String reportPath;
/** Names that refer to the global scope */
private final Set globalNames;
/** Ast change helper */
private final AstChangeProxy changeProxy;
/** Names that are externally defined */
private final Set externalNames = new HashSet<>();
/** Name declarations or assignments, in post-order traversal order */
private final List refNodes = new ArrayList<>();
/**
* When multiple names in the global scope point to the same object, we
* call them aliases. Store a map from each alias name to the alias set.
*/
private final Map aliases = new HashMap<>();
static final DiagnosticType REPORT_PATH_IO_ERROR =
DiagnosticType.error("JSC_REPORT_PATH_IO_ERROR",
"Error writing compiler report to {0}:\n{1}");
/**
* All the aliases in a program form a graph, where each global name is
* a node in the graph, and two names are connected if one directly aliases
* the other.
*
* An {@code AliasSet} represents a connected component in that graph. We do
* not explicitly track the graph--we just track the connected components.
*/
private static class AliasSet {
Set names = new HashSet<>();
// Every alias set starts with exactly 2 names.
AliasSet(String name1, String name2) {
names.add(name1);
names.add(name2);
}
}
/**
* Relationship between the two names.
* Currently only two different reference types exists:
* goog.inherits class relations and all other references.
*/
private static enum RefType {
REGULAR,
INHERITANCE,
}
/**
* Class to hold information that can be determined from a node tree about a
* given name
*/
private static class NameInformation {
/** Fully qualified name */
String name;
/** Whether the name is guaranteed to be externally referenceable */
boolean isExternallyReferenceable = false;
/** Whether this name is a prototype function */
boolean isPrototype = false;
/** Name of the prototype class, i.e. "a" if name is "a.prototype.b" */
@Nullable String prototypeClass = null;
/** Local name of prototype property i.e. "b" if name is "a.prototype.b" */
@Nullable String prototypeProperty = null;
/** Name of the super class of name */
@Nullable String superclass = null;
/** Whether this is a call that only affects the class definition */
boolean onlyAffectsClassDef = false;
@Override
public String toString() {
return "NameInformation:" + name;
}
}
/**
* Struct to hold information about a fully qualified JS name
*/
private static class JsName implements Comparable {
/** Fully qualified name */
String name;
/** Name of prototype functions attached to this name */
List prototypeNames = new ArrayList<>();
/** Whether this is an externally defined name */
boolean externallyDefined = false;
/** Whether this node is referenced */
boolean referenced = false;
/** Whether the name has descendants that are written to. */
boolean hasWrittenDescendants = false;
/** Whether the name is used in a instanceof check */
boolean hasInstanceOfReference = false;
/** Whether the name is directly set */
boolean hasSetterReference = false;
/**
* Output the node as a string
*
* @return Node as a string
*/
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append(name);
if (!prototypeNames.isEmpty()) {
out.append(" (CLASS)\n");
out.append(" - FUNCTIONS: ");
Iterator pIter = prototypeNames.iterator();
while (pIter.hasNext()) {
out.append(pIter.next());
if (pIter.hasNext()) {
out.append(", ");
}
}
}
return out.toString();
}
@Override
public int compareTo(JsName rhs) {
return this.name.compareTo(rhs.name);
}
}
/**
* Interface to get information about and remove unreferenced names.
*/
interface RefNode {
JsName name();
void remove();
}
/**
* Class for nodes that reference a fully-qualified JS name. Fully qualified
* names are of form A or A.B (A.B.C, etc.). References can get the value or
* set the value of the JS name.
*/
private class JsNameRefNode implements RefNode {
/** JsName node for this reference */
JsName name;
/**
* Parent node of the name access
* (ASSIGN, VAR, FUNCTION, OBJECTLIT, or CALL)
*/
Node parent;
/**
* Create a node that refers to a name
*
* @param name The name
* @param node The top node representing the name (GETPROP, NAME, STRING)
*/
JsNameRefNode(JsName name, Node node) {
this.name = name;
this.parent = node.getParent();
}
@Override
public JsName name() {
return name;
}
@Override
public void remove() {
// Setters have VAR, FUNCTION, or ASSIGN parent nodes. CALL parent
// nodes are global refs, and are handled later in this function.
Node containingNode = parent.getParent();
switch (parent.getToken()) {
case VAR:
checkState(parent.hasOneChild());
replaceWithRhs(containingNode, parent);
break;
case FUNCTION:
replaceWithRhs(containingNode, parent);
break;
case ASSIGN:
if (containingNode.isExprResult()) {
replaceWithRhs(containingNode.getParent(), containingNode);
} else {
replaceWithRhs(containingNode, parent);
}
break;
case OBJECTLIT:
// TODO(nicksantos): Come up with a way to remove this.
// If we remove object lit keys, then we will need to also
// create dependency scopes for them.
break;
case EXPR_RESULT:
checkState(isAnalyzableObjectDefinePropertiesDefinition(parent.getFirstChild()));
replaceWithRhs(containingNode, parent);
break;
default:
throw new IllegalArgumentException(
"Unsupported parent node type in JsNameRefNode.remove: " + parent.getToken());
}
}
}
/**
* Class for nodes that set prototype properties or methods.
*/
private class PrototypeSetNode extends JsNameRefNode {
/**
* Create a set node from the name & setter node
*
* @param name The name
* @param parent Parent node that assigns the expression (an ASSIGN)
*/
PrototypeSetNode(JsName name, Node parent) {
super(name, parent.getFirstChild());
checkState(parent.isAssign());
}
@Override public void remove() {
Node grandparent = parent.getParent();
if (grandparent.isExprResult()) {
// name.prototype.foo = function() { ... };
changeProxy.removeChild(grandparent.getParent(), grandparent);
} else {
// ... name.prototype.foo = function() { ... } ...
changeProxy.replaceWith(grandparent, parent,
parent.getLastChild().detach());
}
}
}
/**
* Base class for special reference nodes.
*/
private abstract static class SpecialReferenceNode implements RefNode {
/** JsName node for the function */
JsName name;
/** The CALL node */
Node node;
/**
* Create a special reference node.
*
* @param name The name
* @param node The CALL node
*/
SpecialReferenceNode(JsName name, Node node) {
this.name = name;
this.node = node;
}
@Override
public JsName name() {
return name;
}
Node getParent() {
return node.getParent();
}
Node getGrandparent() {
return node.getGrandparent();
}
}
/**
* Class for nodes that are function calls that may change a function's
* prototype
*/
private class ClassDefiningFunctionNode extends SpecialReferenceNode {
/**
* Create a class defining function node from the name & setter node
*
* @param name The name
* @param node The CALL node
*/
ClassDefiningFunctionNode(JsName name, Node node) {
super(name, node);
checkState(node.isCall());
}
@Override
public void remove() {
checkState(node.isCall());
Node parent = getParent();
if (parent.isExprResult()) {
changeProxy.removeChild(getGrandparent(), parent);
} else {
changeProxy.replaceWith(parent, node, IR.voidNode(IR.number(0)));
}
}
}
/**
* Class for nodes that check instanceof
*/
private class InstanceOfCheckNode extends SpecialReferenceNode {
/**
* Create an instanceof node from the name and parent node
*
* @param name The name
* @param node The qualified name node
*/
InstanceOfCheckNode(JsName name, Node node) {
super(name, node);
checkState(node.isQualifiedName());
checkState(getParent().isInstanceOf());
}
@Override
public void remove() {
changeProxy.replaceWith(getGrandparent(), getParent(), IR.falseNode());
}
}
/**
* Walk through externs and mark nodes as externally declared if declared
*/
private class ProcessExternals extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
NameInformation ns = null;
if (NodeUtil.isVarDeclaration(n)) {
ns = createNameInformation(t, n);
} else if (NodeUtil.isFunctionDeclaration(n)) {
ns = createNameInformation(t, n.getFirstChild());
}
if (ns != null) {
JsName jsName = getName(ns.name, true);
jsName.externallyDefined = true;
externalNames.add(ns.name);
}
}
}
/**
* Identifies all dependency scopes.
*
*
A dependency scope is a relationship between a node tree and a name that
* implies that the node tree will not execute (and thus can be eliminated) if
* the name is never referenced.
*
*
The entire parse tree is ultimately in a dependency scope relationship
* with window
(or an equivalent name for the global scope), but
* the goal here is to find finer-grained relationships. This callback creates
* dependency scopes for every assignment statement, variable declaration, and
* function call in the global scope.
*
*
Note that dependency scope node trees aren't necessarily disjoint.
* In the following code snippet, for example, the function definition
* forms a dependency scope with the name f
and the assignment
* inside the function forms a dependency scope with the name x
.
*
* var x; function f() { x = 1; }
*
*/
private class FindDependencyScopes extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!t.inGlobalHoistScope()) {
return;
}
if (n.isAssign()) {
recordAssignment(t, n, n);
if (!NodeUtil.isImmutableResult(n.getLastChild())) {
recordConsumers(t, n, n);
}
} else if (NodeUtil.isVarDeclaration(n)) {
NameInformation ns = createNameInformation(t, n);
checkNotNull(ns, "createNameInformation returned null for: %s", n);
recordDepScope(n, ns);
} else if (NodeUtil.isFunctionDeclaration(n) && t.inGlobalScope()) {
NameInformation ns = createNameInformation(t, n.getFirstChild());
checkNotNull(ns, "createNameInformation returned null for: %s", n.getFirstChild());
recordDepScope(n, ns);
} else if (NodeUtil.isExprCall(n)) {
Node callNode = n.getFirstChild();
Node nameNode = callNode.getFirstChild();
NameInformation ns = createNameInformation(t, nameNode);
if (ns != null && ns.onlyAffectsClassDef) {
recordDepScope(n, ns);
}
} else if (isAnalyzableObjectDefinePropertiesDefinition(n)) {
Node targetObject = n.getSecondChild();
NameInformation ns = createNameInformation(t, targetObject);
checkNotNull(ns, "createNameInformation returned null for: %s", targetObject);
recordDepScope(n, ns);
}
}
private void recordConsumers(NodeTraversal t, Node n, Node recordNode) {
Node parent = n.getParent();
switch (parent.getToken()) {
case ASSIGN:
if (n == parent.getLastChild()) {
recordAssignment(t, parent, recordNode);
}
recordConsumers(t, parent, recordNode);
break;
case NAME:
NameInformation ns = createNameInformation(t, parent);
checkNotNull(ns, "createNameInformation returned null for: %s", parent);
recordDepScope(recordNode, ns);
break;
case OR:
recordConsumers(t, parent, recordNode);
break;
case AND:
// In "a && b" only "b" can be meaningfully aliased.
// "a" must be falsy, which it must be an immutable, non-Object
case COMMA:
case HOOK:
if (n != parent.getFirstChild()) {
recordConsumers(t, parent, recordNode);
}
break;
default:
break;
}
}
private void recordAssignment(NodeTraversal t, Node n, Node recordNode) {
Node nameNode = n.getFirstChild();
Node parent = n.getParent();
NameInformation ns = createNameInformation(t, nameNode);
if (ns != null) {
if (parent.isVanillaFor()) {
// Patch for assignments that appear in the init,
// condition or iteration part of a FOR loop. Without
// this change, all 3 of those parts try to claim the for
// loop as their dependency scope. The last assignment in
// those three fields wins, which can result in incorrect
// reference edges between referenced and assigned variables.
//
// TODO(user) revisit the dependency scope calculation
// logic.
if (parent.getSecondChild() != n) {
recordDepScope(recordNode, ns);
} else {
recordDepScope(nameNode, ns);
}
} else if (!parent.isCall() || n != parent.getFirstChild()) {
// The rhs of the assignment is the caller, so it's used by the
// context. Don't associate it w/ the lhs.
// FYI: this fixes only the specific case where the assignment is the
// caller expression, but it could be nested deeper in the caller and
// we would still get a bug.
// See testAssignWithCall2 for an example of this.
recordDepScope(recordNode, ns);
}
}
}
/**
* Defines a dependency scope.
*/
private void recordDepScope(Node node, NameInformation name) {
checkNotNull(name);
scopes.put(node, name);
}
}
/**
* Create JsName objects for variable and function declarations in
* the global scope before computing name references. In JavaScript
* it is legal to refer to variable and function names before the
* actual declaration.
*/
private class HoistVariableAndFunctionDeclarations
extends NodeTraversal.AbstractShallowCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (NodeUtil.isVarDeclaration(n)) {
NameInformation ns = createNameInformation(t, n);
checkNotNull(ns, "createNameInformation returned null for: %s", n);
createName(ns.name);
} else if (NodeUtil.isFunctionDeclaration(n)) {
Node nameNode = n.getFirstChild();
NameInformation ns = createNameInformation(t, nameNode);
checkNotNull(ns, "createNameInformation returned null for: %s", nameNode);
createName(nameNode.getString());
}
}
}
/**
* Identifies all declarations of global names and setter statements
* affecting global symbols (assignments to global names).
*
* All declarations and setters must be gathered in a single
* traversal and stored in traversal order so "removeUnreferenced"
* can perform modifications in traversal order.
*/
private class FindDeclarationsAndSetters extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// Record global variable and function declarations
if (t.inGlobalHoistScope()) {
if (NodeUtil.isVarDeclaration(n)) {
NameInformation ns = createNameInformation(t, n);
checkNotNull(ns, "createNameInformation returned null for: %s", n);
recordSet(ns.name, n);
} else if (NodeUtil.isFunctionDeclaration(n) && t.inGlobalScope()) {
Node nameNode = n.getFirstChild();
NameInformation ns = createNameInformation(t, nameNode);
if (ns != null) {
JsName nameInfo = getName(nameNode.getString(), true);
recordSet(nameInfo.name, nameNode);
}
} else if (NodeUtil.isObjectLitKey(n)) {
NameInformation ns = createNameInformation(t, n);
if (ns != null) {
recordSet(ns.name, n);
}
}
}
// Record assignments and call sites
if (n.isAssign() || isAnalyzableObjectDefinePropertiesDefinition(n)) {
Node nameNode = n.isAssign() ? n.getFirstChild() : n;
NameInformation ns = createNameInformation(t, nameNode);
if (ns != null) {
if (ns.isPrototype) {
recordPrototypeSet(ns.prototypeClass, ns.prototypeProperty, n);
} else {
recordSet(ns.name, nameNode);
}
}
} else if (n.isCall()) {
Node nameNode = n.getFirstChild();
NameInformation ns = createNameInformation(t, nameNode);
if (ns != null && ns.onlyAffectsClassDef) {
JsName name = getName(ns.name, true);
refNodes.add(new ClassDefiningFunctionNode(name, n));
}
}
}
/**
* Records the assignment of a value to a global name.
*
* @param name Fully qualified name
* @param node The top node representing the name (GETPROP, NAME, STRING [objlit key],
* or CALL [Object.defineProperties])
*/
private void recordSet(String name, Node node) {
JsName jsn = getName(name, true);
JsNameRefNode nameRefNode = new JsNameRefNode(jsn, node);
refNodes.add(nameRefNode);
jsn.hasSetterReference = true;
// Now, look at all parent names and record that their properties have
// been written to.
if (node.isGetElem() || isAnalyzableObjectDefinePropertiesDefinition(node)) {
recordWriteOnProperties(name);
} else if (name.indexOf('.') != -1) {
recordWriteOnProperties(name.substring(0, name.lastIndexOf('.')));
}
}
/**
* Records the assignment to a prototype property of a global name,
* if possible.
*
* @param className The name of the class.
* @param prototypeProperty The name of the prototype property.
* @param node The top node representing the name (GETPROP)
*/
private void recordPrototypeSet(String className, String prototypeProperty,
Node node) {
JsName name = getName(className, true);
name.prototypeNames.add(prototypeProperty);
refNodes.add(new PrototypeSetNode(name, node));
recordWriteOnProperties(className);
}
/**
* Record that the properties of this name have been written to.
*/
private void recordWriteOnProperties(String parentName) {
do {
JsName parent = getName(parentName, true);
if (parent.hasWrittenDescendants) {
// If we already recorded this name, then all its parents must
// also be recorded. short-circuit this loop.
return;
} else {
parent.hasWrittenDescendants = true;
}
if (parentName.indexOf('.') == -1) {
return;
}
parentName = parentName.substring(0, parentName.lastIndexOf('.'));
} while(true);
}
}
private static final Predicate NON_LOCAL_RESULT_PREDICATE =
new Predicate() {
@Override
public boolean apply(Node input) {
if (input.isCall()) {
return false;
}
// TODO(johnlenz): handle NEW calls that record their 'this'
// in global scope and effectively return an alias.
// Other non-local references are handled by this pass.
return true;
}
};
/**
* Identifies all references between global names.
*
*
A reference from a name f
to a name g
means
* that if the name f
must be defined, then the name
* g
must also be defined. This would be the case if, for
* example, f
were a function that called g
.
*/
private class FindReferences implements Callback {
Set nodesToKeep;
FindReferences() {
nodesToKeep = new HashSet<>();
}
private void addAllChildren(Node n) {
nodesToKeep.add(n);
for (Node child = n.getFirstChild();
child != null;
child = child.getNext()) {
addAllChildren(child);
}
}
private void addSimplifiedChildren(Node n) {
NodeTraversal.traverseEs6(
compiler, n, new GatherSideEffectSubexpressionsCallback(compiler, new NodeAccumulator()));
}
private void addSimplifiedExpression(Node n, Node parent) {
if (parent.isVar()) {
Node value = n.getFirstChild();
if (value != null) {
addSimplifiedChildren(value);
}
} else if (n.isAssign()
&& (parent.isExprResult()
|| parent.isVanillaFor()
|| parent.isReturn())) {
for (Node child : n.children()) {
addSimplifiedChildren(child);
}
} else if (isAnalyzableObjectDefinePropertiesDefinition(n)) {
addSimplifiedChildren(n.getLastChild());
} else if (n.isCall() && parent.isExprResult()) {
addSimplifiedChildren(n);
} else {
addAllChildren(n);
}
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (parent == null) {
return true;
}
// Gather the list of nodes that either have side effects, are
// arguments to function calls with side effects or are used in
// control structure predicates. These names are always
// referenced when the enclosing function is called.
if (n.isVanillaFor()) {
Node decl = n.getFirstChild();
Node pred = decl.getNext();
Node step = pred.getNext();
addSimplifiedExpression(decl, n);
addSimplifiedExpression(pred, n);
addSimplifiedExpression(step, n);
} else if (n.isForIn()) {
Node decl = n.getFirstChild();
Node iter = decl.getNext();
addAllChildren(decl);
addAllChildren(iter);
}
if (parent.isVar() ||
parent.isExprResult() ||
parent.isReturn() ||
parent.isThrow()) {
addSimplifiedExpression(n, parent);
}
if ((parent.isIf() ||
parent.isWhile() ||
parent.isWith() ||
parent.isSwitch() ||
parent.isCase()) &&
parent.getFirstChild() == n) {
addAllChildren(n);
}
if (parent.isDo() && parent.getLastChild() == n) {
addAllChildren(n);
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isName() && (!NodeUtil.isGet(n) || parent.isGetProp())) {
// This is not a simple or qualified name.
return;
}
NameInformation nameInfo = createNameInformation(t, n);
if (nameInfo == null) {
// The name is not a global name
return;
}
if (nameInfo.onlyAffectsClassDef) {
if (nameInfo.superclass != null) {
recordReference(
nameInfo.name, nameInfo.superclass, RefType.INHERITANCE);
}
// Make sure that we record a reference to the function that does
// the inheritance, so that the inherits() function itself does
// not get stripped.
String nodeName = n.getQualifiedName();
if (nodeName != null) {
recordReference(
nameInfo.name, nodeName, RefType.REGULAR);
}
return;
}
// instanceof checks are not handled like regular read references.
boolean isInstanceOfCheck = parent.isInstanceOf() &&
parent.getLastChild() == n;
if (isInstanceOfCheck) {
JsName checkedClass = getName(nameInfo.name, true);
// If we know where this constructor is created, and we
// know we can find all 'new' calls on it, then treat
// this as a special reference. It will be replaced with
// false if there are no other references, because we
// know the class can't be instantiated.
if (checkedClass.hasSetterReference &&
!nameInfo.isExternallyReferenceable &&
// Exclude GETELEMs.
n.isQualifiedName()) {
refNodes.add(new InstanceOfCheckNode(checkedClass, n));
checkedClass.hasInstanceOfReference = true;
return;
}
}
// Determine which name might be potentially referring to this one by
// looking up the nearest enclosing dependency scope. It's unnecessary to
// determine all enclosing dependency scopes because this callback should
// create a chain of references between them.
List referers = getDependencyScope(n);
if (referers.isEmpty()) {
maybeRecordReferenceOrAlias(t, n, parent, nameInfo, null);
} else {
for (NameInformation referring : referers) {
maybeRecordReferenceOrAlias(t, n, parent, nameInfo, referring);
}
recordAliases(referers);
}
}
private void maybeRecordReferenceOrAlias(
NodeTraversal t, Node n, Node parent,
NameInformation nameInfo, @Nullable NameInformation referring) {
String referringName = "";
if (referring != null) {
referringName = referring.isPrototype
? referring.prototypeClass
: referring.name;
}
String name = nameInfo.name;
// A value whose result is the return value of a function call
// can be an alias to global object.
// Here we add an alias to the general "global" object
// to act as a placeholder for the actual (unnamed) value.
if (maybeHiddenAlias(n)) {
recordAlias(name, WINDOW);
}
// An externally referenceable name must always be defined, so we add a
// reference to it from the global scope (a.k.a. window).
if (nameInfo.isExternallyReferenceable) {
recordReference(WINDOW, name, RefType.REGULAR);
maybeRecordAlias(name, parent, referring, referringName);
return;
}
// An assignment implies a reference from the enclosing dependency scope.
// For example, foo references bar in: function foo() {bar=5}.
if (NodeUtil.isVarOrSimpleAssignLhs(n, parent)) {
if (referring != null) {
recordReference(referringName, name, RefType.REGULAR);
}
return;
}
if (nodesToKeep.contains(n)) {
List functionScopes =
getEnclosingFunctionDependencyScope(t);
if (!functionScopes.isEmpty()) {
for (NameInformation functionScope : functionScopes) {
recordReference(functionScope.name, name, RefType.REGULAR);
}
} else {
recordReference(WINDOW, name, RefType.REGULAR);
if (referring != null) {
maybeRecordAlias(name, parent, referring, referringName);
}
}
} else if (referring != null) {
if (!maybeRecordAlias(name, parent, referring, referringName)) {
RefType depType = referring.onlyAffectsClassDef ?
RefType.INHERITANCE : RefType.REGULAR;
recordReference(referringName, name, depType);
}
} else {
// No named dependency scope found. Unfortunately that might
// mean that the expression is a child of an function expression
// or assignment with a complex lhs. In those cases,
// protect this node by creating a reference to WINDOW.
for (Node ancestor : n.getAncestors()) {
if (NodeUtil.isAssignmentOp(ancestor) ||
ancestor.isFunction()) {
recordReference(WINDOW, name, RefType.REGULAR);
break;
}
}
}
}
private void recordAliases(List referers) {
int size = referers.size();
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size; j++) {
recordAlias(referers.get(i).name, referers.get(j).name);
recordAlias(referers.get(j).name, referers.get(i).name);
}
}
}
/**
* A value whose result is the return value of a function call
* can be an alias to global object. The dependency on the call target will
* prevent the removal of the function and its dependent values, but won't
* prevent the alias' removal.
*/
private boolean maybeHiddenAlias(Node n) {
Node parent = n.getParent();
if (NodeUtil.isVarOrSimpleAssignLhs(n, parent)) {
Node rhs = (parent.isVar())
? n.getFirstChild() : parent.getLastChild();
return (rhs != null && !NodeUtil.evaluatesToLocalValue(
rhs, NON_LOCAL_RESULT_PREDICATE));
}
return false;
}
/**
* @return Whether the alias was recorded.
*/
private boolean maybeRecordAlias(
String name, Node parent,
@Nullable NameInformation referring, String referringName) {
// A common type of reference is
// function F() {}
// F.prototype.bar = goog.nullFunction;
//
// In this specific case, we do not want a reference to goog.nullFunction
// to preserve F.
//
// In the general case, the user could do something like
// function F() {}
// F.prototype.bar = goog.nullFunction;
// F.prototype.bar.baz = 3;
// where it would not be safe to remove F.
//
// So we do not treat this alias as a backdoor for people to mutate the
// original object. We think that this heuristic will always be
// OK in real code.
boolean isPrototypePropAssignment =
parent.isAssign()
&& NodeUtil.isPrototypeProperty(parent.getFirstChild());
if ((parent.isName() || parent.isAssign()) && !isPrototypePropAssignment && referring != null
&& scopes.containsEntry(parent, referring)) {
recordAlias(referringName, name);
return true;
}
return false;
}
/**
* Helper class that gathers the list of nodes that would be left
* behind after simplification.
*/
private class NodeAccumulator
implements SideEffectAccumulator {
@Override
public boolean classDefiningCallsHaveSideEffects() {
return false;
}
@Override
public void keepSubTree(Node original) {
addAllChildren(original);
}
@Override
public void keepSimplifiedShortCircuitExpression(Node original) {
Node condition = original.getFirstChild();
Node thenBranch = condition.getNext();
addAllChildren(condition);
addSimplifiedChildren(thenBranch);
}
@Override
public void keepSimplifiedHookExpression(Node hook,
boolean thenHasSideEffects,
boolean elseHasSideEffects) {
Node condition = hook.getFirstChild();
Node thenBranch = condition.getNext();
Node elseBranch = thenBranch.getNext();
addAllChildren(condition);
if (thenHasSideEffects) {
addSimplifiedChildren(thenBranch);
}
if (elseHasSideEffects) {
addSimplifiedChildren(elseBranch);
}
}
}
}
private class RemoveListener implements AstChangeProxy.ChangeListener {
@Override
public void nodeRemoved(Node n) {
compiler.reportCodeChange();
}
}
/**
* Creates a name analyzer, with option to remove unreferenced variables when
* calling process().
*
* The analyzer make a best guess at whether functions affect global scope
* based on usage (no assignment of return value means that a function has
* side effects).
*
* @param compiler The AbstractCompiler
* @param removeUnreferenced If true, remove unreferenced variables during
* process()
*/
NameAnalyzer(
AbstractCompiler compiler,
boolean removeUnreferenced,
String reportPath) {
this.compiler = compiler;
this.removeUnreferenced = removeUnreferenced;
this.reportPath = reportPath;
this.globalNames = DEFAULT_GLOBAL_NAMES;
this.changeProxy = new AstChangeProxy();
}
static void createEmptyReport(AbstractCompiler compiler, String reportPath) {
checkNotNull(reportPath);
try {
Files.write("", new File(reportPath), UTF_8);
} catch (IOException e) {
compiler.report(JSError.make(REPORT_PATH_IO_ERROR, reportPath, e.getMessage()));
}
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, externs, new ProcessExternals());
NodeTraversal.traverseEs6(compiler, root, new FindDependencyScopes());
NodeTraversal.traverseEs6(compiler, root, new HoistVariableAndFunctionDeclarations());
NodeTraversal.traverseEs6(compiler, root, new FindDeclarationsAndSetters());
NodeTraversal.traverseEs6(compiler, root, new FindReferences());
// Create bi-directional references between parent names and their
// descendants. This may create new names.
referenceParentNames();
// If we modify the property of an alias, make sure that modification
// gets reflected in the original object.
referenceAliases();
calculateReferences();
if (reportPath != null) {
try {
Files.append(getHtmlReport(), new File(reportPath), UTF_8);
} catch (IOException e) {
compiler.report(JSError.make(REPORT_PATH_IO_ERROR, reportPath, e.getMessage()));
}
}
if (removeUnreferenced) {
removeUnreferenced();
}
}
/**
* Records an alias of one name to another name.
*/
private void recordAlias(String fromName, String toName) {
recordReference(fromName, toName, RefType.REGULAR);
// We need to add an edge to the alias graph. The alias graph is expressed
// implicitly as a set of connected components, called AliasSets.
//
// There are three possibilities:
// 1) Neither name is part of a connected component. Create a new one.
// 2) Exactly one name is part of a connected component. Merge the new
// name into the component.
// 3) The two names are already part of connected components. Merge
// those components together.
AliasSet toNameAliasSet = aliases.get(toName);
AliasSet fromNameAliasSet = aliases.get(fromName);
AliasSet resultSet = null;
if (toNameAliasSet == null && fromNameAliasSet == null) {
resultSet = new AliasSet(toName, fromName);
} else if (toNameAliasSet != null && fromNameAliasSet != null) {
resultSet = toNameAliasSet;
resultSet.names.addAll(fromNameAliasSet.names);
for (String name : fromNameAliasSet.names) {
aliases.put(name, resultSet);
}
} else if (toNameAliasSet != null) {
resultSet = toNameAliasSet;
resultSet.names.add(fromName);
} else {
resultSet = fromNameAliasSet;
resultSet.names.add(toName);
}
aliases.put(fromName, resultSet);
aliases.put(toName, resultSet);
}
/**
* Records a reference from one name to another name.
*/
private void recordReference(String fromName, String toName,
RefType depType) {
if (fromName.equals(toName)) {
// Don't bother recording self-references.
return;
}
JsName from = getName(fromName, true);
JsName to = getName(toName, true);
referenceGraph.connectIfNotConnectedInDirection(from, depType, to);
}
/**
* Records a reference from one name to another name.
*/
private void recordReference(
DiGraphNode from,
DiGraphNode to,
RefType depType) {
if (from == to) {
// Don't bother recording self-references.
return;
}
if (!referenceGraph.isConnectedInDirection(from, Predicates.equalTo(depType), to)) {
referenceGraph.connect(from, depType, to);
}
}
/**
* Removes all unreferenced variables.
*/
void removeUnreferenced() {
RemoveListener listener = new RemoveListener();
changeProxy.registerListener(listener);
for (RefNode refNode : refNodes) {
JsName name = refNode.name();
if (!name.referenced && !name.externallyDefined) {
refNode.remove();
}
}
changeProxy.unregisterListener(listener);
}
/**
* Generates an HTML report
*
* @return The report
*/
String getHtmlReport() {
StringBuilder sb = new StringBuilder();
sb.append("");
sb.append("OVERALL STATS");
appendListItem(sb, "Total Names: " + countOf(TriState.BOTH, TriState.BOTH));
appendListItem(sb, "Total Classes: "
+ countOf(TriState.TRUE, TriState.BOTH));
appendListItem(sb, "Total Static Functions: "
+ countOf(TriState.FALSE, TriState.BOTH));
appendListItem(sb, "Referenced Names: "
+ countOf(TriState.BOTH, TriState.TRUE));
appendListItem(sb, "Referenced Classes: "
+ countOf(TriState.TRUE, TriState.TRUE));
appendListItem(sb, "Referenced Functions: "
+ countOf(TriState.FALSE, TriState.TRUE));
sb.append("
");
sb.append("ALL NAMES\n");
// Sort before generating to ensure a consistent stable order
for (JsName node : Ordering.natural().sortedCopy(allNames.values())) {
sb.append("- ").append(nameAnchor(node.name)).append("
");
if (!node.prototypeNames.isEmpty()) {
sb.append("- PROTOTYPES: ");
Iterator
protoIter = node.prototypeNames.iterator();
while (protoIter.hasNext()) {
sb.append(protoIter.next());
if (protoIter.hasNext()) {
sb.append(", ");
}
}
}
if (referenceGraph.hasNode(node)) {
List> refersTo =
referenceGraph.getOutEdges(node);
if (!refersTo.isEmpty()) {
sb.append("- REFERS TO: ");
Iterator
> toIter = refersTo.iterator();
while (toIter.hasNext()) {
sb.append(nameLink(toIter.next().getDestination().getValue().name));
if (toIter.hasNext()) {
sb.append(", ");
}
}
}
List> referencedBy =
referenceGraph.getInEdges(node);
if (!referencedBy.isEmpty()) {
sb.append("- REFERENCED BY: ");
Iterator
> fromIter = refersTo.iterator();
while (fromIter.hasNext()) {
sb.append(
nameLink(fromIter.next().getDestination().getValue().name));
if (fromIter.hasNext()) {
sb.append(", ");
}
}
}
}
sb.append(" ");
sb.append("
");
}
sb.append("
");
sb.append("");
return sb.toString();
}
private static void appendListItem(StringBuilder sb, String text) {
sb.append("").append(text).append(" \n");
}
private static String nameLink(String name) {
return "" + name + "";
}
private static String nameAnchor(String name) {
return "" + name + "";
}
/**
* Looks up a {@link JsName} by name, optionally creating one if it doesn't
* already exist.
*
* @param name A fully qualified name
* @param canCreate Whether to create the object if necessary
* @return The {@code JsName} object, or null if one can't be found and
* can't be created.
*/
private JsName getName(String name, boolean canCreate) {
if (canCreate) {
return createName(name);
}
return allNames.get(name);
}
/**
* Creates a {@link JsName} for the given name if it doesn't already
* exist.
*
* @param name A fully qualified name
*/
private JsName createName(String name) {
JsName jsn = allNames.get(name);
if (jsn == null) {
jsn = new JsName();
jsn.name = name;
allNames.put(name, jsn);
}
return jsn;
}
/**
* The NameAnalyzer algorithm works best when all objects have a canonical
* name in the global scope. When multiple names in the global scope
* point to the same object, things start to break down.
*
* For example, if we have
*
* var a = {};
* var b = a;
* a.foo = 3;
* alert(b.foo);
*
* then a.foo and b.foo are the same name, even though NameAnalyzer doesn't
* represent them as such.
*
* To handle this case, we look at all the aliases in the program.
* If descendant properties of that alias are assigned, then we create a
* directional reference from the original name to the alias. For example,
* in this case, the assign to {@code a.foo} triggers a reference from
* {@code b} to {@code a}, but NOT from a to b.
*
* Similarly, "instanceof" checks do not prevent the removal
* of a unaliased name but an instanceof check on an alias can only be removed
* if the other aliases are also removed, so we add a connection here.
*/
private void referenceAliases() {
// Minimize the number of connections in the graph by creating a connected
// cluster for names that are used to modify the object and then ensure
// there is at least one link to the cluster from the other names (which are
// removalable on there own) in the AliasSet.
Set sets = new HashSet<>(aliases.values());
for (AliasSet set : sets) {
DiGraphNode first = null;
Set> required = new HashSet<>();
for (String key : set.names) {
JsName name = getName(key, false);
if (name.hasWrittenDescendants || name.hasInstanceOfReference) {
DiGraphNode node = getGraphNode(name);
required.add(node);
if (first == null) {
first = node;
}
}
}
if (!required.isEmpty()) {
// link the required nodes together to form a cluster so that if one
// is needed, all are kept.
for (DiGraphNode node : required) {
recordReference(node, first, RefType.REGULAR);
recordReference(first, node, RefType.REGULAR);
}
// link all the other aliases to the one of the required nodes, so
// that if they are kept only if referenced directly, but all the
// required nodes are kept if any are referenced.
for (String key : set.names) {
DiGraphNode alias = getGraphNode(getName(key, false));
recordReference(alias, first, RefType.REGULAR);
}
}
}
}
private DiGraphNode getGraphNode(JsName name) {
return referenceGraph.createDirectedGraphNode(name);
}
/**
* Adds mutual references between all known global names and their parent
* names. (e.g. between a.b.c
and a.b
).
*/
private void referenceParentNames() {
// Duplicate set of nodes to process so we don't modify set we are
// currently iterating over
JsName[] allNamesCopy = allNames.values().toArray(new JsName[0]);
for (JsName name : allNamesCopy) {
String curName = name.name;
// Add a reference to the direct parent. It in turn will point to its parent.
if (curName.contains(".")) {
String parentName = curName.substring(0, curName.lastIndexOf('.'));
if (!globalNames.contains(parentName)) {
JsName parentJsName = getName(parentName, true);
DiGraphNode nameNode = getGraphNode(name);
DiGraphNode parentNode = getGraphNode(parentJsName);
recordReference(nameNode, parentNode, RefType.REGULAR);
recordReference(parentNode, nameNode, RefType.REGULAR);
}
}
}
}
/**
* Creates name information for the current node during a traversal.
*
* @param t The node traversal
* @param n The current node
* @return The name information, or null if the name is irrelevant to this pass
*/
@Nullable
private NameInformation createNameInformation(NodeTraversal t, Node n) {
Node parent = n.getParent();
// Build the full name and find its root node by iterating down through all
// GETPROP/GETELEM nodes.
String name = "";
Node rootNameNode = n;
boolean bNameWasShortened = false;
while (true) {
if (NodeUtil.isGet(rootNameNode)) {
Node prop = rootNameNode.getLastChild();
if (rootNameNode.isGetProp()) {
name = "." + prop.getString() + name;
} else {
// We consider the name to be "a.b" in a.b['c'] or a.b[x].d.
bNameWasShortened = true;
name = "";
}
rootNameNode = rootNameNode.getFirstChild();
} else if (NodeUtil.isObjectLitKey(rootNameNode)) {
name = "." + rootNameNode.getString() + name;
// Check if this is an object literal assigned to something.
Node objLit = rootNameNode.getParent();
Node objLitParent = objLit.getParent();
if (objLitParent.isAssign()) {
// This must be the right side of the assign.
rootNameNode = objLitParent.getFirstChild();
} else if (objLitParent.isName()) {
// This must be a VAR initialization.
rootNameNode = objLitParent;
} else if (objLitParent.isStringKey()) {
// This must be a object literal key initialization.
rootNameNode = objLitParent;
} else {
return null;
}
} else if (isAnalyzableObjectDefinePropertiesDefinition(rootNameNode)) {
Node target = rootNameNode.getSecondChild();
if (!target.isQualifiedName()) {
return null;
}
rootNameNode = target;
} else {
break;
}
}
// Check whether this is a class-defining call. Classes may only be defined
// in the global scope.
if (parent.isCall() && t.inGlobalHoistScope()) {
CodingConvention convention = compiler.getCodingConvention();
SubclassRelationship classes = convention.getClassesDefinedByCall(parent);
if (classes != null) {
NameInformation nameInfo = new NameInformation();
nameInfo.name = classes.subclassName;
nameInfo.onlyAffectsClassDef = true;
nameInfo.superclass = classes.superclassName;
return nameInfo;
}
String singletonGetterClass =
convention.getSingletonGetterClassName(parent);
if (singletonGetterClass != null) {
NameInformation nameInfo = new NameInformation();
nameInfo.name = singletonGetterClass;
nameInfo.onlyAffectsClassDef = true;
return nameInfo;
}
}
switch (rootNameNode.getToken()) {
case NAME:
// Check whether this is an assignment to a prototype property
// of an object defined in the global scope.
if (!bNameWasShortened &&
n.isGetProp() &&
parent.isAssign() &&
"prototype".equals(n.getLastChild().getString())) {
if (createNameInformation(t, n.getFirstChild()) != null) {
name = rootNameNode.getString() + name;
name = name.substring(0, name.length() - PROTOTYPE_SUFFIX_LEN);
NameInformation nameInfo = new NameInformation();
nameInfo.name = name;
return nameInfo;
} else {
return null;
}
}
return createNameInformation(
rootNameNode.getString() + name, t.getScope(), rootNameNode);
case THIS:
if (t.inGlobalHoistScope()) {
NameInformation nameInfo = new NameInformation();
if (name.indexOf('.') == 0) {
nameInfo.name = name.substring(1); // strip leading "."
} else {
nameInfo.name = name;
}
nameInfo.isExternallyReferenceable = true;
return nameInfo;
}
return null;
default:
return null;
}
}
/**
* Creates name information for a particular qualified name that occurs in a particular scope.
*
* @param name A qualified name (e.g. "x" or "a.b.c")
* @param scope The scope in which {@code name} occurs
* @param rootNameNode The NAME node for the first token of {@code name}
* @return The name information, or null if the name is irrelevant to this pass
*/
@Nullable
private NameInformation createNameInformation(String name, Scope scope, Node rootNameNode) {
// Check the scope. Currently we're only looking at globally scoped vars.
String rootName = rootNameNode.getString();
Var v = scope.getVar(rootName);
boolean isExtern = (v == null && externalNames.contains(rootName));
boolean isGlobalRef = (v != null && v.isGlobal()) || isExtern ||
rootName.equals(WINDOW);
if (!isGlobalRef) {
return null;
}
NameInformation nameInfo = new NameInformation();
// If a prototype property or method, fill in prototype information.
int idx = name.indexOf(PROTOTYPE_SUBSTRING);
if (idx != -1) {
nameInfo.isPrototype = true;
nameInfo.prototypeClass = name.substring(0, idx);
nameInfo.prototypeProperty = name.substring(
idx + PROTOTYPE_SUBSTRING_LEN);
}
nameInfo.name = name;
nameInfo.isExternallyReferenceable =
isExtern || isExternallyReferenceable(scope, name);
return nameInfo;
}
/**
* Checks whether a name can be referenced outside of the compiled code.
* These names will be the root of dependency trees.
*
* @param scope The current variable scope
* @param name The name
* @return True if can be referenced outside
*/
private boolean isExternallyReferenceable(Scope scope, String name) {
if (compiler.getCodingConvention().isExported(name)) {
return true;
}
if (scope.isLocal()) {
return false;
}
for (String s : globalNames) {
if (name.startsWith(s)) {
return true;
}
}
return false;
}
/**
* Gets the nearest enclosing dependency scope, or null if there isn't one.
*/
private List getDependencyScope(Node n) {
for (Node node : n.getAncestors()) {
List refs = scopes.get(node);
if (!refs.isEmpty()) {
return refs;
}
}
return Collections.emptyList();
}
/**
* Get dependency scope defined by the enclosing function, or null.
* If enclosing function is a function expression, determine scope based on
* its parent if the parent node is a variable declaration or
* assignment.
*/
private List getEnclosingFunctionDependencyScope(
NodeTraversal t) {
Node function = t.getEnclosingFunction();
if (function == null) {
return Collections.emptyList();
}
List refs = scopes.get(function);
if (!refs.isEmpty()) {
return refs;
}
// Function expression. try to get a name from the parent var
// declaration or assignment.
Node parent = function.getParent();
if (parent != null) {
// Account for functions defined in the form:
// var a = cond ? function a() {} : function b() {};
while (parent.isHook()) {
parent = parent.getParent();
}
if (parent.isName()) {
return scopes.get(parent);
}
if (parent.isAssign()) {
return scopes.get(parent);
}
}
return Collections.emptyList();
}
/**
* Propagate "referenced" property down the graph.
*/
private void calculateReferences() {
JsName window = getName(WINDOW, true);
window.referenced = true;
JsName function = getName(FUNCTION, true);
function.referenced = true;
propagateReference(window, function);
}
private void propagateReference(JsName ... names) {
Deque> work = new ArrayDeque<>();
for (JsName name : names) {
work.push(referenceGraph.createDirectedGraphNode(name));
}
while (!work.isEmpty()) {
DiGraphNode source = work.pop();
List> outEdges = source.getOutEdges();
int len = outEdges.size();
for (int i = 0; i < len; i++) {
DiGraphNode item = outEdges.get(i).getDestination();
JsName destNode = item.getValue();
if (!destNode.referenced) {
destNode.referenced = true;
work.push(item);
}
}
}
}
/**
* Enum for saying a value can be true, false, or either (cleaner than using a
* Boolean with null)
*/
private enum TriState {
/** If value is true */
TRUE,
/** If value is false */
FALSE,
/** If value can be true or false */
BOTH
}
/**
* Gets the count of nodes matching the criteria
*
* @param isClass Whether the node is a class
* @param referenced Whether the node is referenced
* @return Number of matches
*/
private int countOf(TriState isClass, TriState referenced) {
int count = 0;
for (JsName name : allNames.values()) {
boolean nodeIsClass = !name.prototypeNames.isEmpty();
boolean classMatch =
isClass == TriState.BOTH || (nodeIsClass && isClass == TriState.TRUE)
|| (!nodeIsClass && isClass == TriState.FALSE);
boolean referenceMatch = referenced == TriState.BOTH
|| (name.referenced && referenced == TriState.TRUE)
|| (!name.referenced && referenced == TriState.FALSE);
if (classMatch && referenceMatch && !name.externallyDefined) {
count++;
}
}
return count;
}
/**
* Extract a list of replacement nodes to use.
*/
private List getSideEffectNodes(Node n) {
List subexpressions = new ArrayList<>();
NodeTraversal.traverseEs6(
compiler,
n,
new GatherSideEffectSubexpressionsCallback(
compiler, new GetReplacementSideEffectSubexpressions(compiler, subexpressions)));
List replacements = new ArrayList<>(subexpressions.size());
for (Node subexpression : subexpressions) {
replacements.add(NodeUtil.newExpr(subexpression));
}
return replacements;
}
/**
* Replace n with a simpler expression, while preserving program
* behavior.
*
* If the n's value is used, replace it with its RHS; otherwise
* replace it with the subexpressions that have side effects.
*/
private void replaceWithRhs(Node parent, Node n) {
if (valueConsumedByParent(n, parent)) {
// parent reads from n directly; replace it with n's rhs + lhs
// subexpressions with side effects.
List replacements = getRhsSubexpressions(n);
List newReplacements = new ArrayList<>();
for (int i = 0; i < replacements.size() - 1; i++) {
newReplacements.addAll(getSideEffectNodes(replacements.get(i)));
}
Node valueExpr = Iterables.getLast(replacements);
valueExpr.detach();
newReplacements.add(valueExpr);
changeProxy.replaceWith(
parent, n, collapseReplacements(newReplacements));
} else if (n.isAssign() && !parent.isVanillaFor()) {
// assignment appears in a RHS expression. we have already
// considered names in the assignment's RHS as being referenced;
// replace the assignment with its RHS.
// TODO(user) make the pass smarter about these cases and/or run
// this pass and RemoveConstantExpressions together in a loop.
Node replacement = n.getLastChild();
replacement.detach();
changeProxy.replaceWith(parent, n, replacement);
} else {
replaceTopLevelExpressionWithRhs(parent, n);
}
}
/**
* Simplify a toplevel expression, while preserving program
* behavior.
*/
private void replaceTopLevelExpressionWithRhs(Node parent, Node n) {
// validate inputs
switch (parent.getToken()) {
case BLOCK:
case ROOT:
case SCRIPT:
case FOR:
case FOR_IN:
case LABEL:
break;
default:
throw new IllegalArgumentException(
"Unsupported parent node type in replaceWithRhs " + parent.getToken());
}
switch (n.getToken()) {
case EXPR_RESULT:
case FUNCTION:
case VAR:
break;
case ASSIGN:
checkArgument(
parent.isVanillaFor(),
"Unsupported assignment in replaceWithRhs. parent: %s",
parent);
break;
default:
throw new IllegalArgumentException(
"Unsupported node type in replaceWithRhs " + n.getToken());
}
// gather replacements
List replacements = new ArrayList<>();
for (Node rhs : getRhsSubexpressions(n)) {
replacements.addAll(getSideEffectNodes(rhs));
}
if (parent.isVanillaFor() || parent.isForIn()) {
// tweak replacements array s.t. it is a single expression node.
if (replacements.isEmpty()) {
replacements.add(IR.empty());
} else {
Node expr = collapseReplacements(replacements);
replacements.clear();
replacements.add(expr);
}
}
changeProxy.replaceWith(parent, n, replacements);
}
/**
* Determine if the parent reads the value of a child expression
* directly. This is true children used in predicates, RETURN
* statements and, RHS of variable declarations and assignments.
*
* In the case of:
* if (a) b else c
*
* This method returns true for "a", and false for "b" and "c": the
* IF expression does something special based on "a"'s value. "b"
* and "c" are effectively outputs. Same logic applies to FOR,
* WHILE and DO loop predicates. AND/OR/HOOK expressions are
* syntactic sugar for IF statements; therefore this method returns
* true for the predicate and false otherwise.
*/
private static boolean valueConsumedByParent(Node n, Node parent) {
if (NodeUtil.isAssignmentOp(parent)) {
return parent.getLastChild() == n;
}
switch (parent.getToken()) {
case NAME:
case RETURN:
return true;
case AND:
case OR:
case HOOK:
case IF:
case WHILE:
return parent.getFirstChild() == n;
case FOR:
case FOR_IN:
return parent.getSecondChild() == n;
case DO:
return parent.getLastChild() == n;
default:
return false;
}
}
/**
* Merge a list of nodes into a single expression. The value of the
* new expression is determined by the last expression in the list.
*/
private static Node collapseReplacements(List replacements) {
Node expr = null;
for (Node rep : replacements) {
if (rep.isExprResult()) {
rep = rep.getFirstChild();
rep.detach();
}
if (expr == null) {
expr = rep;
} else {
expr = IR.comma(expr, rep);
}
}
return expr;
}
/**
* Extract a list of subexpressions that act as right hand sides.
*/
private static List getRhsSubexpressions(Node n) {
switch (n.getToken()) {
case EXPR_RESULT:
// process body
return getRhsSubexpressions(n.getFirstChild());
case FUNCTION:
// function nodes have no RHS
return ImmutableList.of();
case CALL:
{
// In our analyzable case, only the last argument to Object.defineProperties
// (the object literal) can have side-effects
checkState(isAnalyzableObjectDefinePropertiesDefinition(n));
return ImmutableList.of(n.getLastChild());
}
case NAME:
{
// parent is a var node. RHS is the first child
Node rhs = n.getFirstChild();
if (rhs != null) {
return ImmutableList.of(rhs);
} else {
return ImmutableList.of();
}
}
case ASSIGN:
{
// add LHS and RHS expressions - LHS may be a complex expression
Node lhs = n.getFirstChild();
Node rhs = lhs.getNext();
return ImmutableList.of(lhs, rhs);
}
case VAR:
{
// recurse on all children
ImmutableList.Builder nodes = ImmutableList.builder();
for (Node child : n.children()) {
nodes.addAll(getRhsSubexpressions(child));
}
return nodes.build();
}
default:
throw new IllegalArgumentException("AstChangeProxy::getRhs " + n);
}
}
/**
* Check if {@code n} is an Object.defineProperties definition
* that is static enough for this pass to understand and remove.
*/
private static boolean isAnalyzableObjectDefinePropertiesDefinition(Node n) {
// TODO(blickly): Move this code to CodingConvention so that
// it's possible to define alternate ways of defining properties.
return NodeUtil.isObjectDefinePropertiesDefinition(n)
&& n.getParent().isExprResult()
&& n.getFirstChild().getNext().isQualifiedName()
&& n.getLastChild().isObjectLit();
}
}