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

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

Go to download

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

The newest version!
/*
 * 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 com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.NodeUtil.Visitor;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

/**
 * A nifty set of functions to deal with the issues of replacing function parameters with a set of
 * call argument expressions.
 */
class FunctionArgumentInjector {

  // A string to use to represent "this".  Anything that is not a valid
  // identifier can be used, so we use "this".
  static final String THIS_MARKER = "this";

  static final String REST_MARKER = "rest param";

  private final AstAnalyzer astAnalyzer;

  FunctionArgumentInjector(AstAnalyzer astAnalyzer) {
    this.astAnalyzer = astAnalyzer;
  }

  /**
   * With the map provided, replace the names with expression trees.
   *
   * @param node The root node of the tree within which to perform the substitutions.
   * @param parent The parent root node.
   * @param replacements The map of names to template node trees with which to replace the name
   *     Nodes.
   * @return The root node or its replacement.
   */
  Node inject(AbstractCompiler compiler, Node node, Node parent, Map replacements) {
    return inject(compiler, node, parent, replacements, /* replaceThis= */ true);
  }

  private Node inject(
      AbstractCompiler compiler,
      Node node,
      Node parent,
      Map replacements,
      boolean replaceThis) {
    if (node.isName()) {
      Node replacementTemplate = replacements.get(node.getString());
      if (replacementTemplate != null) {
        // This should not be replacing declared names.
        checkState(!(parent.isFunction() || parent.isVar() || parent.isCatch()), parent);
        // The name may need to be replaced more than once,
        // so we need to clone the node.
        Node replacement = replacementTemplate.cloneTree();
        node.replaceWith(replacement);
        return replacement;
      }
    } else if (replaceThis && node.isThis()) {
      Node replacementTemplate = replacements.get(THIS_MARKER);
      checkNotNull(replacementTemplate);
      if (!replacementTemplate.isThis()) {
        // The name may need to be replaced more than once,
        // so we need to clone the node.
        Node replacement = replacementTemplate.cloneTree();
        node.replaceWith(replacement);

        // Remove the value.  This isn't required but it ensures that we won't
        // inject side-effects multiple times as it will trigger the null
        // check above if we do.
        if (compiler.getAstAnalyzer().mayHaveSideEffects(replacementTemplate)) {
          replacements.remove(THIS_MARKER);
        }

        return replacement;
      }
    } else if (node.isFunction() && !node.isArrowFunction()) {
      // Once we enter another non-arrow function the "this" value changes. Don't try
      // to replace it within an inner scope.
      replaceThis = false;
    }

    for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
      // We have to reassign c in case it was replaced, because the removed c's
      // getNext() would no longer be correct.
      c = inject(compiler, c, node, replacements, replaceThis);
    }

    return node;
  }

  /** Get a mapping for function parameter names to call arguments. */
  ImmutableMap getFunctionCallParameterMap(
      final Node fnNode, Node callNode, Supplier safeNameIdSupplier) {
    checkNotNull(fnNode);
    // Create an parameterName -> expression map
    ImmutableMap.Builder argMap = ImmutableMap.builder();

    // CALL NODE: [ NAME, ARG1, ARG2, ... ]
    Node cArg = callNode.getSecondChild();
    if (cArg != null && NodeUtil.isFunctionObjectCall(callNode)) {
      argMap.put(THIS_MARKER, cArg);
      cArg = cArg.getNext();
    } else {
      // 'apply' isn't supported yet.
      checkState(!NodeUtil.isFunctionObjectApply(callNode), callNode);
      argMap.put(THIS_MARKER, NodeUtil.newUndefinedNode(callNode));
    }

    for (Node fnParam = NodeUtil.getFunctionParameters(fnNode).getFirstChild();
        fnParam != null;
        fnParam = fnParam.getNext()) {
      if (cArg != null) {
        if (fnParam.isRest()) {
          checkState(fnParam.getOnlyChild().isName(), fnParam.getOnlyChild());
          Node array = IR.arraylit();
          array.srcrefTreeIfMissing(cArg);
          while (cArg != null) {
            array.addChildToBack(cArg.cloneTree());
            cArg = cArg.getNext();
          }
          argMap.put(fnParam.getOnlyChild().getString(), array);
          return argMap.buildOrThrow();
        } else {
          checkState(fnParam.isName(), fnParam);
          argMap.put(fnParam.getString(), cArg);
        }
        cArg = cArg.getNext();
      } else { // cArg != null
        if (fnParam.isRest()) {
          checkState(fnParam.getOnlyChild().isName(), fnParam);
          // No arguments for REST parameters
          Node array = IR.arraylit().srcref(fnParam);
          argMap.put(fnParam.getOnlyChild().getString(), array);
        } else {
          checkState(fnParam.isName(), fnParam);
          Node srcLocation = callNode;
          argMap.put(fnParam.getString(), NodeUtil.newUndefinedNode(srcLocation));
        }
      }
    }

    // Add temp names for arguments that don't have named parameters in the
    // called function.
    while (cArg != null) {
      String uniquePlaceholder = getUniqueAnonymousParameterName(safeNameIdSupplier);
      argMap.put(uniquePlaceholder, cArg);
      cArg = cArg.getNext();
    }

    return argMap.buildOrThrow();
  }

  /** Parameter names will be name unique when at a later time. */
  private static String getUniqueAnonymousParameterName(Supplier safeNameIdSupplier) {
    return "JSCompiler_inline_anon_param_" + safeNameIdSupplier.get();
  }

  /**
   * Retrieve a set of names that can not be safely substituted in place.
   *
   * 

Example:

   *
   *   function(a) {
   *     a = 0;
   *   }
   * 
* *

Inlining this without taking precautions would cause the call site value to be modified * (bad). */ Set findModifiedParameters(Node fnNode) { ImmutableSet names = getFunctionParameterSet(fnNode); Set unsafeNames = new LinkedHashSet<>(); return findModifiedParameters(fnNode.getLastChild(), names, unsafeNames, false); } /** * Check for uses of the named value that imply a pass-by-value parameter is expected. This is * used to prevent cases like: * *

function (x) { x=2; return x; } * *

We don't want "undefined" to be substituted for "x", and get undefined=2 * * @param n The node in question. * @param names The set of names to check. * @param unsafe The set of names that require aliases. * @param inInnerFunction Whether the inspection is occurring on a inner function. */ private static Set findModifiedParameters( Node n, ImmutableSet names, Set unsafe, boolean inInnerFunction) { checkArgument(unsafe != null); if (n.isName()) { if (names.contains(n.getString()) && (inInnerFunction || canNameValueChange(n))) { unsafe.add(n.getString()); } } else if (n.isFunction()) { // A function parameter can not be replaced with a direct inlined value // if it is referred to by an inner function. The inner function // can out live the call we are replacing, so inner function must // capture a unique name. This approach does not work within loop // bodies so those are forbidden elsewhere. inInnerFunction = true; } for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { findModifiedParameters(c, names, unsafe, inInnerFunction); } return unsafe; } /** * This is similar to NodeUtil.isLValue except that object properties and array member * modification aren't important ("o" in "o.a = 2" is still "o" after assignment, where in as "o = * x", "o" is now "x"). * *

This also looks for the redefinition of a name. function (x) {var x;} * * @param n The NAME node in question. */ private static boolean canNameValueChange(Node n) { return NodeUtil.isLValue(n) && !NodeUtil.getEnclosingStatement(n).isConst() && !NodeUtil.getEnclosingStatement(n).isLet(); } /** * Updates the set of parameter names that correspond to arguments from the call site that require * aliases. * *

If an argument requires a temp (e.g. it is side-effectful, affects mutable state or due to * other reasons), we need to evaluate it into a temporary at the callsite before inlining the * function block into that callsite. However, that argument's early evaluation could also change * the values of previous arguments. Hence, in this function we decide to hoist all those previous * args and evaluate them before this side-effectful argument, even if they are not actually * affected by that side-effectful argument. This simplifies the implementation and doesn't hurt * performance. * *

This method populates the given namesNeedingTemps set with all the parameter names that * require hoisting with a temp. * * @param fnNode The FUNCTION node to be inlined. * @param argMap The argument list for the call to fnNode. * @param namesNeedingTemps The set of names to update. */ void maybeAddTempsForCallArguments( AbstractCompiler compiler, Node fnNode, ImmutableMap argMap, Set namesNeedingTemps, CodingConvention convention) { if (argMap.isEmpty()) { // No arguments to check, we are done. return; } checkArgument(fnNode.isFunction(), fnNode); Node block = fnNode.getLastChild(); /* * This field holds the parameter name corresponding to the last side-effectful argument. Args * corresponding to all parameters before and including this parameter name would get hoisted * using temps. */ String requiresTempsUpToThisParameterName = ""; int argCount = argMap.size(); // We limit the "trivial" bodies to those where there is a single expression or // return, the expression is boolean isTrivialBody = (!block.hasChildren() || (block.hasOneChild() && !bodyMayHaveConditionalCode(block.getLastChild()))); boolean hasMinimalParameters = NodeUtil.isUndefined(argMap.get(THIS_MARKER)) && argCount <= 2; // this + one parameter // Get the list of parameters that may need temporaries due to side-effects. ImmutableSet namesAfterSideEffects = findParametersReferencedAfterSideEffect(argMap.keySet(), block); // Check for arguments that are evaluated more than once. for (Map.Entry entry : argMap.entrySet()) { String parameterName = entry.getKey(); Node cArg = entry.getValue(); if (namesNeedingTemps.contains(parameterName)) { requiresTempsUpToThisParameterName = parameterName; continue; } // Stores whether this arg need to get hoisted using a temporary boolean requiresTemporary = false; final int references = NodeUtil.getNameReferenceCount(block, parameterName); boolean argSideEffects = compiler.getAstAnalyzer().mayHaveSideEffects(cArg); if (!argSideEffects && references == 0) { requiresTemporary = false; } else if (isTrivialBody && hasMinimalParameters && references == 1 && !(NodeUtil.canBeSideEffected(cArg) && namesAfterSideEffects.contains(parameterName))) { // For functions with a trivial body, and where the parameter evaluation order // can't change, and there aren't any side-effect before the parameter, we can // avoid creating a temporary. // // This is done to help inline common trivial functions requiresTemporary = false; } else if (compiler.getAstAnalyzer().mayEffectMutableState(cArg) && references > 0) { // Note: Mutable arguments should be assigned to temps, as the // may be within in a loop: // function x(a) { // for(var i=0; i<0; i++) { // foo(a); // } // x( [] ); // The parameter in the call to foo should not become "[]". requiresTemporary = true; } else if (argSideEffects) { // even if there are no references, we still need to evaluate the // expression if it has side-effects. requiresTemporary = true; } else if (NodeUtil.canBeSideEffected(cArg) && namesAfterSideEffects.contains(parameterName)) { requiresTemporary = true; } else if (references > 1) { // Safe is a misnomer, this is a check for "large". switch (cArg.getToken()) { case NAME: String name = cArg.getString(); // Don't worry about whether this is global or local, just check if it is // "exported" in either case. requiresTemporary = (convention.isExported(name, true) || convention.isExported(name, false)); break; case THIS: requiresTemporary = false; break; case STRINGLIT: requiresTemporary = (cArg.getString().length() >= 2); break; default: requiresTemporary = !NodeUtil.isImmutableValue(cArg); break; } } if (requiresTemporary) { requiresTempsUpToThisParameterName = parameterName; } } if (!requiresTempsUpToThisParameterName.isEmpty()) { // mark all names upto requiresTempsUptoParameterName as namesNeedingTemps for (Map.Entry entry : argMap.entrySet()) { String parameterName = entry.getKey(); if (parameterName.equals(THIS_MARKER) && NodeUtil.isUndefined(argMap.get(THIS_MARKER))) { /* When there is no explicit this arg passed into the call, the argMap contains an entry * <"this", undefined Node>. See the `getFunctionCallParameterMap` method. * * We do not want to add an unnecessary temp for "this", when there wasn't an explicit * "this" passed in at the callsite. That is, we want to transform a call-site like: * {@code `foo(a,b)`} * to * {@code `let a$inline$0 = a; foo(a$inline$0 ,b)`} * and not * {@code `let this$inline$0 = this; let a$inline$0=a; foo(a$inline$0, b)`}. * Hence we skip generating a temporary for an implicit "this" arg */ continue; } // TODO: b/298828688 Use `NodeUtil.isImmutableValue` to detect some simple immutable cases // and skip generating temps for those as well. if (parameterName.equals(requiresTempsUpToThisParameterName)) { namesNeedingTemps.add(parameterName); break; } else { namesNeedingTemps.add(parameterName); } } } } /** * We consider a return or expression trivial if it doesn't contain a conditional expression or a * function. */ boolean bodyMayHaveConditionalCode(Node n) { if (!n.isReturn() && !n.isExprResult()) { return true; } return mayHaveConditionalCode(n); } /** * We consider an expression trivial if it doesn't contain a conditional expression or a function. */ boolean mayHaveConditionalCode(Node n) { for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { switch (c.getToken()) { case FUNCTION: case AND: case OR: case HOOK: case COALESCE: case OPTCHAIN_CALL: case OPTCHAIN_GETELEM: case OPTCHAIN_GETPROP: return true; default: break; } if (mayHaveConditionalCode(c)) { return true; } } return false; } /** * Bootstrap a traversal to look for parameters referenced after a non-local side-effect. * *

NOTE: This assumes no-inner functions. * * @param parameters The set of parameter names. * @param root The function code block. * @return The subset of parameters referenced after the first seen non-local side-effect. */ private ImmutableSet findParametersReferencedAfterSideEffect( ImmutableSet parameters, Node root) { // TODO(johnlenz): Consider using scope for this. Set locals = new LinkedHashSet<>(parameters); gatherLocalNames(root, locals); ReferencedAfterSideEffect collector = new ReferencedAfterSideEffect(parameters, ImmutableSet.copyOf(locals)); NodeUtil.visitPostOrder(root, collector, collector); return collector.getResults(); } /** * Collect parameter names referenced after a non-local side-effect. * *

Assumptions: * *

    *
  • We assume parameters are not modified in the function body (that is checked separately). *
  • There are no inner functions (also checked separately). *
* *

As we are trying to replace parameters with there passed in values we are interested in * anything that may affect those value. So, ignoring changes to local variables, we look for * things that may affect anything outside the local-state. Once such a side-effect is seen any * following reference to the function parameters are collected. These will need to be assigned to * temporaries to prevent changes to their value as would have happened during the function call. * *

To properly handle loop structures all references to the function parameters are recorded * and the decision to keep or throw away those references is deferred until exiting the loop * structure. */ private class ReferencedAfterSideEffect implements Visitor, Predicate { private final ImmutableSet parameters; private final ImmutableSet locals; private boolean sideEffectSeen = false; private final Set parametersReferenced = new LinkedHashSet<>(); private int loopsEntered = 0; ReferencedAfterSideEffect(ImmutableSet parameters, ImmutableSet locals) { this.parameters = parameters; this.locals = locals; } ImmutableSet getResults() { return ImmutableSet.copyOf(parametersReferenced); } @Override public boolean apply(Node node) { // Keep track of any loop structures entered. if (NodeUtil.isLoopStructure(node)) { loopsEntered++; } // If we have found all the parameters, don't bother looking // at the children. return !(sideEffectSeen && parameters.size() == parametersReferenced.size()); } boolean inLoop() { return loopsEntered != 0; } @Override public void visit(Node n) { // If we are exiting a loop. if (NodeUtil.isLoopStructure(n)) { loopsEntered--; if (!inLoop() && !sideEffectSeen) { // Now that the loops has been fully traversed and // no side-effects have been seen, throw away // the references seen in them. parametersReferenced.clear(); } } if (!sideEffectSeen) { // Look for side-effects. if (hasNonLocalSideEffect(n)) { sideEffectSeen = true; } } // If traversing the nodes of a loop save any references // that are seen. if (inLoop() || sideEffectSeen) { // Record references to parameters. if (n.isName()) { String name = n.getString(); if (parameters.contains(name)) { parametersReferenced.add(name); } } else if (n.isThis()) { parametersReferenced.add(THIS_MARKER); } } } /** * @return Whether the node may have non-local side-effects. */ private boolean hasNonLocalSideEffect(Node n) { boolean sideEffect = false; Token type = n.getToken(); // Note: Only care about changes to non-local names, specifically // ignore VAR declaration assignments. if (NodeUtil.isAssignmentOp(n) || type == Token.INC || type == Token.DEC) { Node lhs = n.getFirstChild(); // Ignore changes to local names. if (!isLocalName(lhs)) { sideEffect = true; } } else if (type == Token.CALL) { sideEffect = astAnalyzer.functionCallHasSideEffects(n); } else if (type == Token.NEW) { sideEffect = astAnalyzer.constructorCallHasSideEffects(n); } else if (type == Token.DELPROP) { sideEffect = true; } return sideEffect; } /** * @return Whether node is a reference to locally declared name. */ private boolean isLocalName(Node node) { if (node.isName()) { String name = node.getString(); return locals.contains(name); } return false; } } /** Gather any names declared in the local scope. */ private static void gatherLocalNames(Node n, Set names) { if (n.isFunction()) { if (NodeUtil.isFunctionDeclaration(n)) { names.add(n.getFirstChild().getString()); } // Don't traverse into inner function scopes; return; } else if (n.isName()) { switch (n.getParent().getToken()) { case VAR: case LET: case CONST: case CATCH: names.add(n.getString()); break; default: break; } } for (Node c = n.getFirstChild(); c != null; c = c.getNext()) { gatherLocalNames(c, names); } } /** Get a set of function parameter names. */ private static ImmutableSet getFunctionParameterSet(Node fnNode) { ImmutableSet.Builder builder = ImmutableSet.builder(); for (Node n = NodeUtil.getFunctionParameters(fnNode).getFirstChild(); n != null; n = n.getNext()) { if (n.isRest()) { builder.add(REST_MARKER); } else if (n.isDefaultValue() || n.isObjectPattern() || n.isArrayPattern()) { throw new IllegalStateException("Not supported: " + n); } else { builder.add(n.getString()); } } return builder.build(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy