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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * 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(); } }




    © 2015 - 2024 Weber Informatics LLC | Privacy Policy