/*
 * Copyright 2009 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.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.javascript.jscomp.DefinitionsRemover.Definition;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.jscomp.Scope.Var;
import com.google.javascript.jscomp.graph.DiGraph;
import com.google.javascript.jscomp.graph.FixedPointGraphTraversal;
import com.google.javascript.jscomp.graph.FixedPointGraphTraversal.EdgeCallback;
import com.google.javascript.jscomp.graph.LinkedDirectedGraph;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Compiler pass that computes function purity.  A function is pure if
 * it has no outside visible side effects, and the result of the
 * computation does not depend on external factors that are beyond the
 * control of the application; repeated calls to the function should
 * return the same value as long as global state hasn't changed.
 *
 * Date.now is an example of a function that has no side effects but
 * is not pure.
 *
 *
 * We will prevail, in peace and freedom from fear, and in true
 * health, through the purity and essence of our natural... fluids.
 *                                    - General Turgidson
 */
class PureFunctionIdentifier implements CompilerPass {
  static final DiagnosticType INVALID_NO_SIDE_EFFECT_ANNOTATION =
      DiagnosticType.error(
          "JSC_INVALID_NO_SIDE_EFFECT_ANNOTATION",
          "@nosideeffects may only appear in externs files.");

  static final DiagnosticType INVALID_MODIFIES_ANNOTATION =
    DiagnosticType.error(
        "JSC_INVALID_MODIFIES_ANNOTATION",
        "@modifies may only appear in externs files.");

  private final AbstractCompiler compiler;
  private final DefinitionProvider definitionProvider;

  // Function node -> function side effects map
  private final Map<Node, FunctionInformation> functionSideEffectMap;

  // List of all function call sites; used to iterate in markPureFunctionCalls.
  private final List<Node> allFunctionCalls;

  // Externs and ast tree root, for use in getDebugReport.  These two
  // fields are null until process is called.
  private Node externs;
  private Node root;

  public PureFunctionIdentifier(AbstractCompiler compiler,
                                DefinitionProvider definitionProvider) {
    this.compiler = compiler;
    this.definitionProvider = definitionProvider;
    this.functionSideEffectMap = Maps.newHashMap();
    this.allFunctionCalls = Lists.newArrayList();
    this.externs = null;
    this.root = null;
  }

  @Override
  public void process(Node externsAst, Node srcAst) {
    if (externs != null || root != null) {
      throw new IllegalStateException(
          "It is illegal to call PureFunctionIdentifier.process " +
          "twice the same instance.  Please use a new " +
          "PureFunctionIdentifier instance each time.");
    }

    externs = externsAst;
    root = srcAst;

    NodeTraversal.traverse(compiler, externs, new FunctionAnalyzer(true));
    NodeTraversal.traverse(compiler, root, new FunctionAnalyzer(false));

    propagateSideEffects();

    markPureFunctionCalls();
  }

  /**
   * Compute debug report that includes:
   *  - List of all pure functions.
   *  - Reasons we think the remaining functions have side effects.
   */
  String getDebugReport() {
    Preconditions.checkNotNull(externs);
    Preconditions.checkNotNull(root);

    StringBuilder sb = new StringBuilder();

    FunctionNames functionNames = new FunctionNames(compiler);
    functionNames.process(null, externs);
    functionNames.process(null, root);

    sb.append("Pure functions:\n");
    for (Map.Entry<Node, FunctionInformation> entry :
             functionSideEffectMap.entrySet()) {
      Node function = entry.getKey();
      FunctionInformation functionInfo = entry.getValue();

      boolean isPure =
          functionInfo.mayBePure() && !functionInfo.mayHaveSideEffects();
      if (isPure) {
        sb.append("  " + functionNames.getFunctionName(function) + "\n");
      }
    }
    sb.append("\n");

    for (Map.Entry<Node, FunctionInformation> entry :
             functionSideEffectMap.entrySet()) {
      Node function = entry.getKey();
      FunctionInformation functionInfo = entry.getValue();

      Set<String> depFunctionNames = Sets.newHashSet();
      for (Node callSite : functionInfo.getCallsInFunctionBody()) {
        Collection<Definition> defs =
            getCallableDefinitions(definitionProvider,
                                   callSite.getFirstChild());

        if (defs == null) {
          depFunctionNames.add("<null def list>");
          continue;
        }

        for (Definition def : defs) {
          depFunctionNames.add(
              functionNames.getFunctionName(def.getRValue()));
        }
      }

      sb.append(functionNames.getFunctionName(function) + " " +
                functionInfo.toString() +
                " Calls: " + depFunctionNames + "\n");
    }

    return sb.toString();
  }

  /**
   * Query the DefinitionProvider for the list of definitions that
   * correspond to a given qualified name subtree.  Return null if
   * DefinitionProvider does not contain an entry for a given name,
   * one or more of the values returned by getDeclarations is not
   * callable, or the "name" node is not a GETPROP or NAME.
   *
   * @param definitionProvider The name reference graph
   * @param name Query node
   * @return non-empty definition list or null
   */
  private static Collection<Definition> getCallableDefinitions(
      DefinitionProvider definitionProvider, Node name) {
    if (name.isGetProp() || name.isName()) {
      List<Definition> result = Lists.newArrayList();

      Collection<Definition> decls =
          definitionProvider.getDefinitionsReferencedAt(name);
      if (decls == null) {
        return null;
      }

      for (Definition current : decls) {
        Node rValue = current.getRValue();
        if ((rValue != null) && rValue.isFunction()) {
          result.add(current);
        } else {
          return null;
        }
      }

      return result;
    } else if (name.isOr() || name.isHook()) {
      Node firstVal;
      if (name.isHook()) {
        firstVal = name.getFirstChild().getNext();
      } else {
        firstVal = name.getFirstChild();
      }

      Collection<Definition> defs1 = getCallableDefinitions(definitionProvider,
                                                            firstVal);
      Collection<Definition> defs2 = getCallableDefinitions(definitionProvider,
                                                            firstVal.getNext());
      if (defs1 != null && defs2 != null) {
        defs1.addAll(defs2);
        return defs1;
      } else {
        return null;
      }
    } else if (NodeUtil.isFunctionExpression(name)) {
      // The anonymous function reference is also the definition.
      // TODO(user) Change SimpleDefinitionFinder so it is possible to query for
      // function expressions by function node.

      // isExtern is false in the call to the constructor for the
      // FunctionExpressionDefinition below because we know that
      // getCallableDefinitions() will only be called on the first
      // child of a call and thus the function expression
      // definition will never be an extern.
      return Lists.newArrayList(
          (Definition)
              new DefinitionsRemover.FunctionExpressionDefinition(name, false));
    } else {
      return null;
    }
  }

  /**
   * Propagate side effect information by building a graph based on
   * call site information stored in FunctionInformation and the
   * DefinitionProvider and then running GraphReachability to
   * determine the set of functions that have side effects.
   */
  private void propagateSideEffects() {
    // Nodes are function declarations; Edges are function call sites.
    DiGraph<FunctionInformation, Node> sideEffectGraph =
        LinkedDirectedGraph.createWithoutAnnotations();

    // create graph nodes
    for (FunctionInformation functionInfo : functionSideEffectMap.values()) {
      sideEffectGraph.createNode(functionInfo);
    }

    // add connections to called functions and side effect root.
    for (FunctionInformation functionInfo : functionSideEffectMap.values()) {
      if (!functionInfo.mayHaveSideEffects()) {
        continue;
      }

      for (Node callSite : functionInfo.getCallsInFunctionBody()) {
        Node callee = callSite.getFirstChild();
        Collection<Definition> defs =
            getCallableDefinitions(definitionProvider, callee);
        if (defs == null) {
          // Definition set is not complete or eligible.  Possible
          // causes include:
          //  * "callee" is not of type NAME or GETPROP.
          //  * One or more definitions are not functions.
          //  * One or more definitions are complex.
          //    (e.i. return value of a call that returns a function).
          functionInfo.setTaintsUnknown();
          break;
        }

        for (Definition def : defs) {
          Node defValue = def.getRValue();
          FunctionInformation dep = functionSideEffectMap.get(defValue);
          Preconditions.checkNotNull(dep);
          sideEffectGraph.connect(dep, callSite, functionInfo);
        }
      }
    }

    // Propagate side effect information to a fixed point.
    FixedPointGraphTraversal.newTraversal(new SideEffectPropagationCallback())
        .computeFixedPoint(sideEffectGraph);

    // Mark remaining functions "pure".
    for (FunctionInformation functionInfo : functionSideEffectMap.values()) {
      if (functionInfo.mayBePure()) {
        functionInfo.setIsPure();
      }
    }
  }

