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

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

/*
 * 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.Es6ToEs3Util.withType;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.javascript.jscomp.MakeDeclaredNamesUnique.ContextualRenamer;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
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.Set;
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 } private final AbstractCompiler compiler; private final AstAnalyzer astAnalyzer; private final AstFactory astFactory; private final Supplier safeNameIdSupplier; private final Set knownConstants; private final Scope scope; private final JSType unknownType; private final JSType stringType; /** * TODO(b/124253050): Fix InlineFunctions so this code will always allow method call decomposing. */ private final boolean allowMethodCallDecomposing; ExpressionDecomposer( AbstractCompiler compiler, Supplier safeNameIdSupplier, Set constNames, Scope scope, boolean allowMethodCallDecomposing) { checkNotNull(compiler); checkNotNull(safeNameIdSupplier); checkNotNull(constNames); this.compiler = compiler; this.astAnalyzer = compiler.getAstAnalyzer(); this.astFactory = compiler.createAstFactory(); this.safeNameIdSupplier = safeNameIdSupplier; this.knownConstants = constNames; this.scope = scope; this.allowMethodCallDecomposing = allowMethodCallDecomposing; this.unknownType = compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE); this.stringType = compiler.getTypeRegistry().getNativeType(JSTypeNative.STRING_TYPE); } // An arbitrary limit to prevent catch infinite recursion. private static final int MAX_ITERATIONS = 100; /** * 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}. * *

This method should not be called from outside of this class. Instead call {@link * #maybeExposeExpression(Node)}. */ 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(); 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()) { checkState(allowMethodCallDecomposing, "Object method calls cannot be decomposed."); // 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. expressionParent = 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); // 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. Node expressionParent = expression.getParent(); expressionParent.replaceChild( expression, withType(IR.name(resultName), expression.getJSType())); // Re-add the expression at the appropriate place. Node newExpressionRoot = NodeUtil.newVarNode(resultName, expression); newExpressionRoot.getFirstChild().setJSType(expression.getJSType()); injectionPointParent.addChildBefore(newExpressionRoot, 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); } } private static void insertBefore(Node injectionPoint, Node newNode) { final Node injectionParent = injectionPoint.getParent(); injectionParent.addChildBefore(newNode, injectionPoint); } /** * @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) .useSourceInfoIfMissingFromForTree(expr); Node injectionPointParent = injectionPoint.getParent(); injectionPointParent.addChildBefore(tempVarNodeAssign, injectionPoint); Node assignLhs = buildResultExpression(first, true, tempNameAssign); Node nullNode = astFactory.createNull().useSourceInfoFrom(expr); cond = astFactory.createNe(assignLhs, nullNode).useSourceInfoFrom(expr); trueExpr.addChildToFront( astFactory.exprResult( buildResultExpression( astFactory .createName(tempNameAssign, first.getJSType()) .useSourceInfoFrom(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.useSourceInfoIfMissingFrom(expr); if (needResult) { Node tempVarNode = astFactory .createSingleVarNameDeclaration(tempName) .useSourceInfoIfMissingFromForTree(expr); Node injectionPointParent = injectionPoint.getParent(); injectionPointParent.addChildBefore(tempVarNode, injectionPoint); injectionPointParent.addChildAfter(ifNode, tempVarNode); // Replace the expression with the temporary name. Node replacementValueNode = withType(IR.name(tempName), expr.getJSType()); parent.replaceChild(expr, 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()); Node grandparent = parent.getParent(); grandparent.replaceChild(parent, 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) { JSType type = expr.getJSType(); return withType(IR.assign(withType(IR.name(tempName), type), expr), type).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) || knownConstants.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) { 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.children()) { if (!n.isString() && !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).setJSType(expr.getJSType()).srcref(expr); Node tempNameValue; // If it is ASSIGN_XXX, 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 = withType(new Node(NodeUtil.getOpFromAssignmentOp(parent)), parent.getJSType()) .useSourceInfoIfMissingFrom(parent); Node rightOperand = parent.getLastChild(); parent.setToken(Token.ASSIGN); parent.replaceChild(rightOperand, 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).useSourceInfoFrom(expr.getOnlyChild()); break; case OBJECTLIT: tempNameValue = astFactory.createObjectLit(expr).useSourceInfoFrom(expr.getOnlyChild()); break; default: throw new IllegalStateException("Unexpected parent of SPREAD:" + parent.toStringTree()); } } else { // Replace the expression with the temporary name. parent.replaceChild(expr, 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().setJSType(tempNameValue.getJSType()); insertBefore(injectionPoint, tempVarNode); 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);
   * 
* * @return The replacement node. */ private Node 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 fnType = first.getJSType(); JSType fnCallType = null; if (fnType != null) { 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); Node thisVarNode = extractExpression(getExprNode.getFirstChild(), state.extractBeforeStatement); state.extractBeforeStatement = thisVarNode; // Rewrite the CALL expression. Node thisNameNode = thisVarNode.getFirstChild(); Node functionNameNode = getVarNode.getFirstChild(); // CALL // GETPROP // functionName // "call" // thisName // original-parameter1 // original-parameter2 // ... Node newCall = IR.call( withType( IR.getprop( functionNameNode.cloneNode(), withType(IR.string("call"), stringType)), fnCallType), thisNameNode.cloneNode()) .setJSType(call.getJSType()) .useSourceInfoIfMissingFromForTree(call); // Throw away the call name call.removeFirstChild(); if (call.hasChildren()) { // Add the call parameters to the new call. newCall.addChildrenToBack(call.removeChildren()); } call.replaceWith(newCall); return newCall; } 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() { String name = tempNamePrefix + "_const" + ContextualRenamer.UNIQUE_ID_SEPARATOR + safeNameIdSupplier.get(); this.knownConstants.add(name); return name; } 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)) { // Case: `let x = 5, y = 2 * x;` where `child = y`. // Compound declarations cannot generally be decomposed. Later declarations might reference // earlier ones and if it were possible to separate them, `Normalize` would already have // done so. Therefore, we only support decomposing the first declaration. // TODO(b/121157467): FOR initializers are probably the only source of these cases. 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)) { if (allowMethodCallDecomposing) { return DecompositionType.DECOMPOSABLE; } else { // TODO(b/124253050): Fix InlineFunctions so this code will always allow method call // decomposing. return DecompositionType.UNDECOMPOSABLE; } } } } // 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; } /** * 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. Node parent = tree.getParent(); if (NodeUtil.isObjectCallMethod(parent, "call") && tree.isFirstChildOf(parent) && isTempConstantValueName(tree.getFirstChild())) { return false; } // This is a superset of "NodeUtil.mayHaveSideEffects". return NodeUtil.canBeSideEffected(tree, this.knownConstants, 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