com.google.javascript.jscomp.ExpressionDecomposer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler Show documentation
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.
/*
* 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 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 java.util.Set;
import javax.annotation.Nullable;
/**
* Methods necessary for partially or full decomposing an expression. Initially
* this is intended to expanded the locations were inlining can occur, but has
* other uses as well.
*
* For example:
* var x = y() + z();
*
* Becomes:
* var a = y();
* var b = z();
* x = a + b;
*
* @author [email protected] (John Lenz)
*/
class ExpressionDecomposer {
/**
* @see #canExposeExpression
*/
enum DecompositionType {
UNDECOMPOSABLE,
MOVABLE,
DECOMPOSABLE
}
private final AbstractCompiler compiler;
private final Supplier safeNameIdSupplier;
private final Set knownConstants;
private final Scope scope;
/**
* Whether to allow decomposing foo.bar to "var fn = foo.bar; fn.call(foo);"
* Should be false if targetting IE8 or IE9.
*/
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.safeNameIdSupplier = safeNameIdSupplier;
this.knownConstants = constNames;
this.scope = scope;
this.allowMethodCallDecomposing = allowMethodCallDecomposing;
}
// An arbitrary limit to prevent catch infinite recursion.
private static final int MAX_ITERATIONS = 100;
/**
* If required, rewrite the statement containing the expression.
* @param expression The expression to be exposed.
* @see #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 any rewriting necessary so that the specified expression
* is movable. This is a partial expression decomposition.
* @see #canExposeExpression
*/
void exposeExpression(Node expression) {
Node expressionRoot = findExpressionRoot(expression);
checkState(expressionRoot != null);
Node change = expressionRoot.getParent();
exposeExpression(expressionRoot, expression);
compiler.reportChangeToEnclosingScope(change);
}
// 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.
/**
* Extract the specified expression from its parent expression.
* @see #canExposeExpression
*/
void moveExpression(Node expression) {
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, IR.name(resultName));
// Re-add the expression at the appropriate place.
Node newExpressionRoot = NodeUtil.newVarNode(resultName, expression);
injectionPointParent.addChildBefore(newExpressionRoot, injectionPoint);
compiler.reportChangeToEnclosingScope(injectionPointParent);
}
/**
* Rewrite the expression such that the sub-expression is in a movable
* expression statement while maintaining evaluation order.
*
* Two types of subexpressions are extracted from the source expression:
* 1) subexpressions with side-effects.
* 2) conditional expressions, that contain the call, which are transformed
* into IF statements.
*
* The following terms are used:
* expressionRoot: The top-level node before which the any extracted
* expressions should be placed before.
* nonconditionalExpr: The node that will be extracted either express.
*
*/
private void exposeExpression(Node expressionRoot, Node subExpression) {
Node nonconditionalExpr = findNonconditionalParent(
subExpression, expressionRoot);
// Before extraction, record whether there are side-effect
boolean hasFollowingSideEffects = NodeUtil.mayHaveSideEffects(
nonconditionalExpr, compiler);
Node exprInjectionPoint = findInjectionPoint(nonconditionalExpr);
DecompositionState state = new DecompositionState();
state.sideEffects = hasFollowingSideEffects;
state.extractBeforeStatement = exprInjectionPoint;
// Extract expressions in the reverse order of their evaluation.
for (Node grandchild = null,
child = nonconditionalExpr,
parent = child.getParent();
parent != expressionRoot;
grandchild = child,
child = parent,
parent = child.getParent()) {
checkState(!isConditionalOp(parent) || child == parent.getFirstChild());
if (parent.isAssign()) {
if (isSafeAssign(parent, 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 can not 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 {
// Alias "next()" in "next().foo"
Node left = parent.getFirstChild();
Token type = left.getToken();
if (left != child) {
checkState(NodeUtil.isGet(left));
if (type == Token.GETELEM) {
decomposeSubExpressions(left.getLastChild(), null, state);
}
decomposeSubExpressions(left.getFirstChild(), null, state);
}
}
} else if (parent.isCall() && NodeUtil.isGet(parent.getFirstChild())) {
Node functionExpression = parent.getFirstChild();
decomposeSubExpressions(functionExpression.getNext(), child, state);
// Now handle the call expression
if (isExpressionTreeUnsafe(functionExpression, state.sideEffects)
&& functionExpression.getFirstChild() != grandchild) {
checkState(allowMethodCallDecomposing, "Object method calls can not be decomposed.");
// Either there were preexisting side-effects, or this node has side-effects.
state.sideEffects = true;
// Rewrite the call so "this" is preserved.
Node replacement = rewriteCallExpression(parent, state);
// Continue from here.
parent = replacement;
}
} else if (parent.isObjectLit()) {
decomposeObjectLiteralKeys(parent.getFirstChild(), child, state);
} else {
decomposeSubExpressions(parent.getFirstChild(), child, state);
}
}
// 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 (nonconditionalExpr == 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 = nonconditionalExpr.getParent();
boolean needResult = !parent.isExprResult();
extractConditional(nonconditionalExpr, exprInjectionPoint, needResult);
}
}
/**
* @return "expression" or the node closest to "expression", that does not
* have a conditional ancestor.
*/
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)) {
// Only the first child is always executed, if the function may never
// be called, don't inline it.
if (child != parent.getFirstChild()) {
result = parent;
}
}
}
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();
}
}
/**
* Decompose an object literal.
* @param key The object literal key.
* @param stopNode A node after which to stop iterating.
*/
private void decomposeObjectLiteralKeys(Node key, Node stopNode, DecompositionState state) {
if (key == null || key == stopNode) {
return;
}
decomposeObjectLiteralKeys(key.getNext(), stopNode, state);
decomposeSubExpressions(key.getFirstChild(), stopNode, state);
}
/**
* @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;
}
// Never try to decompose an object literal key.
checkState(!NodeUtil.isObjectLitKey(n), n);
// Decompose the children in reverse evaluation order. This simplifies
// determining if the any of the children following have side-effects.
// If they do we need to be more aggressive about removing values
// from the expression.
decomposeSubExpressions(n.getNext(), stopNode, state);
// Now this node.
// 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 before which 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 = IR.block().srcref(expr);
Node falseExpr = IR.block().srcref(expr);
switch (expr.getToken()) {
case HOOK:
// a = x?y:z --> if (x) {a=y} else {a=z}
cond = first;
trueExpr.addChildToFront(NodeUtil.newExpr(
buildResultExpression(second, needResult, tempName)));
falseExpr.addChildToFront(NodeUtil.newExpr(
buildResultExpression(last, needResult, tempName)));
break;
case AND:
// a = x&&y --> if (a=x) {a=y} else {}
cond = buildResultExpression(first, needResult, tempName);
trueExpr.addChildToFront(NodeUtil.newExpr(
buildResultExpression(last, needResult, tempName)));
break;
case OR:
// a = x||y --> if (a=x) {} else {a=y}
cond = buildResultExpression(first, needResult, tempName);
falseExpr.addChildToFront(NodeUtil.newExpr(
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 = IR.ifNode(cond, trueExpr, falseExpr);
} else {
ifNode = IR.ifNode(cond, trueExpr);
}
ifNode.useSourceInfoIfMissingFrom(expr);
if (needResult) {
Node tempVarNode = NodeUtil.newVarNode(tempName, null)
.useSourceInfoIfMissingFromForTree(expr);
Node injectionPointParent = injectionPoint.getParent();
injectionPointParent.addChildBefore(tempVarNode, injectionPoint);
injectionPointParent.addChildAfter(ifNode, tempVarNode);
// Replace the expression with the temporary name.
Node replacementValueNode = IR.name(tempName);
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) {
return IR.assign(IR.name(tempName), 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) || 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()
&& parent.getFirstChild() == expr;
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.isGet(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).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.isGet(expr), expr);
// Transform "x += 2" into "x = temp + 2"
Node opNode = new Node(NodeUtil.getOpFromAssignmentOp(parent))
.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 {
// 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);
Node injectionPointParent = injectionPoint.getParent();
injectionPointParent.addChildBefore(tempVarNode, 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);
*
* @return The replacement node.
*/
private Node rewriteCallExpression(Node call, DecompositionState state) {
checkArgument(call.isCall(), call);
Node first = call.getFirstChild();
checkArgument(NodeUtil.isGet(first), first);
// 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.isGet(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(
IR.getprop(
functionNameNode.cloneNode(), IR.string("call")),
thisNameNode.cloneNode()).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(String s) {
return s.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:
return true;
default:
return false;
}
}
/**
* @return The statement containing the expression or null if the subExpression
* is not contain in a Node where inlining is known to be possible.
* For example, a WHILE node condition expression.
*/
@Nullable
static Node findExpressionRoot(Node subExpression) {
Node child = subExpression;
for (Node parent : child.getAncestors()) {
Token parentType = parent.getToken();
switch (parentType) {
// 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 == parent.getFirstChild());
return parent;
case VAR:
case CONST:
case LET:
Preconditions.checkState(child == parent.getFirstChild());
if (parent.getParent().isVanillaFor()
&& parent == parent.getParent().getFirstChild()) {
return null;
} else {
return parent;
}
// Any of these indicate an unsupported expression:
case FOR:
if (child == parent.getFirstChild()) {
return parent;
}
// fall through
case FOR_IN:
case FOR_OF:
case SCRIPT:
case BLOCK:
case LABEL:
case CASE:
case DEFAULT_CASE:
return null;
default:
break;
}
child = parent;
}
throw new IllegalStateException("Unexpected AST structure.");
}
/**
* Determine whether a expression is movable, or can be be made movable after
* decomposing the containing expression.
*
* A subexpression is MOVABLE if it can be replaced with a temporary holding
* its results and moved to immediately before the root of the expression.
* There are three conditions that must be met for this to occur:
* 1) There must be a location to inject a statement for the expression. For
* example, this condition can not be met if the expression is a loop
* condition or CASE condition.
* 2) If the expression can be affected by side-effects, there can not be a
* side-effect between original location and the expression root.
* 3) If the expression has side-effects, there can not be any other
* expression that can be affected between the original location and the
* expression root.
*
* An expression is DECOMPOSABLE if it can be rewritten so that an
* subExpression is MOVABLE.
*
* An expression is decomposed by moving any other sub-expressions that
* preventing an subExpression from being MOVABLE.
*
* @return Whether This is a call that can be moved to an new point in the
* AST to allow it to be inlined.
*/
DecompositionType canExposeExpression(Node subExpression) {
Node expressionRoot = findExpressionRoot(subExpression);
if (expressionRoot != null) {
return isSubexpressionMovable(expressionRoot, subExpression);
}
return DecompositionType.UNDECOMPOSABLE;
}
/**
* Walk the AST from the call site to the expression root and verify that
* the portions of the expression that are evaluated before the call:
* 1) are unaffected by the side-effects, if any, of the call.
* 2) have no side-effects, that may influence the call.
*
* For example, if x has side-effects:
* a = 1 + x();
* the call to x can be moved because the final value of "a" can not be
* influenced by x(), but in:
* a = b + x();
* the call to x cannot be moved because the value of "b" may be modified
* by the call to x.
*
* If x is without side-effects in:
* a = b + x();
* the call to x can be moved, but in:
* a = (b.foo = c) + x();
* the call to x can not be moved because the value of b.foo may be referenced
* by x(). Note: this is true even if b is a local variable; the object that
* b refers to may have a global alias.
*
* @return UNDECOMPOSABLE if the expression cannot be moved, DECOMPOSABLE if
* decomposition is required before the expression can be moved, otherwise MOVABLE.
*/
private DecompositionType isSubexpressionMovable(Node expressionRoot, Node subExpression) {
boolean requiresDecomposition = false;
boolean seenSideEffects = NodeUtil.mayHaveSideEffects(subExpression, compiler);
Node child = subExpression;
for (Node parent : child.getAncestors()) {
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: This depends on the siblings being in the same order as they
// are evaluated.
// 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 can not 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.
for (Node n : parent.children()) {
if (n == child) {
// None of the preceding siblings have side-effects.
// This is OK.
break;
}
if (isExpressionTreeUnsafe(n, seenSideEffects)) {
seenSideEffects = true;
requiresDecomposition = true;
}
}
// In Internet Explorer, DOM objects and other external objects
// methods can not be called indirectly, as is required when the
// object or its property can be side-effected. For example,
// when exposing expression f() (with side-effects) in: x.m(f())
// either the value of x or its property m might have changed, so
// both the 'this' value ('x') and the function to be called ('x.m')
// need to be preserved. Like so:
// var t1 = x, t2 = x.m, t3 = f();
// t2.call(t1, t3);
// As IE doesn't support the call to these non-JavaScript objects
// methods in this way. We can't do this.
// We don't currently distinguish between these types of objects
// in the extern definitions and if we did we would need accurate
// type information.
//
Node first = parent.getFirstChild();
if (requiresDecomposition && parent.isCall() && NodeUtil.isGet(first)) {
if (allowMethodCallDecomposing) {
return DecompositionType.DECOMPOSABLE;
} else {
return DecompositionType.UNDECOMPOSABLE;
}
}
}
}
// Continue looking up the expression tree.
child = parent;
}
// With a valid tree we should never get here.
throw new IllegalStateException("Unexpected.");
}
/**
* 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 can not influence the state before
* the call to foo.
*
* It is also safe in cases like 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;
}
/**
* @return Whether anything in the expression tree prevents a call from
* being moved.
*/
private boolean isExpressionTreeUnsafe(Node n, boolean followingSideEffectsExist) {
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 = n.getParent();
if (NodeUtil.isObjectCallMethod(parent, "call")
&& n == parent.getFirstChild()
&& n.getFirstChild().isName()
&& isTempConstantValueName(n.getFirstChild().getString())) {
return false;
}
// This is a superset of "NodeUtil.mayHaveSideEffects".
return NodeUtil.canBeSideEffected(n, 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 NodeUtil.mayHaveSideEffects(n, compiler);
}
}
}