  /**
   * Set no side effect property at pure-function call sites.
   */
  private void markPureFunctionCalls() {
    for (Node callNode : allFunctionCalls) {
      Node name = callNode.getFirstChild();
      Collection<Definition> defs =
          getCallableDefinitions(definitionProvider, name);
      // Default to side effects, non-local results
      Node.SideEffectFlags flags = new Node.SideEffectFlags();
      if (defs == null) {
        flags.setMutatesGlobalState();
        flags.setThrows();
        flags.setReturnsTainted();
      } else {
        flags.clearAllFlags();
        for (Definition def : defs) {
          FunctionInformation functionInfo =
              functionSideEffectMap.get(def.getRValue());
          Preconditions.checkNotNull(functionInfo);
          // TODO(johnlenz): set the arguments separately from the
          // global state flag.
          if (functionInfo.mutatesGlobalState()) {
            flags.setMutatesGlobalState();
          }

          if (functionInfo.functionThrows) {
            flags.setThrows();
          }

          if (!callNode.isNew()) {
            if (functionInfo.taintsThis) {
              flags.setMutatesThis();
            }
          }

          if (functionInfo.taintsReturn) {
            flags.setReturnsTainted();
          }

          if (flags.areAllFlagsSet()) {
            break;
          }
        }
      }

      // Handle special cases (Math, RegExp)
      if (callNode.isCall()) {
        Preconditions.checkState(compiler != null);
        if (!NodeUtil.functionCallHasSideEffects(callNode, compiler)) {
          flags.clearSideEffectFlags();
        }
      } else if (callNode.isNew()) {
        // Handle known cases now (Object, Date, RegExp, etc)
        if (!NodeUtil.constructorCallHasSideEffects(callNode)) {
          flags.clearSideEffectFlags();
        }
      }

      callNode.setSideEffectFlags(flags.valueOf());
    }
  }

  /**
   * Gather list of functions, functions with @nosideeffects
   * annotations, call sites, and functions that may mutate variables
   * not defined in the local scope.
   */
  private class FunctionAnalyzer implements ScopedCallback {
    private final boolean inExterns;

    FunctionAnalyzer(boolean inExterns) {
      this.inExterns = inExterns;
    }

    @Override
    public boolean shouldTraverse(NodeTraversal traversal,
                                  Node node,
                                  Node parent) {

      // Functions need to be processed as part of pre-traversal so an
      // entry for the enclosing function exists in the
      // FunctionInformation map when processing assignments and calls
      // inside visit.
      if (node.isFunction()) {
        Node gramp = parent.getParent();
        visitFunction(traversal, node, parent, gramp);
      }

      return true;
    }

    @Override
    public void visit(NodeTraversal traversal, Node node, Node parent) {

      if (inExterns) {
        return;
      }

      if (!NodeUtil.nodeTypeMayHaveSideEffects(node)
          && !node.isReturn()) {
        return;
      }

      if (node.isCall() || node.isNew()) {
        allFunctionCalls.add(node);
      }

      Node enclosingFunction = traversal.getEnclosingFunction();
      if (enclosingFunction != null) {
        FunctionInformation sideEffectInfo =
            functionSideEffectMap.get(enclosingFunction);
        Preconditions.checkNotNull(sideEffectInfo);

        if (NodeUtil.isAssignmentOp(node)) {
          visitAssignmentOrUnaryOperator(
              sideEffectInfo, traversal.getScope(),
              node, node.getFirstChild(), node.getLastChild());
        } else {
          switch(node.getType()) {
            case Token.CALL:
            case Token.NEW:
              visitCall(sideEffectInfo, node);
              break;
            case Token.DELPROP:
            case Token.DEC:
            case Token.INC:
              visitAssignmentOrUnaryOperator(
                  sideEffectInfo, traversal.getScope(),
                  node, node.getFirstChild(), null);
              break;
            case Token.NAME:
              // Variable definition are not side effects.
              // Just check that the name appears in the context of a
              // variable declaration.
              Preconditions.checkArgument(
                  NodeUtil.isVarDeclaration(node));
              Node value = node.getFirstChild();
              // Assignment to local, if the value isn't a safe local value,
              // new object creation or literal or known primitive result
              // value, add it to the local blacklist.
              if (value != null && !NodeUtil.evaluatesToLocalValue(value)) {
                Scope scope = traversal.getScope();
                Var var = scope.getVar(node.getString());
                sideEffectInfo.blacklistLocal(var);
              }
              break;
            case Token.THROW:
              visitThrow(sideEffectInfo);
              break;
            case Token.RETURN:
              if (node.hasChildren()
                  && !NodeUtil.evaluatesToLocalValue(node.getFirstChild())) {
                sideEffectInfo.setTaintsReturn();
              }
              break;
            default:
              throw new IllegalArgumentException(
                  "Unhandled side effect node type " +
                  Token.name(node.getType()));
          }
        }
      }
    }

    @Override
    public void enterScope(NodeTraversal t) {
      // Nothing to do.
    }

    @Override
    public void exitScope(NodeTraversal t) {
      if (t.inGlobalScope()) {
        return;
      }

      // Handle deferred local variable modifications:
      //
      FunctionInformation sideEffectInfo =
        functionSideEffectMap.get(t.getScopeRoot());
      if (sideEffectInfo.mutatesGlobalState()){
        sideEffectInfo.resetLocalVars();
        return;
      }

      for (Iterator<Var> i = t.getScope().getVars(); i.hasNext();) {
        Var v = i.next();
        boolean localVar = false;
        // Parameters and catch values come can from other scopes.
        if (v.getParentNode().isVar()) {
          // TODO(johnlenz): create a useful parameter list
          sideEffectInfo.knownLocals.add(v.getName());
          localVar = true;
        }

        // Take care of locals that might have been tainted.
        if (!localVar || sideEffectInfo.blacklisted.contains(v)) {
          if (sideEffectInfo.taintedLocals.contains(v)) {
            // If the function has global side-effects
            // don't bother with the local side-effects.
            sideEffectInfo.setTaintsUnknown();
            sideEffectInfo.resetLocalVars();
            break;
          }
        }
      }

      sideEffectInfo.taintedLocals = null;
      sideEffectInfo.blacklisted = null;
    }


    /**
     * Record information about the side effects caused by an
     * assignment or mutating unary operator.
     *
     * If the operation modifies this or taints global state, mark the
     * enclosing function as having those side effects.
     * @param op operation being performed.
     * @param lhs The store location (name or get) being operated on.
     * @param rhs The right have value, if any.
     */
    private void visitAssignmentOrUnaryOperator(
        FunctionInformation sideEffectInfo,
        Scope scope, Node op, Node lhs, Node rhs) {
      if (lhs.isName()) {
        Var var = scope.getVar(lhs.getString());
        if (var == null || var.scope != scope) {
          sideEffectInfo.setTaintsGlobalState();
        } else {
          // Assignment to local, if the value isn't a safe local value,
          // a literal or new object creation, add it to the local blacklist.
          // parameter values depend on the caller.

          // Note: other ops result in the name or prop being assigned a local
          // value (x++ results in a number, for instance)
          Preconditions.checkState(
              NodeUtil.isAssignmentOp(op)
              || isIncDec(op) || op.isDelProp());
          if (rhs != null
              && op.isAssign()
              && !NodeUtil.evaluatesToLocalValue(rhs)) {
            sideEffectInfo.blacklistLocal(var);
          }
        }
      } else if (NodeUtil.isGet(lhs)) {
        if (lhs.getFirstChild().isThis()) {
          sideEffectInfo.setTaintsThis();
        } else {
          Var var = null;
          Node objectNode = lhs.getFirstChild();
          if (objectNode.isName()) {
            var = scope.getVar(objectNode.getString());
          }
          if (var == null || var.scope != scope) {
            sideEffectInfo.setTaintsUnknown();
          } else {
            // Maybe a local object modification.  We won't know for sure until
            // we exit the scope and can validate the value of the local.
            //
            sideEffectInfo.addTaintedLocalObject(var);
          }
        }
      } else {
        // TODO(johnlenz): track down what is inserting NULL on the LHS
        // of an assign.

        // The only valid LHS expressions are NAME, GETELEM, or GETPROP.
        // throw new IllegalStateException(
        //     "Unexpected LHS expression:" + lhs.toStringTree()
        //    + ", parent: " + op.toStringTree() );
        sideEffectInfo.setTaintsUnknown();
      }
    }

    /**
     * Record information about a call site.
     */
    private void visitCall(FunctionInformation sideEffectInfo, Node node) {
      // Handle special cases (Math, RegExp)
      if (node.isCall()
          && !NodeUtil.functionCallHasSideEffects(node, compiler)) {
        return;
      }

      // Handle known cases now (Object, Date, RegExp, etc)
      if (node.isNew()
          && !NodeUtil.constructorCallHasSideEffects(node)) {
        return;
      }

      sideEffectInfo.appendCall(node);
    }

    /**
     * Record function and check for @nosideeffects annotations.
     */
    private void visitFunction(NodeTraversal traversal,
                               Node node,
                               Node parent,
                               Node gramp) {
      Preconditions.checkArgument(!functionSideEffectMap.containsKey(node));

      FunctionInformation sideEffectInfo = new FunctionInformation(inExterns);
      functionSideEffectMap.put(node, sideEffectInfo);

      if (inExterns) {
        JSType jstype = node.getJSType();
        boolean knownLocalResult = false;
        FunctionType functionType = JSType.toMaybeFunctionType(jstype);
        if (functionType != null) {
          JSType jstypeReturn = functionType.getReturnType();
          if (isLocalValueType(jstypeReturn, true)) {
            knownLocalResult = true;
          }
        }
        if (!knownLocalResult) {
          sideEffectInfo.setTaintsReturn();
        }
      }

      JSDocInfo info = getJSDocInfoForFunction(node, parent, gramp);
      if (info != null) {
        boolean hasSpecificSideEffects = false;
        if (hasSideEffectsThisAnnotation(info)) {
          if (inExterns) {
            hasSpecificSideEffects = true;
            sideEffectInfo.setTaintsThis();
          } else {
            traversal.report(node, INVALID_MODIFIES_ANNOTATION);
          }
        }

        if (hasSideEffectsArgumentsAnnotation(info)) {
          if (inExterns) {
            hasSpecificSideEffects = true;
            sideEffectInfo.setTaintsArguments();
          } else {
            traversal.report(node, INVALID_MODIFIES_ANNOTATION);
          }
        }

        if (inExterns && !info.getThrownTypes().isEmpty()) {
          hasSpecificSideEffects = true;
          sideEffectInfo.setFunctionThrows();
        }

        if (!hasSpecificSideEffects) {
          if (hasNoSideEffectsAnnotation(info)) {
            if (inExterns) {
              sideEffectInfo.setIsPure();
            } else {
              traversal.report(node, INVALID_NO_SIDE_EFFECT_ANNOTATION);
            }
          } else if (inExterns) {
            sideEffectInfo.setTaintsGlobalState();
          }
        }
      } else {
        if (inExterns) {
          sideEffectInfo.setTaintsGlobalState();
        }
      }
    }

