com.google.javascript.jscomp.FunctionToBlockMutator 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.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.javascript.jscomp.FunctionArgumentInjector.THIS_MARKER;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.MakeDeclaredNamesUnique.InlineRenamer;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/**
* A class to transform the body of a function into a generic block suitable
* for inlining.
*/
class FunctionToBlockMutator {
private final AbstractCompiler compiler;
private final Supplier safeNameIdSupplier;
private final FunctionArgumentInjector functionArgumentInjector;
FunctionToBlockMutator(
AbstractCompiler compiler, Supplier safeNameIdSupplier) {
this.compiler = checkNotNull(compiler);
this.safeNameIdSupplier = safeNameIdSupplier;
this.functionArgumentInjector = new FunctionArgumentInjector(compiler.getAstAnalyzer());
}
/**
* @param fnName The name to use when preparing human readable names.
* @param fnNode The function to prepare.
* @param callNode The call node that will be replaced.
* @param resultName Function results should be assigned to this name.
* @param needsDefaultResult Whether the result value must be set.
* @param isCallInLoop Whether the function body must be prepared to be
* injected into the body of a loop.
* @return A clone of the function body mutated to be suitable for injection
* as a statement into another code block.
*/
Node mutate(String fnName, Node fnNode, Node callNode,
String resultName, boolean needsDefaultResult, boolean isCallInLoop) {
return mutateInternal(
fnName,
fnNode,
callNode,
resultName,
needsDefaultResult,
isCallInLoop,
/* renameLocals= */ true);
}
/**
* Used where the inlining occurs into an isolated scope such as a module. Renaming is avoided
* since the associated JSDoc annotations are not updated.
*
* @param fnName The name to use when preparing human readable names.
* @param fnNode The function to prepare.
* @param callNode The call node that will be replaced.
* @param resultName Function results should be assigned to this name.
* @param needsDefaultResult Whether the result value must be set.
* @param isCallInLoop Whether the function body must be prepared to be injected into the body of
* a loop.
* @return A clone of the function body mutated to be suitable for injection as a statement into
* another code block.
*/
Node mutateWithoutRenaming(
String fnName,
Node fnNode,
Node callNode,
String resultName,
boolean needsDefaultResult,
boolean isCallInLoop) {
return mutateInternal(
fnName,
fnNode,
callNode,
resultName,
needsDefaultResult,
isCallInLoop,
/* renameLocals= */ false);
}
/**
* @param fnName The name to use when preparing human readable names.
* @param fnNode The function to prepare.
* @param callNode The call node that will be replaced.
* @param resultName Function results should be assigned to this name.
* @param needsDefaultResult Whether the result value must be set.
* @param isCallInLoop Whether the function body must be prepared to be injected into the body of
* a loop.
* @param renameLocals If the inlining is part of module rewriting and doesn't require making
* local names unique
* @return A clone of the function body mutated to be suitable for injection as a statement into
* another code block.
*/
private Node mutateInternal(
String fnName,
Node fnNode,
Node callNode,
String resultName,
boolean needsDefaultResult,
boolean isCallInLoop,
boolean renameLocals) {
Node newFnNode = fnNode.cloneTree();
// Wrap the clone in a script, in a root, so that makeLocalNamesUnique sees a coherent tree.
IR.root(IR.script(newFnNode));
if (renameLocals) {
// Now that parameter names have been replaced, make sure all the local
// names are unique, to allow functions to be inlined multiple times
// without causing conflicts.
makeLocalNamesUnique(newFnNode, isCallInLoop);
// Function declarations must be rewritten as function expressions as
// they will be within a block and normalization prevents function
// declarations within block as browser implementations vary.
rewriteFunctionDeclarations(newFnNode.getLastChild());
}
// TODO(johnlenz): Mark NAME nodes constant for parameters that are not modified.
Set namesToAlias = functionArgumentInjector.findModifiedParameters(newFnNode);
ImmutableMap args =
functionArgumentInjector.getFunctionCallParameterMap(
newFnNode, callNode, this.safeNameIdSupplier);
boolean hasArgs = !args.isEmpty();
if (hasArgs) {
functionArgumentInjector.maybeAddTempsForCallArguments(
compiler, newFnNode, args, namesToAlias, compiler.getCodingConvention());
}
Node newBlock = NodeUtil.getFunctionBody(newFnNode);
// Make the newBlock insertable .
newBlock.detach();
if (hasArgs) {
Node inlineResult = aliasAndInlineArguments(newBlock, args, namesToAlias);
checkState(newBlock == inlineResult);
}
//
// For calls inlined into loops, VAR declarations are not reinitialized to
// undefined as they would have been if the function were called, so ensure
// that they are properly initialized.
//
if (isCallInLoop) {
fixUninitializedVarDeclarations(newBlock, newBlock);
}
String labelName = getLabelNameForFunction(fnName);
Node injectableBlock = replaceReturns(
newBlock, resultName, labelName, needsDefaultResult);
checkState(injectableBlock != null);
return injectableBlock;
}
/**
* @param n The node to inspect
*/
private static @Nullable Node rewriteFunctionDeclarations(Node n) {
if (n.isFunction()) {
if (NodeUtil.isFunctionDeclaration(n)) {
// Rewrite: function f() {} ==> var f = function() {}
Node fnNameNode = n.getFirstChild();
Node name = IR.name(fnNameNode.getString()).srcref(fnNameNode);
Node var = IR.var(name).srcref(n);
fnNameNode.setString("");
// Add the VAR, remove the FUNCTION
n.replaceWith(var);
// readd the function as a function expression
name.addChildToFront(n);
return var;
}
return null;
}
// Keep track of any rewritten functions and hoist them to the top
// of the block they are defined in. This isn't fully compliant hoisting
// but it does address a large set of use cases.
List functionsToHoist = new ArrayList<>();
for (Node c = n.getFirstChild(), next; c != null; c = next) {
next = c.getNext(); // We may rewrite "c"
Node fnVar = rewriteFunctionDeclarations(c);
if (fnVar != null) {
functionsToHoist.add(0, fnVar);
}
}
for (Node fnVar : functionsToHoist) {
if (n.getFirstChild() != fnVar) {
n.addChildToFront(fnVar.detach());
}
}
return null;
}
/**
* For all VAR node with uninitialized declarations, set
* the values to be "undefined".
*/
private static void fixUninitializedVarDeclarations(Node n, Node containingBlock) {
// Inner loop structure must already have logic to initialize its
// variables. In particular FOR-IN structures must not be modified.
if (NodeUtil.isLoopStructure(n)) {
return;
}
if ((n.isVar() || n.isLet()) && n.hasOneChild()) {
Node name = n.getFirstChild();
// It isn't initialized.
if (!name.hasChildren()) {
Node srcLocation = name;
name.addChildToBack(NodeUtil.newUndefinedNode(srcLocation));
containingBlock.addChildToFront(n.detach());
}
return;
}
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
fixUninitializedVarDeclarations(c, containingBlock);
}
}
/**
* Fix-up all local names to be unique for this subtree.
* @param fnNode A mutable instance of the function to be inlined.
*/
private void makeLocalNamesUnique(Node fnNode, boolean isCallInLoop) {
Supplier idSupplier = compiler.getUniqueNameIdSupplier();
// Make variable names unique to this instance.
NodeTraversal.traverseScopeRoots(
compiler,
null,
ImmutableList.of(fnNode),
new MakeDeclaredNamesUnique(
new InlineRenamer(
compiler.getCodingConvention(), idSupplier, "inline_", isCallInLoop, true, null),
false),
true);
// Make label names unique to this instance.
new RenameLabels(compiler, new LabelNameSupplier(idSupplier), false, false)
.process(null, fnNode);
}
static class LabelNameSupplier implements Supplier {
final Supplier idSupplier;
LabelNameSupplier(Supplier idSupplier) {
this.idSupplier = idSupplier;
}
@Override
public String get() {
return "JSCompiler_inline_label_" + idSupplier.get();
}
}
/**
* Create a unique label name.
*/
private String getLabelNameForFunction(String fnName) {
String name = (isNullOrEmpty(fnName)) ? "anon" : fnName;
return "JSCompiler_inline_label_" + name + "_" + safeNameIdSupplier.get();
}
/**
* Create a unique "this" name.
*/
private String getUniqueThisName() {
return "JSCompiler_inline_this_" + safeNameIdSupplier.get();
}
/**
* Inlines the arguments within the node tree using the given argument map,
* replaces "unsafe" names with local aliases.
*
* The aliases for unsafe require new VAR declarations, so this function
* can not be used in for direct CALL node replacement as VAR nodes can not be
* created there.
*
* @return The node or its replacement.
*/
private Node aliasAndInlineArguments(
Node fnTemplateRoot, ImmutableMap argMap, Set namesToAlias) {
if (namesToAlias == null || namesToAlias.isEmpty()) {
// There are no names to alias, just inline the arguments directly.
Node result = functionArgumentInjector.inject(compiler, fnTemplateRoot, null, argMap);
checkState(result == fnTemplateRoot);
return result;
} else {
// Create local alias of names that can not be safely
// used directly.
// An arg map that will be updated to contain the
// safe aliases.
Map newArgMap = new LinkedHashMap<>(argMap);
// Declare the alias in the same order as they
// are declared.
List newVars = new ArrayList<>();
// NOTE: argMap is a linked map so we get the parameters in the
// order that they were declared.
for (Entry entry : argMap.entrySet()) {
String name = entry.getKey();
if (namesToAlias.contains(name)) {
if (name.equals(THIS_MARKER)) {
boolean referencesThis = NodeUtil.referencesEnclosingReceiver(fnTemplateRoot);
// Update "this", this is only necessary if "this" is referenced
// and the value of "this" is not Token.THIS, or the value of "this"
// has side effects.
Node value = entry.getValue();
if (!value.isThis()
&& (referencesThis || compiler.getAstAnalyzer().mayHaveSideEffects(value))) {
String newName = getUniqueThisName();
Node newValue = entry.getValue().cloneTree();
Node newNode = NodeUtil.newVarNode(newName, newValue).srcrefTreeIfMissing(newValue);
newVars.add(0, newNode);
// Remove the parameter from the list to replace.
newArgMap.put(THIS_MARKER, IR.name(newName).srcrefTree(newValue));
}
} else {
Node newValue = entry.getValue().cloneTree();
Node newNode = NodeUtil.newVarNode(name, newValue).srcrefTreeIfMissing(newValue);
newVars.add(0, newNode);
// Remove the parameter from the list to replace.
newArgMap.remove(name);
}
}
}
// Inline the arguments.
Node result = functionArgumentInjector.inject(compiler, fnTemplateRoot, null, newArgMap);
checkState(result == fnTemplateRoot);
// Now that the names have been replaced, add the new aliases for
// the old names.
for (Node n : newVars) {
fnTemplateRoot.addChildToFront(n);
}
return result;
}
}
/**
* Convert returns to assignments and breaks, as needed.
* For example, with a labelName of 'foo':
* {
* return a;
* }
* becomes:
* foo: {
* a;
* break foo;
* }
* or
* foo: {
* resultName = a;
* break foo;
* }
*
* @param resultMustBeSet Whether the result must always be set to a value.
* @return The node containing the transformed block, this may be different
* than the passed in node 'block'.
*/
private static Node replaceReturns(
Node block, String resultName, String labelName,
boolean resultMustBeSet) {
checkNotNull(block);
checkNotNull(labelName);
Node root = block;
boolean hasReturnAtExit = false;
int returnCount = NodeUtil.getNodeTypeReferenceCount(
block, Token.RETURN, new NodeUtil.MatchShallowStatement());
if (returnCount > 0) {
hasReturnAtExit = hasReturnAtExit(block);
// TODO(johnlenz): Simpler not to special case this,
// and let it be optimized later.
if (hasReturnAtExit) {
convertLastReturnToStatement(block, resultName);
returnCount--;
}
if (returnCount > 0) {
// A label and breaks are needed.
// Add the breaks
replaceReturnWithBreak(block, null, resultName, labelName);
// Add label
Node name = IR.labelName(labelName).srcref(block);
Node label = IR.label(name, block).srcref(block);
Node newRoot = IR.block().srcref(block);
newRoot.addChildToBack(label);
// The label is now the root.
root = newRoot;
}
}
// If there wasn't an return at the end of the function block, and we need
// a result, add one to the block.
if (resultMustBeSet && !hasReturnAtExit && resultName != null) {
addDummyAssignment(block, resultName);
}
return root;
}
/**********************************************************************
* Functions following here are general node transformation functions
**********************************************************************/
/**
* Example:
* a = (void) 0;
*/
private static void addDummyAssignment(Node node, String resultName) {
checkArgument(node.isBlock());
// A result is needed create a dummy value.
Node srcLocation = node;
Node retVal = NodeUtil.newUndefinedNode(srcLocation);
Node resultNode = createAssignStatementNode(resultName, retVal);
resultNode.srcrefTreeIfMissing(node);
node.addChildToBack(resultNode);
}
/**
* Replace the 'return' statement with its child expression. "return foo()" becomes "foo()" or
* "resultName = foo()" "return" is removed or becomes "resultName = void 0".
*/
private static void convertLastReturnToStatement(Node block, String resultName) {
Node ret = block.getLastChild();
checkArgument(ret.isReturn());
Node resultNode = getReplacementReturnStatement(ret, resultName);
if (resultNode == null) {
ret.detach();
} else {
resultNode.srcrefTreeIfMissing(ret);
ret.replaceWith(resultNode);
}
}
/**
* Create a valid statement Node containing an assignment to name of the
* given expression.
*/
private static Node createAssignStatementNode(String name, Node expression) {
// Create 'name = result-expression;' statement.
// EXPR (ASSIGN (NAME, EXPRESSION))
Node nameNode = IR.name(name);
Node assign = IR.assign(nameNode, expression);
return NodeUtil.newExpr(assign);
}
/**
* Replace the 'return' statement with its child expression.
* If the result is needed (resultName != null):
* "return foo()" becomes "resultName = foo()"
* "return" becomes "resultName = void 0".
* Otherwise:
* "return foo()" becomes "foo()"
* "return", null is returned.
*/
private static Node getReplacementReturnStatement(
Node node, String resultName) {
Node resultNode = null;
Node retVal = null;
if (node.hasChildren()) {
// Clone the child as the child hasn't been removed
// from the node yet.
retVal = node.getFirstChild().cloneTree();
}
if (resultName == null) {
if (retVal != null) {
resultNode = NodeUtil.newExpr(retVal); // maybe null.
}
} else {
if (retVal == null) {
// A result is needed create a dummy value.
Node srcLocation = node;
retVal = NodeUtil.newUndefinedNode(srcLocation);
}
// Create a "resultName = retVal;" statement.
resultNode = createAssignStatementNode(resultName, retVal);
}
return resultNode;
}
/**
* @return Whether the given block end with an return statement.
*/
private static boolean hasReturnAtExit(Node block) {
// Only inline functions that return something (empty returns
// will be handled by ConstFolding+EmptyFunctionRemoval)
return (block.getLastChild().isReturn());
}
/**
* Replace the 'return' statement with its child expression. "return foo()" becomes "{foo();
* break;}" or "{resultName = foo(); break;}" "return" becomes {break;} or "{resultName = void
* 0;break;}".
*/
private static Node replaceReturnWithBreak(
Node current, @Nullable Node parent, String resultName, String labelName) {
if (current.isFunction()
|| current.isExprResult()) {
// Don't recurse into functions definitions, and expressions can't
// contain RETURN nodes.
return current;
}
if (current.isReturn()) {
checkState(NodeUtil.isStatementBlock(parent));
Node resultNode = getReplacementReturnStatement(current, resultName);
Node breakNode = IR.breakNode(IR.labelName(labelName));
// Replace the node in parent, and reset current to the first new child.
breakNode.srcrefTreeIfMissing(current);
current.replaceWith(breakNode);
if (resultNode != null) {
resultNode.srcrefTreeIfMissing(current);
resultNode.insertBefore(breakNode);
}
current = breakNode;
} else {
for (Node c = current.getFirstChild(); c != null; c = c.getNext()) {
// c may be replaced.
c = replaceReturnWithBreak(c, current, resultName, labelName);
}
}
return current;
}
}