com.google.javascript.jscomp.ExpressionDecomposer Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
* 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);
checkNotNull(expressionRoot);
checkState(NodeUtil.isStatement(expressionRoot), expressionRoot);
Node change = expressionRoot.getParent();
exposeExpression(expressionRoot, expression);
compiler.reportChangeToEnclosingScope(change);
}
/**
* 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);
}
}
// 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);
}
/**
* @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.
// 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
if (!IR.mayBeExpression(n)) {
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 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);
}
}
}