com.google.javascript.jscomp.NameAnalyzer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.NameAnalyzer.java

Source

/*
 * 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 com.google.common.base.Preconditions;
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.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
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 com.google.javascript.rhino.Token;

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 java.util.TreeMap;

/**
 * This pass identifies all global names, simple (e.g. <code>a</code>) or
 * qualified (e.g. <code>a.b.c</code>), and the dependencies between them, then
 * removes code associated with unreferenced names. It starts by assuming that
 * only externally accessible names (e.g. <code>window</code>) are referenced,
 * then iteratively marks additional names as referenced (e.g. <code>Foo</code>
 * in <code>window['foo'] = new Foo();</code>). This makes it possible to
 * eliminate code containing circular references.
 *
 * <p>Qualified names can be defined using dotted or object literal syntax
 * (<code>a.b.c = x;</code> or <code>a.b = {c: x};</code>, respectively).
 *
 * <p>Removal of prototype classes is currently all or nothing. In other words,
 * prototype properties and methods are never individually removed.
 *
 * <p>Optionally generates pretty HTML output of data so that it is easy to
 * analyze dependencies.
 *
 * <p>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<String, JsName> allNames = new TreeMap<>();

    /** Reference dependency graph */
    private LinkedDirectedGraph<JsName, RefType> 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<Node, NameInformation> 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<String> DEFAULT_GLOBAL_NAMES = ImmutableSet.of("window", "goog.global");

    /** Whether to remove unreferenced variables in main pass */
    private final boolean removeUnreferenced;

    /** Names that refer to the global scope */
    private final Set<String> globalNames;

    /** Ast change helper */
    private final AstChangeProxy changeProxy;

    /** Names that are externally defined */
    private final Set<String> externalNames = new HashSet<>();

    /** Name declarations or assignments, in post-order traversal order */
    private final List<RefNode> 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<String, AliasSet> aliases = new HashMap<>();

    /**
     * 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<String> 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" */
        String prototypeClass = null;

        /** Local name of prototype property i.e. "b" if name is "a.prototype.b" */
        String prototypeProperty = null;

        /** Name of the super class of name */
        String superclass = null;

        /** Whether this is a call that only affects the class definition */
        boolean onlyAffectsClassDef = false;
    }

    /**
     * Struct to hold information about a fully qualified JS name
     */
    private static class JsName implements Comparable<JsName> {
        /** Fully qualified name */
        String name;

        /** Name of prototype functions attached to this name */
        List<String> 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<String> 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.getType()) {
            case Token.VAR:
                Preconditions.checkState(parent.hasOneChild());
                replaceWithRhs(containingNode, parent);
                break;
            case Token.FUNCTION:
                replaceWithRhs(containingNode, parent);
                break;
            case Token.ASSIGN:
                if (containingNode.isExprResult()) {
                    replaceWithRhs(containingNode.getParent(), containingNode);
                } else {
                    replaceWithRhs(containingNode, parent);
                }
                break;
            case Token.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;
            }
        }
    }

    /**
     * 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());

            Preconditions.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().detachFromParent());
            }
        }
    }

    /**
     * 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.getParent() == null ? null : node.getParent().getParent();
        }
    }

    /**
     * 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);
            Preconditions.checkState(node.isCall());
        }

        @Override
        public void remove() {
            Preconditions.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);
            Preconditions.checkState(node.isQualifiedName());
            Preconditions.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);
            }
        }
    }

    /**
     * <p>Identifies all dependency scopes.
     *
     * <p>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.
     *
     * <p>The entire parse tree is ultimately in a dependency scope relationship
     * with <code>window</code> (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.
     *
     * <p>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 <code>f</code> and the assignment
     * inside the function forms a dependency scope with the name <code>x</code>.
     * <pre>
     * var x; function f() { x = 1; }
     * </pre>
     */
    private class FindDependencyScopes extends AbstractPostOrderCallback {
        @Override
        public void visit(NodeTraversal t, Node n, Node parent) {
            if (!t.inGlobalScope()) {
                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);
                recordDepScope(n, ns);
            } else if (NodeUtil.isFunctionDeclaration(n)) {
                NameInformation ns = createNameInformation(t, 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);
                }
            }
        }

        private void recordConsumers(NodeTraversal t, Node n, Node recordNode) {
            Node parent = n.getParent();
            switch (parent.getType()) {
            case Token.ASSIGN:
                if (n == parent.getLastChild()) {
                    recordAssignment(t, parent, recordNode);
                }
                recordConsumers(t, parent, recordNode);
                break;
            case Token.NAME:
                NameInformation ns = createNameInformation(t, parent);
                recordDepScope(recordNode, ns);
                break;
            case Token.OR:
                recordConsumers(t, parent, recordNode);
                break;
            case Token.AND:
                // In "a && b" only "b" can be meaningfully aliased.
                // "a" must be falsy, which it must be an immutable, non-Object
            case Token.COMMA:
            case Token.HOOK:
                if (n != parent.getFirstChild()) {
                    recordConsumers(t, parent, recordNode);
                }
                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.isFor() && !NodeUtil.isForIn(parent)) {
                    // 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.getFirstChild().getNext() != n) {
                        recordDepScope(recordNode, ns);
                    } else {
                        recordDepScope(nameNode, ns);
                    }
                } else if (!(parent.isCall() && parent.getFirstChild() == n)) {
                    // 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) {
            Preconditions.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);
                Preconditions.checkNotNull(ns, "NameInformation is null");
                createName(ns.name);
            } else if (NodeUtil.isFunctionDeclaration(n)) {
                Node nameNode = n.getFirstChild();
                NameInformation ns = createNameInformation(t, nameNode);
                Preconditions.checkNotNull(ns, "NameInformation is null");
                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.inGlobalScope()) {
                if (NodeUtil.isVarDeclaration(n)) {
                    NameInformation ns = createNameInformation(t, n);
                    Preconditions.checkNotNull(ns);
                    recordSet(ns.name, n);
                } else if (NodeUtil.isFunctionDeclaration(n)) {
                    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()) {
                Node nameNode = n.getFirstChild();

                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, or STRING
         * [objlit key])
         */
        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()) {
                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<Node> NON_LOCAL_RESULT_PREDICATE = new Predicate<Node>() {
        @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;
        }
    };

    /**
     * <p>Identifies all references between global names.
     *
     * <p>A reference from a name <code>f</code> to a name <code>g</code> means
     * that if the name <code>f</code> must be defined, then the name
     * <code>g</code> must also be defined. This would be the case if, for
     * example, <code>f</code> were a function that called <code>g</code>.
     */
    private class FindReferences implements Callback {
        Set<Node> 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.traverse(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.isFor() || parent.isReturn())) {
                for (Node child : n.children()) {
                    addSimplifiedChildren(child);
                }
            } 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.isFor()) {
                if (!NodeUtil.isForIn(n)) {
                    Node decl = n.getFirstChild();
                    Node pred = decl.getNext();
                    Node step = pred.getNext();
                    addSimplifiedExpression(decl, n);
                    addSimplifiedExpression(pred, n);
                    addSimplifiedExpression(step, n);
                } else { // n.getChildCount() == 3
                    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<NameInformation> 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,
                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<NameInformation> 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<NameInformation> 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, 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) {
        this.compiler = compiler;
        this.removeUnreferenced = removeUnreferenced;
        this.globalNames = DEFAULT_GLOBAL_NAMES;
        this.changeProxy = new AstChangeProxy();
    }

    @Override
    public void process(Node externs, Node root) {
        NodeTraversal.traverse(compiler, externs, new ProcessExternals());
        NodeTraversal.traverse(compiler, root, new FindDependencyScopes());
        NodeTraversal.traverse(compiler, root, new HoistVariableAndFunctionDeclarations());
        NodeTraversal.traverse(compiler, root, new FindDeclarationsAndSetters());
        NodeTraversal.traverse(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 (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<JsName, RefType> from, DiGraphNode<JsName, RefType> 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("<html><body><style type=\"text/css\">" + "body, td, p {font-family: Arial; font-size: 83%} "
                + "ul {margin-top:2px; margin-left:0px; padding-left:1em;} "
                + "li {margin-top:3px; margin-left:24px; padding-left:0px;" + "padding-bottom: 4px}</style>");
        sb.append("OVERALL STATS<ul>");
        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("</ul>");

        sb.append("ALL NAMES<ul>\n");
        for (JsName node : allNames.values()) {
            sb.append("<li>" + nameAnchor(node.name) + "<ul>");
            if (!node.prototypeNames.isEmpty()) {
                sb.append("<li>PROTOTYPES: ");
                Iterator<String> protoIter = node.prototypeNames.iterator();
                while (protoIter.hasNext()) {
                    sb.append(protoIter.next());
                    if (protoIter.hasNext()) {
                        sb.append(", ");
                    }
                }
            }

            if (referenceGraph.hasNode(node)) {
                List<DiGraphEdge<JsName, RefType>> refersTo = referenceGraph.getOutEdges(node);
                if (!refersTo.isEmpty()) {
                    sb.append("<li>REFERS TO: ");
                    Iterator<DiGraphEdge<JsName, RefType>> toIter = refersTo.iterator();
                    while (toIter.hasNext()) {
                        sb.append(nameLink(toIter.next().getDestination().getValue().name));
                        if (toIter.hasNext()) {
                            sb.append(", ");
                        }
                    }
                }

                List<DiGraphEdge<JsName, RefType>> referencedBy = referenceGraph.getInEdges(node);
                if (!referencedBy.isEmpty()) {
                    sb.append("<li>REFERENCED BY: ");
                    Iterator<DiGraphEdge<JsName, RefType>> fromIter = refersTo.iterator();
                    while (fromIter.hasNext()) {
                        sb.append(nameLink(fromIter.next().getDestination().getValue().name));
                        if (fromIter.hasNext()) {
                            sb.append(", ");
                        }
                    }
                }
            }
            sb.append("</li>");
            sb.append("</ul></li>");
        }
        sb.append("</ul>");
        sb.append("</body></html>");

        return sb.toString();
    }

    private static void appendListItem(StringBuilder sb, String text) {
        sb.append("<li>" + text + "</li>\n");
    }

    private static String nameLink(String name) {
        return "<a href=\"#" + name + "\">" + name + "</a>";
    }

    private static String nameAnchor(String name) {
        return "<a name=\"" + name + "\">" + name + "</a>";
    }

    /**
     * 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) {
            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 void createName(String name) {
        JsName jsn = allNames.get(name);
        if (jsn == null) {
            jsn = new JsName();
            jsn.name = name;
            allNames.put(name, 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
     * <code>
     * var a = {};
     * var b = a;
     * a.foo = 3;
     * alert(b.foo);
     * </code>
     * 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<AliasSet> sets = new HashSet<>(aliases.values());
        for (AliasSet set : sets) {
            DiGraphNode<JsName, RefType> first = null;
            Set<DiGraphNode<JsName, RefType>> required = new HashSet<>();
            for (String key : set.names) {
                JsName name = getName(key, false);
                if (name.hasWrittenDescendants || name.hasInstanceOfReference) {
                    DiGraphNode<JsName, RefType> 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<JsName, RefType> 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<JsName, RefType> alias = getGraphNode(getName(key, false));
                    recordReference(alias, first, RefType.REGULAR);
                }
            }
        }
    }

    private DiGraphNode<JsName, RefType> getGraphNode(JsName name) {
        return referenceGraph.createDirectedGraphNode(name);
    }

    /**
     * Adds mutual references between all known global names and their parent
     * names. (e.g. between <code>a.b.c</code> and <code>a.b</code>).
     */
    private void referenceParentNames() {
        // Duplicate set of nodes to process so we don't modify set we are
        // currently iterating over
        Set<JsName> allNamesCopy = new HashSet<>(allNames.values());

        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<JsName, RefType> nameNode = getGraphNode(name);
                    DiGraphNode<JsName, RefType> 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
     */
    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 {
                break;
            }
        }

        // Check whether this is a class-defining call. Classes may only be defined
        // in the global scope.
        if (parent.isCall() && t.inGlobalScope()) {
            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.getType()) {
        case Token.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 Token.THIS:
            if (t.inGlobalScope()) {
                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
     */
    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<NameInformation> getDependencyScope(Node n) {
        for (Node node : n.getAncestors()) {
            List<NameInformation> 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<NameInformation> getEnclosingFunctionDependencyScope(NodeTraversal t) {
        Node function = t.getEnclosingFunction();
        if (function == null) {
            return Collections.emptyList();
        }

        List<NameInformation> 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<DiGraphNode<JsName, RefType>> work = new ArrayDeque<>();
        for (JsName name : names) {
            work.push(referenceGraph.createDirectedGraphNode(name));
        }
        while (!work.isEmpty()) {
            DiGraphNode<JsName, RefType> source = work.pop();
            List<DiGraphEdge<JsName, RefType>> outEdges = source.getOutEdges();
            int len = outEdges.size();
            for (int i = 0; i < len; i++) {
                DiGraphNode<JsName, RefType> 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<Node> getSideEffectNodes(Node n) {
        List<Node> subexpressions = new ArrayList<>();
        NodeTraversal.traverse(compiler, n, new GatherSideEffectSubexpressionsCallback(compiler,
                new GetReplacementSideEffectSubexpressions(compiler, subexpressions)));

        List<Node> 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<Node> replacements = getRhsSubexpressions(n);
            List<Node> newReplacements = new ArrayList<>();
            for (int i = 0; i < replacements.size() - 1; i++) {
                newReplacements.addAll(getSideEffectNodes(replacements.get(i)));
            }
            Node valueExpr = replacements.get(replacements.size() - 1);
            valueExpr.detachFromParent();
            newReplacements.add(valueExpr);
            changeProxy.replaceWith(parent, n, collapseReplacements(newReplacements));
        } else if (n.isAssign() && !parent.isFor()) {
            // 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.detachFromParent();
            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.getType()) {
        case Token.BLOCK:
        case Token.SCRIPT:
        case Token.FOR:
        case Token.LABEL:
            break;
        default:
            throw new IllegalArgumentException(
                    "Unsupported parent node type in replaceWithRhs " + Token.name(parent.getType()));
        }

        switch (n.getType()) {
        case Token.EXPR_RESULT:
        case Token.FUNCTION:
        case Token.VAR:
            break;
        case Token.ASSIGN:
            Preconditions.checkArgument(parent.isFor(), "Unsupported assignment in replaceWithRhs. parent: %s",
                    Token.name(parent.getType()));
            break;
        default:
            throw new IllegalArgumentException(
                    "Unsupported node type in replaceWithRhs " + Token.name(n.getType()));
        }

        // gather replacements
        List<Node> replacements = new ArrayList<>();
        for (Node rhs : getRhsSubexpressions(n)) {
            replacements.addAll(getSideEffectNodes(rhs));
        }

        if (parent.isFor()) {
            // 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.getType()) {
        case Token.NAME:
        case Token.RETURN:
            return true;

        case Token.AND:
        case Token.OR:
        case Token.HOOK:
        case Token.IF:
        case Token.WHILE:
            return parent.getFirstChild() == n;

        case Token.FOR:
            return parent.getFirstChild().getNext() == n;

        case Token.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<Node> replacements) {
        Node expr = null;
        for (Node rep : replacements) {
            if (rep.isExprResult()) {
                rep = rep.getFirstChild();
                rep.detachFromParent();
            }

            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<Node> getRhsSubexpressions(Node n) {
        switch (n.getType()) {
        case Token.EXPR_RESULT:
            // process body
            return getRhsSubexpressions(n.getFirstChild());
        case Token.FUNCTION:
            // function nodes have no RHS
            return ImmutableList.of();
        case Token.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 Token.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 Token.VAR: {
            // recurse on all children
            ImmutableList.Builder<Node> nodes = ImmutableList.builder();
            for (Node child : n.children()) {
                nodes.addAll(getRhsSubexpressions(child));
            }
            return nodes.build();
        }
        default:
            throw new IllegalArgumentException("AstChangeProxy::getRhs " + n);
        }
    }
}