/* * 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 com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.CodingConvention.SubclassRelationship; import com.google.javascript.jscomp.NodeTraversal.Callback; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import java.util.ArrayList; import java.util.List; /** * Callback that gathers subexpressions that may have side effects * and appends copies of those subexpressions to the replacements * list. In the case of branching subexpressions, it simplifies the * subexpression before adding it to the replacement list. * */ class GatherSideEffectSubexpressionsCallback implements Callback { /** * Used by GatherSideEffectSubexpressionsCallback to notify client * code about side effect expressions that should be kept. */ interface SideEffectAccumulator { /** * Returns true if the "mixin" and "inherits" function calls * should be treated as if they had side effects. */ boolean classDefiningCallsHaveSideEffects(); /** * Adds subtree to the list of nodes that have side effects. * * @param original - root of the tree. */ void keepSubTree(Node original); /** * Simplifies a subtree whose root node is an AND or OR expression * and adds the resulting subtree to the list of nodes that have * side effects. * * @param original - root of the and/or expression. */ void keepSimplifiedShortCircuitExpression(Node original); /** * Simplifies a subtree whose root node is a HOOK expression * and adds the resulting subtree to the list of nodes that have * side effects. * * @param hook - root of the hook expression. * @param thenHasSideEffects - then branch has side effects * @param elseHasSideEffects - else branch has side effects */ void keepSimplifiedHookExpression(Node hook, boolean thenHasSideEffects, boolean elseHasSideEffects); } /** * Populates the provided replacement list by appending copies of * subtrees that have side effects. * * It is OK if this class tears up the original tree, because * we're going to throw the tree out anyway. */ static final class GetReplacementSideEffectSubexpressions implements SideEffectAccumulator { private final AbstractCompiler compiler; private final List<Node> replacements; /** * Creates the accumulator. * * @param compiler - the AbstractCompiler * @param replacements - list to accumulate into */ GetReplacementSideEffectSubexpressions(AbstractCompiler compiler, List<Node> replacements) { this.compiler = compiler; this.replacements = replacements; } @Override public boolean classDefiningCallsHaveSideEffects() { return true; } @Override public void keepSubTree(Node original) { if (original.getParent() != null) { original.detach(); } replacements.add(original); } @Override public void keepSimplifiedShortCircuitExpression(Node original) { Preconditions.checkArgument( (original.isAnd()) || (original.isOr()), "Expected: AND or OR, Got: %s", original.getToken()); Node left = original.getFirstChild(); Node right = left.getNext(); Node simplifiedRight = simplifyShortCircuitBranch(right); original.detachChildren(); original.addChildToBack(left); original.addChildToBack(simplifiedRight); keepSubTree(original); } @Override public void keepSimplifiedHookExpression(Node hook, boolean thenHasSideEffects, boolean elseHasSideEffects) { Preconditions.checkArgument(hook.isHook(), "Expected: HOOK, Got: %s", hook.getToken()); Node condition = hook.getFirstChild(); Node thenBranch = condition.getNext(); Node elseBranch = thenBranch.getNext(); if (thenHasSideEffects && elseHasSideEffects) { hook.detachChildren(); hook.addChildToBack(condition); hook.addChildToBack(simplifyShortCircuitBranch(thenBranch)); hook.addChildToBack(simplifyShortCircuitBranch(elseBranch)); keepSubTree(hook); } else if (thenHasSideEffects || elseHasSideEffects) { Token type = thenHasSideEffects ? Token.AND : Token.OR; Node body = thenHasSideEffects ? thenBranch : elseBranch; Node simplified = new Node( type, condition.detach(), simplifyShortCircuitBranch(body)) .useSourceInfoIfMissingFrom(hook); keepSubTree(simplified); } else { throw new IllegalArgumentException( "keepSimplifiedHookExpression must keep at least 1 branch"); } } private Node simplifyShortCircuitBranch(Node node) { List<Node> parts = new ArrayList<>(); NodeTraversal.traverseEs6( compiler, node, new GatherSideEffectSubexpressionsCallback( compiler, new GetReplacementSideEffectSubexpressions(compiler, parts))); Node ret = null; for (Node part : parts) { if (ret != null) { ret = IR.comma(ret, part).srcref(node); } else { ret = part; } } if (ret == null) { throw new IllegalArgumentException( "expected at least one side effect subexpression in short " + "circuit branch."); } return ret; } } private static final ImmutableSet<Token> FORBIDDEN_TYPES = ImmutableSet.of(Token.BLOCK, Token.SCRIPT, Token.VAR, Token.EXPR_RESULT, Token.RETURN); private final AbstractCompiler compiler; private final SideEffectAccumulator accumulator; /** * @param compiler - AbstractCompiler object * @param accumulator - object that will accumulate roots of * subtrees that have side effects. */ GatherSideEffectSubexpressionsCallback(AbstractCompiler compiler, SideEffectAccumulator accumulator) { this.compiler = compiler; this.accumulator = accumulator; } /** * Determines if a call defines a class inheritance or mixing * relation, according to the current coding convention. */ private boolean isClassDefiningCall(Node callNode) { SubclassRelationship classes = compiler.getCodingConvention().getClassesDefinedByCall(callNode); return classes != null; } /** * Computes the list of subtrees whose root nodes have side effects. * * <p>If the current subtree's root has side effects this method should * call accumulator.keepSubTree and return 'false' to add the * subtree to the result list and avoid avoid traversing the nodes children. * * <p>Branching nodes whose then or else branch contain side effects * must be simplified by doing a recursive traversal; this method * should call the appropriate accumulator 'keepSimplified' method * and return 'false' to stop the regular traversal. */ @Override public boolean shouldTraverse( NodeTraversal traversal, Node node, Node parent) { if (FORBIDDEN_TYPES.contains(node.getToken()) || NodeUtil.isControlStructure(node)) { throw new IllegalArgumentException(node.getToken() + " nodes are not supported."); } // Do not recurse into nested functions. if (node.isFunction()) { return false; } // simplify and maybe keep hook expression. if (node.isHook()) { return processHook(node); } // simplify and maybe keep AND/OR expression. if ((node.isAnd()) || (node.isOr())) { return processShortCircuitExpression(node); } if (!NodeUtil.nodeTypeMayHaveSideEffects(node, compiler)) { return true; } else { // Node type suggests that the expression has side effects. if (node.isCall()) { return processFunctionCall(node); } else if (node.isNew()) { return processConstructorCall(node); } else { accumulator.keepSubTree(node); return false; } } } /** * Processes an AND or OR expression. * * @return true to continue traversal, false otherwise */ boolean processShortCircuitExpression(Node node) { Preconditions.checkArgument( (node.isAnd()) || (node.isOr()), "Expected: AND or OR, Got: %s", node.getToken()); // keep whole expression if RHS of the branching expression // contains a call. Node left = node.getFirstChild(); Node right = left.getNext(); if (NodeUtil.mayHaveSideEffects(right, compiler)) { accumulator.keepSimplifiedShortCircuitExpression(node); return false; } else { return true; } } /** * Processes a HOOK expression. * * @return true to continue traversal, false otherwise */ boolean processHook(Node node) { Preconditions.checkArgument(node.isHook(), "Expected: HOOK, Got: %s", node.getToken()); Node condition = node.getFirstChild(); Node ifBranch = condition.getNext(); Node elseBranch = ifBranch.getNext(); boolean thenHasSideEffects = NodeUtil.mayHaveSideEffects( ifBranch, compiler); boolean elseHasSideEffects = NodeUtil.mayHaveSideEffects( elseBranch, compiler); if (thenHasSideEffects || elseHasSideEffects) { accumulator.keepSimplifiedHookExpression( node, thenHasSideEffects, elseHasSideEffects); return false; } else { return true; } } /** * Processes a CALL expression. * * @return true to continue traversal, false otherwise */ boolean processFunctionCall(Node node) { Preconditions.checkArgument(node.isCall(), "Expected: CALL, Got: %s", node.getToken()); // Calls to functions that are known to be "pure" have no side // effects. Node functionName = node.getFirstChild(); if (functionName.isName() || functionName.isGetProp()) { if (!accumulator.classDefiningCallsHaveSideEffects() && isClassDefiningCall(node)) { return true; } } if (!NodeUtil.functionCallHasSideEffects(node)) { return true; } accumulator.keepSubTree(node); return false; } /** * Processes a NEW expression. * * @return true to continue traversal, false otherwise */ boolean processConstructorCall(Node node) { Preconditions.checkArgument(node.isNew(), "Expected: NEW, Got: %s", node.getToken()); // Calls to constructors that are known to be "pure" have no // side effects. if (!NodeUtil.constructorCallHasSideEffects(node)) { return true; } accumulator.keepSubTree(node); return false; } @Override public void visit(NodeTraversal traversal, Node node, Node parent) {} }