com.google.javascript.jscomp.ParenthesizeFunctionsInChunks Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2022 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.checkState;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.javascript.jscomp.diagnostic.LogFile;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/**
* Marks all functions of specified chunks for eager parsing by adding the node property
* Node.MARK_FOR_PARENTHESIZE, which will wrap the fn in ().
*
* For non function expressions, we re-write the expression as follows:
*
*
* - Before: function foo() { ... }
*
- After: var foo = (function() { ... })
*
*
* A log file is created 'eager_compile_chunks.log' with output on how many functions were marked
* for eager compile for each specified chunk.
*/
public final class ParenthesizeFunctionsInChunks implements CompilerPass {
private final AbstractCompiler compiler;
private final Set parenthesizeFunctionsInChunks;
/**
* @param compiler An abstract compiler.
* @param parenthesizeFunctionsInChunks The set of chunk names in which to parenthesize top level
* functions.
*/
public ParenthesizeFunctionsInChunks(
AbstractCompiler compiler, Set parenthesizeFunctionsInChunks) {
this.compiler = compiler;
this.parenthesizeFunctionsInChunks = parenthesizeFunctionsInChunks;
}
@Override
public void process(Node externs, Node root) {
Traversal traversal = new Traversal(parenthesizeFunctionsInChunks);
NodeTraversal.traverse(compiler, root, traversal);
Map chunkToEagerCompileFnCounts = traversal.getChunkToEagerCompileFnCounts();
try (LogFile log = compiler.createOrReopenLog(getClass(), "eager_compile_chunks.log")) {
for (Map.Entry entry : chunkToEagerCompileFnCounts.entrySet()) {
log.log("%s: %d fn's marked for eager compile", entry.getKey(), entry.getValue());
}
}
}
private static class Traversal implements NodeTraversal.Callback {
private final Set parenthesizeFunctionsInChunks;
// The stack of nested block scopes for the node we're currently visiting.
private final Deque nestedBlockScopes = new ArrayDeque<>();
// A multimap relating a scope node to any children nodes which should be hosted into its scope.
private final ListMultimap hoistNodesToScope = ArrayListMultimap.create();
// Map for recording diagnostic information about what was marked for eager compilation.
private final Map chunkToEagerCompileFnCounts = new HashMap<>();
public Traversal(Set parenthesizeFunctionsInChunks) {
this.parenthesizeFunctionsInChunks = parenthesizeFunctionsInChunks;
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (!shouldParenthesizeTree(t, n)) {
return false;
}
if (parent != null && parent.isFunction()) {
return false; // Don't visit the contents of any functions.
}
if (NodeUtil.isStatementBlock(n)) {
nestedBlockScopes.push(n); // Enter block scope.
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (isInnerMostBlockScope(n)) {
hoistChildrenToTopOfScope(n);
nestedBlockScopes.pop(); // Exit block scope.
} else if (NodeUtil.isFunctionExpression(n)) {
n.setMarkForParenthesize(true);
incrementEagerlyCompiledFunctionCount(t);
} else if (NodeUtil.isFunctionDeclaration(n)) {
n.setMarkForParenthesize(true);
addChildForHoistToScope(functionDeclarationToFunctionExpression(t, n));
incrementEagerlyCompiledFunctionCount(t);
} else {
// Ex: Method function definitions.
// Do nothing.
}
}
public Map getChunkToEagerCompileFnCounts() {
checkState(
nestedBlockScopes.isEmpty(), "Expected empty scope stack. Got: %s", nestedBlockScopes);
checkState(
hoistNodesToScope.isEmpty(), "Expected empty hoist map. Got: %s", hoistNodesToScope);
return chunkToEagerCompileFnCounts;
}
/**
* Whether we should parenthesize functions in the given tree of the traversal. If there is no
* chunk, then we are compiling a single-chunk output, so we will just parenthesize all
* top-level functions.
*/
private boolean shouldParenthesizeTree(NodeTraversal t, Node n) {
if (!n.isScript()) {
return true;
}
String chunkName = getChunkName(t);
return chunkName == null || parenthesizeFunctionsInChunks.contains(chunkName);
}
private void incrementEagerlyCompiledFunctionCount(NodeTraversal t) {
chunkToEagerCompileFnCounts.merge(getChunkName(t), 1L, (oldValue, value) -> oldValue + 1);
}
/** Gets the chunk name of the current traveral or null if it doesn't belong to a chunk. */
private @Nullable String getChunkName(NodeTraversal t) {
JSChunk chunk = t.getChunk();
return (chunk != null ? chunk.getName() : null);
}
/** Whether this node is the inner-most block scope. */
private boolean isInnerMostBlockScope(Node n) {
return !nestedBlockScopes.isEmpty() && nestedBlockScopes.peek() == n;
}
/**
* Converts the given function declaration into a function expression suitable for wrapping in
* parenthesis. The function expression is assigned to a `var` declaration so that it is
* semantically hoisted to the inner-most function scope like with function declarations.
*/
private Node functionDeclarationToFunctionExpression(NodeTraversal t, Node n) {
AbstractCompiler compiler = t.getCompiler();
Node nameNode = n.getFirstChild();
Node name = IR.name(nameNode.getString()).srcref(nameNode);
Node var = IR.var(name).srcref(n);
// read the function, but remove the function name, to guard against
// functions that re-assign to themselves, which will end up causing a
// recursive loop.
nameNode.setString("");
compiler.reportChangeToEnclosingScope(n.getLastChild());
// Add the VAR, remove the FUNCTION
n.replaceWith(var);
compiler.reportChangeToEnclosingScope(var);
name.addChildToFront(n);
return var;
}
/**
* Marks a node to be moved to the top of its parent *block* scope. This is useful for hoisting
* function expression assignments to emulate how function declarations are assigned.
*
*
* Function declarations have the following semantics:
* 1. Hoists the function variable declaration to the top of the inner-most *function* scope.
* 2. Hoists the function assignment to the top of the inner-most *block* scope.
*
*/
private void addChildForHoistToScope(Node node) {
if (nestedBlockScopes.isEmpty()) { // This node is already at the root scope.
return;
}
Node innerMostBlockScope = nestedBlockScopes.peek();
hoistNodesToScope.put(innerMostBlockScope, node.detach());
}
/** Hoists any marked nodes to the beginning of this scope. */
private void hoistChildrenToTopOfScope(Node scope) {
List nodes = new ArrayList<>(hoistNodesToScope.removeAll(scope));
Collections.reverse(nodes); // Maintain node order when pushing as first sibling.
for (Node node : nodes) {
scope.addChildToFront(node);
}
}
}
}