    /**
     * @return Whether the jstype is something known to be a local value.
     */
    private boolean isLocalValueType(JSType jstype, boolean recurse) {
      Preconditions.checkNotNull(jstype);
      JSType subtype =  jstype.getGreatestSubtype(
          compiler.getTypeRegistry().getNativeType(JSTypeNative.OBJECT_TYPE));
      // If the type includes anything related to a object type, don't assume
      // anything about the locality of the value.
      return subtype.isNoType();
    }

    /**
     * Record that the enclosing function throws.
     */
    private void visitThrow(FunctionInformation sideEffectInfo) {
      sideEffectInfo.setFunctionThrows();
    }

    /**
     * Get the doc info associated with the function.
     */
    private JSDocInfo getJSDocInfoForFunction(
        Node node, Node parent, Node gramp) {
      JSDocInfo info = node.getJSDocInfo();
      if (info != null) {
        return info;
      } else if (parent.isName()) {
        return gramp.hasOneChild() ? gramp.getJSDocInfo() : null;
      } else if (parent.isAssign()) {
        return parent.getJSDocInfo();
      } else {
        return null;
      }
    }

    /**
     * Get the value of the @nosideeffects annotation stored in the
     * doc info.
     */
    private boolean hasNoSideEffectsAnnotation(JSDocInfo docInfo) {
      Preconditions.checkNotNull(docInfo);
      return docInfo.isNoSideEffects();
    }

    /**
     * Get the value of the @modifies{this} annotation stored in the
     * doc info.
     */
    private boolean hasSideEffectsThisAnnotation(JSDocInfo docInfo) {
      Preconditions.checkNotNull(docInfo);
      return (docInfo.getModifies().contains("this"));
    }

    /**
     * @returns Whether the @modifies annotation includes "arguments"
     * or any named parameters.
     */
    private boolean hasSideEffectsArgumentsAnnotation(JSDocInfo docInfo) {
      Preconditions.checkNotNull(docInfo);
      Set<String> modifies = docInfo.getModifies();
      // TODO(johnlenz): if we start tracking parameters individually
      // this should simply be a check for "arguments".
      return (modifies.size() > 1
          || (modifies.size() == 1 && !modifies.contains("this")));
    }
  }

  private static boolean isIncDec(Node n) {
    int type = n.getType();
    return (type == Token.INC || type == Token.DEC);
  }

  /**
   * @return Whether the node is known to be a value that is not a reference
   *     outside the local scope.
   */
  @SuppressWarnings("unused")
  private static boolean isKnownLocalValue(final Node value) {
    Predicate<Node> taintingPredicate = new Predicate<Node>() {
      @Override
      public boolean apply(Node value) {
        switch (value.getType()) {
          case Token.ASSIGN:
            // The assignment might cause an alias, look at the LHS.
            return false;
          case Token.THIS:
            // TODO(johnlenz): maybe redirect this to be a tainting list for 'this'.
            return false;
          case Token.NAME:
            // TODO(johnlenz): add to local tainting list, if the NAME
            // is known to be a local.
            return false;
          case Token.GETELEM:
          case Token.GETPROP:
            // There is no information about the locality of object properties.
            return false;
          case Token.CALL:
            // TODO(johnlenz): add to local tainting list, if the call result
            // is not known to be a local result.
            return false;
        }
        return false;
      }
    };

    return NodeUtil.evaluatesToLocalValue(value, taintingPredicate);
  }

