Package com.google.javascript.jscomp

Source Code of com.google.javascript.jscomp.PureFunctionIdentifier$FunctionAnalyzer

/*
* 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 static java.nio.charset.StandardCharsets.UTF_8;

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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
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.
*
* @author johnlenz@google.com (John Lenz)
*
* 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);
          if (functionInfo.mutatesGlobalState()) {
            flags.setMutatesGlobalState();
          }

          if (functionInfo.mutatesArguments()) {
            flags.setMutatesArguments();
          }

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

          if (!callNode.isNew()) {
            if (functionInfo.taintsThis()) {
              // A FunctionInfo for "f" maps to both "f()" and "f.call()" nodes.
              if (isCallOrApply(callNode)) {
                flags.setMutatesArguments();
              } else {
                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 param = v.getParentNode().isParamList();
        if (param &&
            !sideEffectInfo.blacklisted().contains(v) &&
            sideEffectInfo.taintedLocals().contains(v)) {
          sideEffectInfo.setTaintsArguments();
          continue;
        }

        boolean localVar = false;
        // Parameters and catch values come can from other scopes.
        if (v.getParentNode().isVar()) {
          // TODO(johnlenz): create a useful parameter list
//           sideEffectInfo.addKnownLocal(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 = Collections.emptySet();
      sideEffectInfo.blacklisted = Collections.emptySet();
    }


    /**
     * 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)) {
            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) {
      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"));
    }

    /**
     * @return 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 (!caller.mutatesGlobalState() && callee.mutatesArguments() &&
          !NodeUtil.allArgsUnescapedLocal(callSite)) {
        // TODO(nicksantos): We should track locals in the caller
        // and using that to be more precise. See testMutatesArguments3.
        caller.setTaintsGlobalState();
        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()) {
          // Notice that we're using "mutatesThis" from the callee
          // FunctionInfo. If the call site is actually a .call or .apply, then
          // the "this" is going to be one of its arguments.
          boolean isCallOrApply = isCallOrApply(callSite);
          Node objectNode = isCallOrApply ?
              callSite.getFirstChild().getNext() :
              callSite.getFirstChild().getFirstChild();
          if (objectNode != null && objectNode.isName()
              && !isCallOrApply) {
            // 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.

            // 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) {
            // 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;
    }
  }

  private static boolean isCallOrApply(Node callSite) {
    return NodeUtil.isFunctionObjectCall(callSite) ||
      NodeUtil.isFunctionObjectApply(callSite);
  }

  /**
   * 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 List<Node> callsInFunctionBody = null;
    private Set<Var> blacklisted = null;
    private Set<Var> taintedLocals = null;
//     private Set<String> knownLocals = null;
    private int bitmask = 0;

    private static final int EXTERN_MASK = 1 << 0;
    private static final int PURE_FUNCTION_MASK = 1 << 1;
    private static final int FUNCTION_THROWS_MASK = 1 << 2;
    private static final int TAINTS_GLOBAL_STATE_MASK = 1 << 3;
    private static final int TAINTS_THIS_MASK = 1 << 4;
    private static final int TAINTS_ARGUMENTS_MASK = 1 << 5;
    private static final int TAINTS_UNKNOWN_MASK = 1 << 6;
    private static final int TAINTS_RETURN_MASK = 1 << 7;

    private void setMask(int mask, boolean value) {
      if (value) {
        bitmask |= mask;
      } else {
        bitmask &= ~mask;
      }
    }

    private boolean getMask(int mask) {
      return (bitmask & mask) != 0;
    }

    private boolean extern() {
      return getMask(EXTERN_MASK);
    }

    private boolean pureFunction() {
      return getMask(PURE_FUNCTION_MASK);
    }

    private boolean taintsGlobalState() {
      return getMask(TAINTS_GLOBAL_STATE_MASK);
    }

    private boolean taintsThis() {
      return getMask(TAINTS_THIS_MASK);
    }

    private boolean taintsArguments() {
      return getMask(TAINTS_ARGUMENTS_MASK);
    }

    private boolean taintsUnknown() {
      return getMask(TAINTS_UNKNOWN_MASK);
    }

    private boolean taintsReturn() {
      return getMask(TAINTS_RETURN_MASK);
    }

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

    FunctionInformation(boolean extern) {
      this.setMask(EXTERN_MASK, extern);
      checkInvariant();
    }

    public Set<Var> taintedLocals() {
      if (taintedLocals == null) {
        return Collections.emptySet();
      }
      return taintedLocals;
    }

    /**
     * @param var
     */
    void addTaintedLocalObject(Var var) {
      if (taintedLocals == null) {
        taintedLocals = new HashSet<>();
      }
      taintedLocals.add(var);
    }

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

//     public void addKnownLocal(String name) {
//       if (knownLocals == null) {
//         knownLocals = new HashSet<>();
//       }
//       knownLocals.add(name);
//     }

    public Set<Var> blacklisted() {
      if (blacklisted == null) {
        return Collections.emptySet();
      }
      return blacklisted;
    }

    /**
     * @param var
     */
    public void blacklistLocal(Var var) {
      if (blacklisted == null) {
        blacklisted = new HashSet<>();
      }
      blacklisted.add(var);
    }

    /**
     * @return false if function known to have side effects.
     */
    boolean mayBePure() {
      return !getMask(
          FUNCTION_THROWS_MASK
          | TAINTS_GLOBAL_STATE_MASK
          | TAINTS_THIS_MASK
          | TAINTS_ARGUMENTS_MASK
          | TAINTS_UNKNOWN_MASK);
    }

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

    /**
     * Mark the function as being pure.
     */
    void setIsPure() {
      this.setMask(PURE_FUNCTION_MASK, true);
      checkInvariant();
    }

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

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

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

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

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

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


    /**
     * Returns true if function mutates global state.
     */
    boolean mutatesGlobalState() {
      return getMask(TAINTS_GLOBAL_STATE_MASK | TAINTS_UNKNOWN_MASK);
    }


    /**
     * Returns true if function mutates its arguments.
     */
    boolean mutatesArguments() {
      return getMask(
          TAINTS_GLOBAL_STATE_MASK
          | TAINTS_ARGUMENTS_MASK
          | TAINTS_UNKNOWN_MASK);
    }

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

    /**
     * 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) {
      if (callsInFunctionBody == null) {
        callsInFunctionBody = new ArrayList<>();
      }
      callsInFunctionBody.add(callNode);
    }

    /**
     * Gets the list of CALL and NEW nodes.
     */
    List<Node> getCallsInFunctionBody() {
      if (callsInFunctionBody == null) {
        return Collections.emptyList();
      }
      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),
              UTF_8);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }
}
TOP

Related Classes of com.google.javascript.jscomp.PureFunctionIdentifier$FunctionAnalyzer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.