com.google.javascript.jscomp.FunctionToBlockMutator Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* 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.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 java.util.function.Supplier;
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),
MakeDeclaredNamesUnique.builder()
.withRenamer(
new InlineRenamer(
compiler.getCodingConvention(),
idSupplier,
"inline_",
isCallInLoop,
true,
null))
.withMarkChanges(false)
.build(),
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;
}
}