  /**
   * Callback that propagates side effect information across call sites.
   */
  private static class SideEffectPropagationCallback
      implements EdgeCallback<FunctionInformation, Node> {
    @Override
    public boolean traverseEdge(FunctionInformation callee,
                                Node callSite,
                                FunctionInformation caller) {
      Preconditions.checkArgument(callSite.isCall() ||
                                  callSite.isNew());

      boolean changed = false;
      if (!caller.mutatesGlobalState() && callee.mutatesGlobalState()) {
        caller.setTaintsGlobalState();
        changed = true;
      }

      if (!caller.functionThrows() && callee.functionThrows()) {
        caller.setFunctionThrows();
        changed = true;
      }

      if (callee.mutatesThis()) {
        // Side effects only propagate via regular calls.
        // Calling a constructor that modifies "this" has no side effects.
        if (!callSite.isNew()) {
          Node objectNode = getCallThisObject(callSite);
          if (objectNode != null && objectNode.isName()
              && !isCallOrApply(callSite)) {
            // Exclude ".call" and ".apply" as the value may still be
            // null or undefined. We don't need to worry about this with a
            // direct method call because null and undefined don't have any
            // properties.
            String name = objectNode.getString();

            // TODO(nicksantos): Turn this back on when locals-tracking
            // is fixed. See testLocalizedSideEffects11.
            //if (!caller.knownLocals.contains(name)) {
              if (!caller.mutatesGlobalState()) {
                caller.setTaintsGlobalState();
                changed = true;
              }
            //}
          } else if (objectNode != null && objectNode.isThis()) {
            if (!caller.mutatesThis()) {
              caller.setTaintsThis();
              changed = true;
            }
          } else if (objectNode != null
              && NodeUtil.evaluatesToLocalValue(objectNode)
              && !isCallOrApply(callSite)) {
            // Modifying 'this' on a known local object doesn't change any
            // significant state.
            // TODO(johnlenz): We can improve this by including literal values
            // that we know for sure are not null.
          } else if (!caller.mutatesGlobalState()) {
            caller.setTaintsGlobalState();
            changed = true;
          }
        }
      }

      return changed;
    }
  }

  /**
   * Analyze a call site and extract the node that will be act as
   * "this" inside the call, which is either the object part of the
   * qualified function name, the first argument to the call in the
   * case of ".call" and ".apply" or null if object is not specified
   * in either of those ways.
   *
   * @return node that will act as "this" for the call.
   */
  private static Node getCallThisObject(Node callSite) {
    Node callTarget = callSite.getFirstChild();
    if (!NodeUtil.isGet(callTarget)) {

      // "this" is not specified explicitly; call modifies global "this".
      return null;
    }

    String propString = callTarget.getLastChild().getString();
    if (propString.equals("call") || propString.equals("apply")) {
      return callTarget.getNext();
    } else {
      return callTarget.getFirstChild();
    }
  }

  private static boolean isCallOrApply(Node callSite) {
    Node callTarget = callSite.getFirstChild();
    if (NodeUtil.isGet(callTarget)) {
      String propString = callTarget.getLastChild().getString();
      if (propString.equals("call") || propString.equals("apply")) {
        return true;
      }
    }
    return false;
  }

  /**
   * Keeps track of a function's known side effects by type and the
   * list of calls that appear in a function's body.
   */
  private static class FunctionInformation {
    private final boolean extern;
    private final List<Node> callsInFunctionBody = Lists.newArrayList();
    private Set<Var> blacklisted = Sets.newHashSet();
    private Set<Var> taintedLocals = Sets.newHashSet();
    private Set<String> knownLocals = Sets.newHashSet();
    private boolean pureFunction = false;
    private boolean functionThrows = false;
    private boolean taintsGlobalState = false;
    private boolean taintsThis = false;
    private boolean taintsArguments = false;
    private boolean taintsUnknown = false;
    private boolean taintsReturn = false;

    FunctionInformation(boolean extern) {
      this.extern = extern;
      checkInvariant();
    }

    /**
     * @param var
     */
    void addTaintedLocalObject(Var var) {
      taintedLocals.add(var);
    }

    void resetLocalVars() {
      blacklisted = null;
      taintedLocals = null;
      knownLocals = Collections.emptySet();
    }

    /**
     * @param var
     */
    public void blacklistLocal(Var var) {
      blacklisted.add(var);
    }

    /**
     * @returns false if function known to have side effects.
     */
    boolean mayBePure() {
      return !(functionThrows ||
               taintsGlobalState ||
               taintsThis ||
               taintsArguments ||
               taintsUnknown);
    }

    /**
     * @returns false if function known to be pure.
     */
    boolean mayHaveSideEffects() {
      return !pureFunction;
    }

    /**
     * Mark the function as being pure.
     */
    void setIsPure() {
      pureFunction = true;
      checkInvariant();
    }

    /**
     * Marks the function as having "modifies globals" side effects.
     */
    void setTaintsGlobalState() {
      taintsGlobalState = true;
      checkInvariant();
    }

    /**
     * Marks the function as having "modifies this" side effects.
     */
    void setTaintsThis() {
      taintsThis = true;
      checkInvariant();
    }

    /**
     * Marks the function as having "modifies arguments" side effects.
     */
    void setTaintsArguments() {
      taintsArguments = true;
      checkInvariant();
    }

    /**
     * Marks the function as having "throw" side effects.
     */
    void setFunctionThrows() {
      functionThrows = true;
      checkInvariant();
    }

    /**
     * Marks the function as having "complex" side effects that are
     * not otherwise explicitly tracked.
     */
    void setTaintsUnknown() {
      taintsUnknown = true;
      checkInvariant();
    }

    /**
     * Marks the function as having non-local return result.
     */
    void setTaintsReturn() {
      taintsReturn = true;
      checkInvariant();
    }


    /**
     * Returns true if function mutates global state.
     */
    boolean mutatesGlobalState() {
      // TODO(johnlenz): track arguments separately.
      return taintsGlobalState || taintsArguments || taintsUnknown;
    }

    /**
     * Returns true if function mutates "this".
     */
    boolean mutatesThis() {
      return taintsThis;
    }

    /**
     * Returns true if function has an explicit "throw".
     */
    boolean functionThrows() {
      return functionThrows;
    }

    /**
     * Verify internal consistency.  Should be called at the end of
     * every method that mutates internal state.
     */
    private void checkInvariant() {
      boolean invariant = mayBePure() || mayHaveSideEffects();
      if (!invariant) {
        throw new IllegalStateException("Invariant failed.  " + toString());
      }
    }

    /**
     * Add a CALL or NEW node to the list of calls this function makes.
     */
    void appendCall(Node callNode) {
      callsInFunctionBody.add(callNode);
    }

    /**
     * Gets the list of CALL and NEW nodes.
     */
    List<Node> getCallsInFunctionBody() {
      return callsInFunctionBody;
    }

    @Override
    public String toString() {
      List<String> status = Lists.newArrayList();
      if (extern) {
        status.add("extern");
      }

      if (pureFunction) {
        status.add("pure");
      }

      if (taintsThis) {
        status.add("this");
      }

      if (taintsGlobalState) {
        status.add("global");
      }

      if (functionThrows) {
        status.add("throw");
      }

      if (taintsUnknown) {
        status.add("complex");
      }

      return "Side effects: " + status.toString();
    }
  }

  /**
   * A compiler pass that constructs a reference graph and drives
   * the PureFunctionIdentifier across it.
   */
  static class Driver implements CompilerPass {
    private final AbstractCompiler compiler;
    private final String reportPath;
    private final boolean useNameReferenceGraph;

    Driver(AbstractCompiler compiler, String reportPath,
        boolean useNameReferenceGraph) {
      this.compiler = compiler;
      this.reportPath = reportPath;
      this.useNameReferenceGraph = useNameReferenceGraph;
    }

    @Override
    public void process(Node externs, Node root) {
      DefinitionProvider definitionProvider = null;
      if (useNameReferenceGraph) {
        NameReferenceGraphConstruction graphBuilder =
            new NameReferenceGraphConstruction(compiler);
        graphBuilder.process(externs, root);
        definitionProvider = graphBuilder.getNameReferenceGraph();
      } else {
        SimpleDefinitionFinder defFinder = new SimpleDefinitionFinder(compiler);
        defFinder.process(externs, root);
        definitionProvider = defFinder;
      }

      PureFunctionIdentifier pureFunctionIdentifier =
          new PureFunctionIdentifier(compiler, definitionProvider);
      pureFunctionIdentifier.process(externs, root);

      if (reportPath != null) {
        try {
          Files.write(pureFunctionIdentifier.getDebugReport(),
              new File(reportPath),
              Charsets.UTF_8);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }
}