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

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

There is a newer version: 9.0.8
Show 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.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.AstFactory.type;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.MakeDeclaredNamesUnique.ContextualRenamer;
import com.google.javascript.jscomp.colors.StandardColors;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.ArrayDeque;
import java.util.EnumSet;
import javax.annotation.Nullable;

/**
 * Partially or fully decomposes an expression with respect to some sub-expression. Initially this
 * is intended to expand the locations where inlining can occur, but has other uses as well.
 *
 * 

For example: `var x = y() + z();` becomes `var a = y(); var b = z(); var x = a + b;`. * *

Decomposing, in this context does not mean full decomposition to "atomic" expressions. While * it is possible to iteratively apply decomposition to get statements with at most one side-effect, * that isn't the intended purpose of this class. The focus is on decomposing "just enough" to * "free" a particular subexpression. For example: * *

    *
  • Given: `return (alert() + alert()) + z();` *
  • Exposing: `z()` *
  • Sufficient decomposition: `var temp = alert() + alert(); return temp + z();` *
*/ class ExpressionDecomposer { /** @see {@link #canExposeExpression} */ enum DecompositionType { UNDECOMPOSABLE, MOVABLE, DECOMPOSABLE } /** * Identify special case logic that needs to be enabled to work around edge cases, such as bad * JSVM behavior. */ public enum Workaround { // IE11 throws an exception for `window.location.assign.call(window.location, url)` BROKEN_IE11_LOCATION_ASSIGN } private final AbstractCompiler compiler; private final AstAnalyzer astAnalyzer; private final AstFactory astFactory; private final Supplier safeNameIdSupplier; private final ImmutableSet knownConstantFunctions; private final Scope scope; private final EnumSet enabledWorkarounds; @Nullable private final JSType unknownType; /** * Create an ExpressionDecomposer. * *

DO NOT USE THIS METHOD DIRECTLY. Call `compiler.createExpressionDecomposer()` instead so the * compiler can supply the workarounds needed for this compilation. * * @param constFunctionNames set of names known to be constant functions. Used by InlineFunctions * to prevent this pass from breaking bookkeeping for functions it's inlining. * @param enabledWorkarounds indicates which workarounds for bad JSVM behavior should be enabled. */ ExpressionDecomposer( AbstractCompiler compiler, Supplier safeNameIdSupplier, ImmutableSet constFunctionNames, Scope scope, EnumSet enabledWorkarounds) { checkNotNull(compiler); checkNotNull(safeNameIdSupplier); checkNotNull(constFunctionNames); this.compiler = compiler; this.astAnalyzer = compiler.getAstAnalyzer(); this.astFactory = compiler.createAstFactory(); this.safeNameIdSupplier = safeNameIdSupplier; this.knownConstantFunctions = constFunctionNames; this.scope = scope; this.enabledWorkarounds = enabledWorkarounds; this.unknownType = compiler.hasTypeCheckingRun() && !compiler.hasOptimizationColors() ? compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE) : null; } // An arbitrary limit to prevent catch infinite recursion. // Raised from 100->1000 on Jan 22, 2021 private static final int MAX_ITERATIONS = 1000; /** * Perform any rewriting necessary so that the specified expression is {@code MOVABLE}. * *

This method is a primary entrypoint into this class. It performs expression decomposition * such that {@code expression} can be moved to a preceding statement without changing behaviour. * *

Exposing {@code expression} generally doesn't mean that {@code expression} itself will * moved. An expression is exposed within a larger statement if no preceding expression would * interact with it. * * @see {@link #canExposeExpression} */ void maybeExposeExpression(Node expression) { // If the expression needs to exposed. int i = 0; while (DecompositionType.DECOMPOSABLE == canExposeExpression(expression)) { exposeExpression(expression); i++; if (i > MAX_ITERATIONS) { throw new IllegalStateException( "DecomposeExpression depth exceeded on:\n" + expression.toStringTree()); } } } /** * Perform partial decomposition to get the given expression closer to being {@code MOVEABLE}. */ private void exposeExpression(Node expression) { // First rewrite all optional chains containing the expression. // This must be done first, because the expression root may be an optional chain, and rewriting // it creates a new node to be the expression root. rewriteAllContainingOptionalChains(expression); Node expressionRoot = findExpressionRoot(expression); checkNotNull(expressionRoot); checkState(NodeUtil.isStatement(expressionRoot), expressionRoot); exposeExpression(expressionRoot, expression); } /** * Rewrite {@code expressionRoot} such that {@code subExpression} is a {@code MOVABLE} while * maintaining evaluation order. * *

IMPORTANT: This method assumes there are no optional chain parents of subExpression. The * single-argument version of this method takes care of that and should be the only caller of this * method. * *

Two types of subexpressions are extracted from the source expression: * *

    *
  1. subexpressions with side-effects *
  2. conditional expressions that contain {@code subExpression}, which are transformed into IF * statements. *
* *

The following terms are used: * *

    *
  • expressionRoot: The top-level node, before which any extracted expressions should be * placed. *
  • nodeWithNonconditionalParent: The node that will be extracted. *
* * @param expressionRoot The root of the subtree within which to expose {@code subExpression}. * @param subExpression A descendant of {@code expressionRoot} to be exposed. */ private void exposeExpression(Node expressionRoot, Node subExpression) { Node nodeWithNonconditionalParent = findNonconditionalParent(subExpression, expressionRoot); // Before extraction, record whether there are side-effect boolean hasFollowingSideEffects = astAnalyzer.mayHaveSideEffects(nodeWithNonconditionalParent); Node exprInjectionPoint = findInjectionPoint(nodeWithNonconditionalParent); DecompositionState state = new DecompositionState(); state.sideEffects = hasFollowingSideEffects; state.extractBeforeStatement = exprInjectionPoint; // Extract expressions in the reverse order of their evaluation. This is roughly, traverse up // the AST extracting any preceding expressions that may have side-effects or be side-effected. Node lastExposedSubexpression = null; Node expressionToExpose = nodeWithNonconditionalParent; Node expressionParent = expressionToExpose.getParent(); while (expressionParent != expressionRoot) { checkState( !isConditionalOp(expressionParent) || expressionToExpose.isFirstChildOf(expressionParent), expressionParent); if (expressionParent.isAssign()) { if (isSafeAssign(expressionParent, state.sideEffects)) { // It is always safe to inline "foo()" for expressions such as // "a = b = c = foo();" // As the assignment is unaffected by side effect of "foo()" // and the names assigned-to cannot influence the state before // the call to foo. // // This is not true of more complex LHS values, such as // a.x = foo(); // next().x = foo(); // in these cases the checks below are necessary. } else if (!expressionToExpose.isFirstChildOf(expressionParent)) { // Alias "next()" in "next().foo" Node left = expressionParent.getFirstChild(); switch (left.getToken()) { case GETELEM: decomposeSubExpressions(left.getLastChild(), null, state); // Fall through. case GETPROP: decomposeSubExpressions(left.getFirstChild(), null, state); break; default: throw new IllegalStateException("Expected a property access: " + left.toStringTree()); } } } else if (expressionParent.isCall() && NodeUtil.isNormalGet(expressionParent.getFirstChild())) { Node callee = expressionParent.getFirstChild(); if (callee != expressionToExpose) { decomposeSubExpressions(callee.getNext(), expressionToExpose, state); } // Now handle the call expression. We only have to do this if we arrived at decomposing this // call through one of the arguments, rather than the callee; otherwise the callee would // already be safe. if (isExpressionTreeUnsafe(callee, state.sideEffects) && lastExposedSubexpression != callee.getFirstChild()) { // Either there were preexisting side-effects, or this node has side-effects. state.sideEffects = true; // Rewrite the call so "this" is preserved and continue walking up from there. rewriteCallExpression(expressionParent, state); } } else { decomposeSubExpressions(expressionParent.getFirstChild(), expressionToExpose, state); } lastExposedSubexpression = expressionToExpose; expressionToExpose = expressionParent; expressionParent = expressionToExpose.getParent(); } // Now extract the expression that the decomposition is being performed to // to allow to be moved. All expressions that need to be evaluated before // this have been extracted, so add the expression statement after the // other extracted expressions and the original statement (or replace // the original statement. if (nodeWithNonconditionalParent == subExpression) { // Don't extract the call, as that introduces an extra constant VAR // that will simply need to be inlined back. It will be handled as // an EXPRESSION call site type. // Node extractedCall = extractExpression(decomposition, expressionRoot); } else { Node parent = nodeWithNonconditionalParent.getParent(); boolean needResult = !parent.isExprResult(); extractConditional(nodeWithNonconditionalParent, exprInjectionPoint, needResult); } } /** Rewrite all of the optional chains containing the given subExpression. */ private void rewriteAllContainingOptionalChains(Node subExpression) { final OptionalChainRewriter.Builder optChainRewriterBuilder = OptionalChainRewriter.builder(compiler) .setTmpVarNameCreator(this::getTempConstantValueName) .setScope(this.scope); // Rewriting the chains changes the shape of the AST in a way that would interfere // with the simple traversal from child to parent done here, so we'll traverse // them all first, then rewrite them. final ArrayDeque rewriters = new ArrayDeque<>(); for (Node exprParent = subExpression.getParent(); !NodeUtil.isStatement(exprParent); exprParent = exprParent.getParent()) { if (NodeUtil.isEndOfFullOptChain(exprParent)) { // We want to rewrite the outermost chain first, so the last one // we find is the first one we rewrite. rewriters.addFirst(optChainRewriterBuilder.build(exprParent)); } else if (exprParent.isCall()) { // It is possible to make a non-optional call against an optional chain callee by applying // parentheses like this. // `(obj?.method)(arg)` // I don't think there's a good reason to do that, since it will cause a runtime exception // if the chain is ever undefined, but it is allowed, so we must handle it. // Fortunately the OptionalChainRewriter knows how to fix the call so it will still get // made with the right `this` value. Node callee = exprParent.getFirstChild(); if (NodeUtil.isOptChainGet(callee)) { // By definition callee must end an optional chain, because it is the first child of a // non-optional parent. // checkState(NodeUtil.isEndOfFullOptChain(callee)) rewriters.addFirst(optChainRewriterBuilder.build(callee)); } } } for (OptionalChainRewriter rewriter : rewriters) { rewriter.rewrite(); } } /** * Extract the specified expression from its parent expression. * * @see #canExposeExpression */ void moveExpression(Node expression) { // TODO(johnlenz): This is not currently used by the function inliner, // as moving the call out of the expression before the actual function call // causes additional variables to be introduced. As the variable // inliner is improved, this might be a viable option. String resultName = getResultValueName(); Node injectionPoint = findInjectionPoint(expression); checkNotNull(injectionPoint); Node injectionPointParent = injectionPoint.getParent(); checkNotNull(injectionPointParent); checkState(NodeUtil.isStatementBlock(injectionPointParent)); // Replace the expression with a reference to the new name. expression.replaceWith(IR.name(resultName).copyTypeFrom(expression)); // Re-add the expression at the appropriate place. Node newExpressionRoot = NodeUtil.newVarNode(resultName, expression); newExpressionRoot.getFirstChild().copyTypeFrom(expression); newExpressionRoot.insertBefore(injectionPoint); compiler.reportChangeToEnclosingScope(injectionPointParent); } /** * Returns the enclosing expression to decompose * *

The intention is to indicate the top-most node that could be rewritten as an if-statement in * order to better expose subExpression for inlining. * *

Examples: * *

{@code
   * a = (x() && y()) && subExpression; // result is (x() && y()) && subExpression
   * a = x() && (y() && subExpression); // result is x() && (y() && subExpression)
   * a = (x() && subExpression) && y(); // result is x() && subExpression
   * a = x() && (subExpression && y()); // result is x() && (subExpression && y())
   * a = (subExpression && x()) && y(); // result is subExpression
   * a = subExpression && (x() && y()); // result is subExpression
   * }
* *

When subExpression is contained within an optional chain, we want to treat everything after * a `?.` up until the next `?.` as a single conditional operation. * *

Examples: * *

   * a = subExpression.x?.y.z();          // result is subExpression
   * a = x()?.[subExpression].y;          // result is x()?.[subExpression].y
   * a = x()?.y.z?.p(subExpression).q?.r; // result is x()?.y.z?.p(subExpression).q
   * a
   * 
* * @param subExpression the expression to consider entire chains * @param expressionRoot a node containing subExpression. The returned node will be a descendent * of this one. */ private static Node findNonconditionalParent(Node subExpression, Node expressionRoot) { Node result = subExpression; for (Node child = subExpression, parent = child.getParent(); parent != expressionRoot; child = parent, parent = child.getParent()) { if (isConditionalOp(parent) && !child.isFirstChildOf(parent)) { // subExpression is not part of the first child (which is always executed), so // parent decides whether subExpression will be executed or not result = parent; } } if (NodeUtil.isOptChainNode(result)) { // the loop above may have left result pointing into the middle of an optional chain for // a case like this. // `x?.y.z(subExpression).p.q?.r.s` // result is currently `x?.y.z(subExpression)`, but we want it to be the full sub-chain // containing subExpression // `x?.y.z(subExpression).p.q` result = NodeUtil.getEndOfOptChainSegment(result); } return result; } /** * A simple class to track two things: - whether side effects have been seen. - the last statement * inserted */ private static class DecompositionState { boolean sideEffects; Node extractBeforeStatement; @Override public String toString() { return toStringHelper(this) .add("sideEffects", sideEffects) .add("extractBeforeStatement", extractBeforeStatement) .toString(); } } /** * @param n The node with which to start iterating. * @param stopNode A node after which to stop iterating. */ private void decomposeSubExpressions(Node n, Node stopNode, DecompositionState state) { if (n == null || n == stopNode) { return; } // Decompose the children in reverse evaluation order. This simplifies determining if any of // the children following have side-effects. If they do we need to be more aggressive about // removing values from the expression. Reverse order also maintains evaluation order as each // extracted statemented is inserted on top of the others. decomposeSubExpressions(n.getNext(), stopNode, state); // Now this node. if (NodeUtil.mayBeObjectLitKey(n) // TODO(b/111621528): Delete when fixed. || n.isComputedProp()) { if (n.isComputedProp()) { // If the prop is computed we have to fork the decomposition between the key and value. This // is because we can't move the property assignment itself; COMPUTED_PROP must remain a // child of OBJECTLIT for example. // // We decompose the value of the prop first because decomposition is in reverse order of // evaluation. decomposeSubExpressions(n.getSecondChild(), stopNode, state); } // Decompose the children of the prop rather than the prop itself. In the computed case this // will be the key, otherwise it will be the value. n = n.getFirstChild(); } else if (n.isTemplateLitSub()) { // A template literal substitution expression like ${f()} is represented in the AST as // TEMPLATELIT_SUB // CALL // NAME f // The TEMPLATELIT_SUB node is not actually an expression and can't be extracted, but we may // need to extract the expression inside of it. n = n.getFirstChild(); } else if (n.isSpread()) { // SPREADs aren't expression but they can still be extracted using temp variables. // // Because object-spread can trigger getters we assume all spreads have side-effects. // TODO(nickreid): Use `assumeGettersArePure` here. It would have been a pain to pipe it down // here and write all the tests. Since there are very few cases, and it doesn't affect code // removal, we didn't bother initially. Everything always works one way. } else if (!IR.mayBeExpression(n)) { // If n is not an expression then it can't be extracted. For example if n is the destructuring // pattern on the left side of a VAR statement: // var {pattern} = rhs(); // See test case: testExposeExpression18 return; } // TODO(johnlenz): Move "safety" code to a shared class. if (isExpressionTreeUnsafe(n, state.sideEffects)) { // Either there were preexisting side-effects, or this node has side-effects. state.sideEffects = true; state.extractBeforeStatement = extractExpression(n, state.extractBeforeStatement); } } /** * @param expr The conditional expression to extract. * @param injectionPoint The node before which the extracted expression would be injected. * @param needResult Whether the result of the expression is required. * @return The node that contains the logic of the expression after extraction. */ private Node extractConditional(Node expr, Node injectionPoint, boolean needResult) { Node parent = expr.getParent(); String tempName = getTempValueName(); // Break down the conditional. Node first = expr.getFirstChild(); Node second = first.getNext(); Node last = expr.getLastChild(); // Isolate the children nodes. expr.detachChildren(); // Transform the conditional to an IF statement. Node cond = null; Node trueExpr = astFactory.createBlock().srcref(expr); Node falseExpr = astFactory.createBlock().srcref(expr); switch (expr.getToken()) { case HOOK: // a = x?y:z --> if (x) {a=y} else {a=z} cond = first; trueExpr.addChildToFront( astFactory.exprResult(buildResultExpression(second, needResult, tempName))); falseExpr.addChildToFront( astFactory.exprResult(buildResultExpression(last, needResult, tempName))); break; case AND: // a = x&&y --> if (a=x) {a=y} else {} cond = buildResultExpression(first, needResult, tempName); trueExpr.addChildToFront( astFactory.exprResult(buildResultExpression(last, needResult, tempName))); break; case OR: // a = x||y --> if (a=x) {} else {a=y} cond = buildResultExpression(first, needResult, tempName); falseExpr.addChildToFront( astFactory.exprResult(buildResultExpression(last, needResult, tempName))); break; case COALESCE: // a = x ?? y --> if ((temp=x)!=null) {a=temp} else {a=y} String tempNameAssign = getTempValueName(); Node tempVarNodeAssign = astFactory.createSingleVarNameDeclaration(tempNameAssign).srcrefTreeIfMissing(expr); tempVarNodeAssign.insertBefore(injectionPoint); Node assignLhs = buildResultExpression(first, true, tempNameAssign); Node nullNode = astFactory.createNull().srcref(expr); cond = astFactory.createNe(assignLhs, nullNode).srcref(expr); trueExpr.addChildToFront( astFactory.exprResult( buildResultExpression( astFactory.createName(tempNameAssign, type(first)).srcref(expr), needResult, tempName))); falseExpr.addChildToFront( astFactory.exprResult(buildResultExpression(last, needResult, tempName))); break; default: // With a valid tree we should never get here. throw new IllegalStateException("Unexpected expression: " + expr); } Node ifNode; if (falseExpr.hasChildren()) { ifNode = astFactory.createIf(cond, trueExpr, falseExpr); } else { ifNode = astFactory.createIf(cond, trueExpr); } ifNode.srcrefIfMissing(expr); if (needResult) { Node tempVarNode = astFactory.createSingleVarNameDeclaration(tempName).srcrefTreeIfMissing(expr); tempVarNode.insertBefore(injectionPoint); ifNode.insertAfter(tempVarNode); // Replace the expression with the temporary name. Node replacementValueNode = IR.name(tempName).copyTypeFrom(expr); expr.replaceWith(replacementValueNode); } else { // Only conditionals that are the direct child of an expression statement // don't need results, for those simply replace the expression statement. checkArgument(parent.isExprResult()); parent.replaceWith(ifNode); } return ifNode; } /** * Create an expression tree for an expression. * *

If the result of the expression is needed, then: * *

   * ASSIGN
   *   tempName
   *   expr
   * 
* * otherwise, simply: `expr` */ private static Node buildResultExpression(Node expr, boolean needResult, String tempName) { if (needResult) { return IR.assign(IR.name(tempName).copyTypeFrom(expr), expr) .copyTypeFrom(expr) .srcrefTree(expr); } else { return expr; } } private boolean isConstantNameNode(Node n) { // Non-constant names values may have been changed. return n.isName() && (NodeUtil.isConstantVar(n, scope) || knownConstantFunctions.contains(n.getString())); } /** * @param expr The expression to extract. * @param injectionPoint The node before which to added the extracted expression. * @return The extracted statement node. */ private Node extractExpression(Node expr, Node injectionPoint) { // Since all instances of logical assignment ops are normalized into an expression that // separates the logical operation from the assignment, logical assignment ops are // never seen here, and its logical operation part is instead handled by extractConditional(). checkState(!NodeUtil.isLogicalAssignmentOp(expr), expr); Node parent = expr.getParent(); boolean isLhsOfAssignOp = NodeUtil.isAssignmentOp(parent) && !parent.isAssign() && expr.isFirstChildOf(parent); Node firstExtractedNode = null; // Expressions on the LHS of an assignment-op must have any possible // side-effects extracted as the value must be duplicated: // next().foo += 2; // becomes: // var t1 = next(); // t1.foo = t1.foo + 2; if (isLhsOfAssignOp && NodeUtil.isNormalGet(expr)) { for (Node n = expr.getFirstChild(); n != null; n = n.getNext()) { if (!n.isStringLit() && !isConstantNameNode(n)) { Node extractedNode = extractExpression(n, injectionPoint); if (firstExtractedNode == null) { firstExtractedNode = extractedNode; } } } } // The temp is known to be constant. String tempName = getTempConstantValueName(); Node replacementValueNode = IR.name(tempName).copyTypeFrom(expr).srcref(expr); final Node tempNameValue; // If it is ASSIGN_XXX and not a logical assignment operator keep the // assignment in place and extract the original value of the LHS operand. if (isLhsOfAssignOp) { checkState(expr.isName() || NodeUtil.isNormalGet(expr), expr); // Transform "x += 2" into "x = temp + 2" Node opNode = new Node(NodeUtil.getOpFromAssignmentOp(parent)) .copyTypeFrom(parent) .srcrefIfMissing(parent); Node rightOperand = parent.getLastChild(); parent.setToken(Token.ASSIGN); rightOperand.replaceWith(opNode); opNode.addChildToFront(replacementValueNode); opNode.addChildToBack(rightOperand); // The original expression is still being used, so make a clone. tempNameValue = expr.cloneTree(); } else if (expr.isSpread()) { // We need to treat spreads differently because unlike other expressions, they can't be // directly assigned to new variables. Instead we wrap them in a literal. // // We make sure to do `var tmp = [...fn()];` rather than `var tmp = fn()` because the // execution of a spread on an arbitrary iterable/object can both have side-effects and be // side-effected. However, once done we are then sure that spreading `tmp` is isolated. // Replace the expression with the spread for the temporary name. Node spreadCopy = expr.cloneNode(); spreadCopy.addChildToBack(replacementValueNode); expr.replaceWith(spreadCopy); // Move the original node into a legal context. switch (parent.getToken()) { case ARRAYLIT: case CALL: case NEW: tempNameValue = astFactory.createArraylit(expr).srcref(expr.getOnlyChild()); break; case OBJECTLIT: tempNameValue = astFactory.createObjectLit(expr).srcref(expr.getOnlyChild()); break; default: throw new IllegalStateException("Unexpected parent of SPREAD:" + parent.toStringTree()); } } else { // Replace the expression with the temporary name. expr.replaceWith(replacementValueNode); // Keep the original node so that CALL expressions can still be found // and inlined properly. tempNameValue = expr; } // Re-add the expression in the declaration of the temporary name. Node tempVarNode = NodeUtil.newVarNode(tempName, tempNameValue); tempVarNode.getFirstChild().copyTypeFrom(tempNameValue); tempVarNode.getFirstChild().setInferredConstantVar(true); Scope containingHoistScope = scope.getClosestHoistScope(); containingHoistScope.declare(tempName, tempVarNode.getFirstChild(), /* input= */ null); tempVarNode.insertBefore(injectionPoint); if (firstExtractedNode == null) { firstExtractedNode = tempVarNode; } checkState(firstExtractedNode.isVar()); return firstExtractedNode; } /** * Rewrite the call so "this" is preserved. * *
a.b(c);
* * becomes: * *
   * var temp1 = a; var temp0 = temp1.b;
   * temp0.call(temp1,c);
   * 
*/ private void rewriteCallExpression(Node call, DecompositionState state) { checkArgument(call.isCall(), call); Node first = call.getFirstChild(); checkArgument(NodeUtil.isNormalGet(first), first); // Find the type of (fn expression).call JSType fnCallType = null; if (astFactory.isAddingTypes()) { JSType fnType = first.getJSType(); fnCallType = fnType.isFunctionType() ? fnType.toMaybeFunctionType().getPropertyType("call") : unknownType; } // Extracts the expression representing the function to call. For example: // "a['b'].c" from "a['b'].c()" Node getVarNode = extractExpression(first, state.extractBeforeStatement); state.extractBeforeStatement = getVarNode; // Extracts the object reference to be used as "this". For example: // "a['b']" from "a['b'].c" Node getExprNode = getVarNode.getFirstFirstChild(); checkArgument(NodeUtil.isNormalGet(getExprNode), getExprNode); final Node origThisValue = getExprNode.getFirstChild(); final Node functionNameNode = getVarNode.getFirstChild().cloneNode(); final Node receiverNode; if (origThisValue.isThis()) { // No need to create a variable for `this`, just clone it. receiverNode = origThisValue.cloneNode(); } else if (origThisValue.isSuper()) { // Original callee was like `super.prop(args)`. // The correct way to call the value `super.prop` from a temporary variable is // `tmpVar.call(this, args)`, so just create a `this` here. receiverNode = astFactory.createThis(type(origThisValue)).srcref(origThisValue); } else { final Node thisVarNode = extractExpression(origThisValue, state.extractBeforeStatement); state.extractBeforeStatement = thisVarNode; receiverNode = thisVarNode.getFirstChild().cloneNode(); } // CALL // GETPROP "call" // functionName // thisName // original-parameter1 // original-parameter2 // ... // Reuse the existing CALL node instead of creating a new one to avoid breaking InlineFunction's // bookkeeping. See b/124253050. call.removeFirstChild(); call.addChildToFront(receiverNode); call.addChildToFront( astFactory .createGetProp(functionNameNode, "call", type(fnCallType, StandardColors.TOP_OBJECT)) .srcrefTreeIfMissing(call)); call.putBooleanProp(Node.FREE_CALL, false); } private String tempNamePrefix = "JSCompiler_temp"; private String resultNamePrefix = "JSCompiler_inline_result"; /** Allow the temp name to be overridden to make tests more readable. */ @VisibleForTesting public void setTempNamePrefix(String prefix) { this.tempNamePrefix = prefix; } /** Create a unique temp name. */ private String getTempValueName() { return tempNamePrefix + ContextualRenamer.UNIQUE_ID_SEPARATOR + safeNameIdSupplier.get(); } /** Allow the temp name to be overridden to make tests more readable. */ @VisibleForTesting public void setResultNamePrefix(String prefix) { this.resultNamePrefix = prefix; } /** Create a unique name for call results. */ private String getResultValueName() { return resultNamePrefix + ContextualRenamer.UNIQUE_ID_SEPARATOR + safeNameIdSupplier.get(); } /** Create a constant unique temp name. */ private String getTempConstantValueName() { return tempNamePrefix + "_const" + ContextualRenamer.UNIQUE_ID_SEPARATOR + safeNameIdSupplier.get(); } private boolean isTempConstantValueName(Node name) { return name.isName() && name.getString() .startsWith(tempNamePrefix + "_const" + ContextualRenamer.UNIQUE_ID_SEPARATOR); } /** * @return For the subExpression, find the nearest statement Node before which it can be inlined. * Null if no such location can be found. */ @Nullable static Node findInjectionPoint(Node subExpression) { Node expressionRoot = findExpressionRoot(subExpression); checkNotNull(expressionRoot); Node injectionPoint = expressionRoot; Node parent = injectionPoint.getParent(); while (parent.isLabel()) { injectionPoint = parent; parent = injectionPoint.getParent(); } checkState(NodeUtil.isStatementBlock(parent), parent); return injectionPoint; } /** @return Whether the node is a conditional op. */ private static boolean isConditionalOp(Node n) { switch (n.getToken()) { case HOOK: case AND: case OR: case COALESCE: case OPTCHAIN_GETELEM: case OPTCHAIN_GETPROP: case OPTCHAIN_CALL: return true; default: return false; } } /** * Finds the statement containing {@code subExpression}. * *

If {@code subExpression} is not contained by a statement where inlining is known to be * possible, {@code null} is returned. For example, the condition expression of a WHILE loop. */ @Nullable private static Node findExpressionRoot(Node subExpression) { Node child = subExpression; for (Node current : child.getAncestors()) { Node parent = current.getParent(); switch (current.getToken()) { // Supported expression roots: // SWITCH and IF can have multiple children, but the CASE, DEFAULT, // or BLOCK will be encountered first for any of the children other // than the condition. case EXPR_RESULT: case IF: case SWITCH: case RETURN: case THROW: Preconditions.checkState(child.isFirstChildOf(current)); return current; case VAR: // Normalization will remove LABELs from VARs. case LET: case CONST: if (NodeUtil.isAnyFor(parent)) { break; // Name declarations may not be roots if they're for-loop initializers. } return current; // Any of these indicate an unsupported expression: case FOR: if (child.isFirstChildOf(current)) { // Only the initializer of a for-loop could possibly be decomposed since the other // statements need to execute each iteration. return current; } // fall through case FOR_IN: case FOR_OF: case FOR_AWAIT_OF: case DO: case WHILE: case SCRIPT: case BLOCK: case LABEL: case CASE: case DEFAULT_CASE: case DEFAULT_VALUE: case PARAM_LIST: return null; default: break; } child = current; } throw new IllegalStateException("Unexpected AST structure."); } /** * Determines if {@code subExpression} can be moved before {@code expressionRoot} without changing * the behaviour of the code, or if there is a rewriting that would make such motion possible. * *

Walks the AST from {@code subExpression} to {@code expressionRoot} and verifies that the * portions of the {@code expressionRoot} subtree that are evaluated before {@code subExpression}: * *

    *
  1. are unaffected by the side-effects, if any, of the {@code subExpression}. *
  2. have no side-effects that may influence the {@code subExpression}. *
  3. have a syntactically legal rewriting. *
* *

Examples: * *

    *
      *
    • {@code expressionRoot} = `a = 1 + x();` *
    • {@code subExpression} = `x()`, has side-effects *
    • {@code MOVABLE} because the final value of `1` cannot be influenced by `x()`. *
    *
      *
    • {@code expressionRoot} = `a = b + x();` *
    • {@code subExpression} = `x()`, has side-effects *
    • {@code DECOMPOSABLE} because `b` may be modified by `x()`, but `b` can be cached. *
    *
      *
    • {@code expressionRoot} = `a = b + x();` *
    • {@code subExpression} = `x()`, no side-effects *
    • {@code MOVABLE} because `x()` can be computed before or after `b` is resolved. *
    *
      *
    • {@code expressionRoot} = `a = (b = c) + x();` *
    • {@code subExpression} = `x()`, no side-effects, is side-effected *
    • {@code DECOMPOSABLE} because `x()` may read `b`. *
    *
* * @return *
    *
  • {@code MOVABLE} if {@code subExpression} can already be moved. *
  • {@code DECOMPOSABLE} if the {@code expressionRoot} subtree could be rewritten such * that {@code subExpression} would be made movable. *
  • {@code UNDECOMPOSABLE} otherwise. *
*/ DecompositionType canExposeExpression(Node subExpression) { Node expressionRoot = findExpressionRoot(subExpression); if (expressionRoot != null) { return isSubexpressionMovable(expressionRoot, subExpression); } return DecompositionType.UNDECOMPOSABLE; } /** @see {@link #canExposeExpression(Node subExpression)} */ private DecompositionType isSubexpressionMovable(Node expressionRoot, Node subExpression) { boolean requiresDecomposition = false; boolean seenSideEffects = astAnalyzer.mayHaveSideEffects(subExpression); if (NodeUtil.isOptChainNode(subExpression) && !NodeUtil.isEndOfFullOptChain(subExpression)) { // e.g `sub?.expression.rest?.of.expression` // It is always necessary to decompose the prefix of an optional chain. requiresDecomposition = true; } Node child = subExpression; for (Node parent : child.getAncestors()) { if (NodeUtil.isNameDeclaration(parent) && !child.isFirstChildOf(parent)) { // Most declarations are split by `Normalize` but to do so in loops would change the // behavior of the code. We don't current handle this case but it is possible: // For this case: `for (let x = 5, y = 2 * x; ...` where `child = y`. // As later expressions may reference the earlier expression, it would be necessary to // rewrite references when extracting the expressions: // `var temp1 = 5; var temp2 = 2 * temp1; for (let x = temp1, y = temp2; ...` return DecompositionType.UNDECOMPOSABLE; } if (parent == expressionRoot) { // Done. The walk back to the root of the expression is complete, and // nothing was encountered that blocks the call from being moved. return requiresDecomposition ? DecompositionType.DECOMPOSABLE : DecompositionType.MOVABLE; } if (isConditionalOp(parent)) { // Only the first child is always executed, otherwise it must be // decomposed. if (child != parent.getFirstChild()) { requiresDecomposition = true; } } else { // Only inline the call if none of the preceding siblings in the // expression have side-effects, and are unaffected by the side-effects, // if any, of the call in question. // NOTE: The siblings are not always in the order in which they are evaluated, so we call // getEvaluationDirection to see in which order to traverse the siblings. // SPECIAL CASE: Assignment to a simple name if (isSafeAssign(parent, seenSideEffects)) { // It is always safe to inline "foo()" for expressions such as // "a = b = c = foo();" // As the assignment is unaffected by side effect of "foo()" // and the names assigned-to cannot influence the state before // the call to foo. // // This is not true of more complex LHS values, such as // a.x = foo(); // next().x = foo(); // in these cases the checks below are necessary. } else { // Everything else. EvaluationDirection direction = getEvaluationDirection(parent); for (Node n = getFirstEvaluatedChild(parent, direction); n != null; n = getNextEvaluatedSibling(n, direction)) { if (n == child) { // None of the preceding siblings have side-effects. // This is OK. break; } if (isExpressionTreeUnsafe(n, seenSideEffects)) { seenSideEffects = true; requiresDecomposition = true; } } Node first = parent.getFirstChild(); if (requiresDecomposition && parent.isCall() && NodeUtil.isNormalGet(first)) { return DecompositionType.DECOMPOSABLE; } } } // Continue looking up the expression tree. child = parent; } // With a valid tree we should never get here. throw new IllegalStateException("Unexpected."); } private enum EvaluationDirection { FORWARD, REVERSE } /** * Returns the order in which the given node's children should be evaluated. * *

In most cases, this is EvaluationDirection.FORWARD because the AST order matches the actual * evaluation order. A few nodes require reversed evaluation instead. */ private static EvaluationDirection getEvaluationDirection(Node node) { switch (node.getToken()) { case DESTRUCTURING_LHS: case ASSIGN: case DEFAULT_VALUE: if (node.getFirstChild().isDestructuringPattern()) { // The lhs of a destructuring assignment is evaluated AFTER the rhs. This is only true for // destructuring, though, not assignments like "first().x = second()" where "first()" is // evaluated first. return EvaluationDirection.REVERSE; } // fall through default: return EvaluationDirection.FORWARD; } } private Node getFirstEvaluatedChild(Node parent, EvaluationDirection direction) { return direction == EvaluationDirection.FORWARD ? parent.getFirstChild() : parent.getLastChild(); } private Node getNextEvaluatedSibling(Node node, EvaluationDirection direction) { return direction == EvaluationDirection.FORWARD ? node.getNext() : node.getPrevious(); } /** * It is always safe to inline "foo()" for expressions such as "a = b = c = foo();" As the * assignment is unaffected by side effect of "foo()" and the names assigned-to cannot influence * the state before the call to foo. * *

It is also safe in cases where the object is constant: * *

   * CONST_NAME.a = foo()
   * CONST_NAME[CONST_VALUE] = foo();
   * 
* *

This is not true of more complex LHS values, such as * *

   * a.x = foo();
   * next().x = foo();
   * 
* * in these cases the checks below are necessary. * * @param seenSideEffects If true, check to see if node-tree maybe affected by side-effects, * otherwise if the tree has side-effects. @see isExpressionTreeUnsafe * @return Whether the assignment is safe from side-effects. */ private boolean isSafeAssign(Node n, boolean seenSideEffects) { if (n.isAssign()) { Node lhs = n.getFirstChild(); switch (lhs.getToken()) { case NAME: return true; case GETPROP: return !isExpressionTreeUnsafe(lhs.getFirstChild(), seenSideEffects); case GETELEM: return !isExpressionTreeUnsafe(lhs.getFirstChild(), seenSideEffects) && !isExpressionTreeUnsafe(lhs.getLastChild(), seenSideEffects); default: break; } } return false; } /** * We must not decompose this method qname. * *

See usage location for more information. */ private static final QualifiedName WINDOW_LOCATION_ASSIGN = QualifiedName.of("window.location.assign"); /** * Determines if there is any subexpression below {@code tree} that would make it incorrect for * some expression that follows {@code tree}, {@code E}, to be executed before {@code tree}. * * @param followingSideEffectsExist whether {@code E} causes side-effects. * @return {@code true} if {@code tree} contains any subexpressions that would make movement * incorrect. */ private boolean isExpressionTreeUnsafe(Node tree, boolean followingSideEffectsExist) { if (tree.isSpread()) { // Spread expressions would cause recursive rewriting if not special cased here. // When extracted, spreads can't be assigned to a single variable and instead are put into // a literal. However, that literal must be spread again at the original site. This // check is what prevents the original spread from triggering recursion. if (isTempConstantValueName(tree.getOnlyChild())) { return false; } } if (followingSideEffectsExist) { // If the call to be inlined has side-effects, check to see if this // expression tree can be affected by any side-effects. // Assume that "tmp1.call(...)" is safe (where tmp1 is a const temp variable created by // ExpressionDecomposer) otherwise we end up trying to decompose the same tree // an infinite number of times. Also assume that "fn.call" is safe when "fn" is a known // constant function, as decomposing it may mess up InlineFunction's bookkeeping if it is // attempting to inline "fn". Node parent = tree.getParent(); if (NodeUtil.isObjectCallMethod(parent, "call") && tree.isFirstChildOf(parent) && (isTempConstantValueName(tree.getFirstChild()) || knownConstantFunctions.contains(tree.getFirstChild().getQualifiedName()))) { return false; } if (enabledWorkarounds.contains(Workaround.BROKEN_IE11_LOCATION_ASSIGN) && WINDOW_LOCATION_ASSIGN.matches(tree)) { // IE11 throws an exception if we decompose a call to `window.location.assign` // e.g. // ```js // obj = window.location, method = obj.assign, method.call(obj, url); // exception on IE11 // ``` // So we will consider it to be a constant value that doesn't need to be decomposed to // protect it against side effects. // This is a hack to work around a specific case we've seen in real world code. // We know it won't catch some cases, but we would rather miss those than add more // sophisticated logic that would also encounter false-positives (e.g. is `x.assign` a // reference to `window.location.assign`?). // // We cannot have canBeSideEffected() (below) do this check for us, because it only accepts // simple names as constants. return false; } // This is a superset of "AstAnalyzer.mayHaveSideEffects". return NodeUtil.canBeSideEffected(tree, this.knownConstantFunctions, scope); } else { // The function called doesn't have side-effects but check to see if there // are side-effects that that may affect it. return astAnalyzer.mayHaveSideEffects(tree); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy