com.google.javascript.jscomp.J2clClinitPrunerPass Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded 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 2016 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 com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/** An optimization pass to prune J2CL clinits. */
public class J2clClinitPrunerPass implements CompilerPass {
private final Map emptiedClinitMethods = new LinkedHashMap<>();
private final AbstractCompiler compiler;
private final List changedScopeNodes;
J2clClinitPrunerPass(AbstractCompiler compiler, List changedScopeNodes) {
this.compiler = compiler;
this.changedScopeNodes = changedScopeNodes;
}
@Override
public void process(Node externs, Node root) {
if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
return;
}
removeRedundantClinits(root, changedScopeNodes);
pruneEmptyClinits(root, changedScopeNodes);
if (emptiedClinitMethods.isEmpty()) {
// Since no clinits are pruned, we don't need look for more opportunities.
return;
}
Multimap clinitReferences = collectClinitReferences(root);
do {
List newChangedScopes = cleanEmptyClinitReferences(clinitReferences);
pruneEmptyClinits(root, newChangedScopes);
} while (!emptiedClinitMethods.isEmpty());
// Update the function side-effect markers on the AST.
// Removing a clinit from a function may make it side-effect free.
new PureFunctionIdentifier.Driver(compiler).process(externs, root);
}
private void removeRedundantClinits(Node root, List changedScopeNodes) {
List changedRoots = getNonNestedParentScopeNodes(changedScopeNodes);
NodeTraversal.traverseScopeRoots(
compiler, root, changedRoots, new RedundantClinitPruner(changedRoots), true);
NodeTraversal.traverseScopeRoots(
compiler, root, changedScopeNodes, new LookAheadRedundantClinitPruner(), false);
}
private void pruneEmptyClinits(Node root, List changedScopes) {
// Clear emptiedClinitMethods before EmptyClinitPruner to populate only with new ones.
emptiedClinitMethods.clear();
NodeTraversal.traverseScopeRoots(
compiler, root, changedScopes, new EmptyClinitPruner(), false);
// Make sure replacements are to final destination instead of pointing intermediate ones.
for (Entry clinitReplacementEntry : emptiedClinitMethods.entrySet()) {
clinitReplacementEntry.setValue(resolveReplacement(clinitReplacementEntry.getValue()));
}
}
private @Nullable Node resolveReplacement(Node node) {
if (node == null) {
return null;
}
String clinitName = getClinitMethodName(node);
return emptiedClinitMethods.containsKey(clinitName)
? resolveReplacement(emptiedClinitMethods.get(clinitName))
: node;
}
private Multimap collectClinitReferences(Node root) {
final Multimap clinitReferences = HashMultimap.create();
NodeTraversal.traverse(
compiler,
root,
new AbstractPostOrderCallback() {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
String clinitName = getClinitMethodName(node);
if (clinitName != null) {
clinitReferences.put(clinitName, node);
}
}
});
return clinitReferences;
}
private List cleanEmptyClinitReferences(Multimap clinitReferences) {
final List newChangedScopes = new ArrayList<>();
// resolveReplacement step above should not require any particular iteration order of the
// emptiedClinitMethods but we are using LinkedHashMap to be extra safe.
for (Entry clinitReplacementEntry : emptiedClinitMethods.entrySet()) {
String clinitName = clinitReplacementEntry.getKey();
Node replacement = clinitReplacementEntry.getValue();
Collection references = clinitReferences.removeAll(clinitName);
for (Node reference : references) {
Node changedScope = NodeUtil.getEnclosingChangeScopeRoot(reference.getParent());
if (replacement == null) {
NodeUtil.deleteFunctionCall(reference, compiler);
} else {
replacement = replacement.cloneTree();
reference.replaceWith(replacement);
compiler.reportChangeToChangeScope(changedScope);
clinitReferences.put(getClinitMethodName(replacement), replacement);
}
newChangedScopes.add(changedScope);
}
}
return newChangedScopes;
}
private static @Nullable List getNonNestedParentScopeNodes(List changedScopeNodes) {
return changedScopeNodes == null
? null
: NodeUtil.removeNestedChangeScopeNodes(
NodeUtil.getParentChangeScopeNodes(changedScopeNodes));
}
/** Removes redundant clinit calls inside method body if it is guaranteed to be called earlier. */
private final class RedundantClinitPruner implements NodeTraversal.Callback {
private final ImmutableSet roots;
private final Deque> stateStack = new ArrayDeque<>();
private HierarchicalSet clinitsCalledAtBranch = new HierarchicalSet<>(null);
RedundantClinitPruner(Iterable roots) {
this.roots = (roots == null) ? ImmutableSet.of() : ImmutableSet.copyOf(roots);
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node node, Node parent) {
if (this.roots.contains(node)) {
// Reset the clinit call tracking when starting over on a new scope.
clinitsCalledAtBranch = new HierarchicalSet<>(null);
stateStack.clear();
}
if (NodeUtil.isFunctionDeclaration(node) || node.isScript()) {
// In the case of function declarations, unlike function expressions, we don't know when the
// function will be executed so assume there are no clinits already executed.
// In the case of script roots, when using modules, we can not assume the scripts are going
// to be executed in program order.
stateStack.addLast(clinitsCalledAtBranch);
clinitsCalledAtBranch = new HierarchicalSet<>(null);
}
if (isNewControlBranch(parent)) {
clinitsCalledAtBranch = new HierarchicalSet<>(clinitsCalledAtBranch);
if (isClinitMethod(parent)) {
// Adds itself as any of your children can assume clinit is already called.
clinitsCalledAtBranch.add(getQualifiedNameOfFunction(parent));
}
}
return true;
}
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
tryRemovingClinit(node);
if (isNewControlBranch(parent)) {
clinitsCalledAtBranch = clinitsCalledAtBranch.parent;
}
if (NodeUtil.isFunctionDeclaration(node) || node.isScript()) {
// In the case of function declarations, unlike function expressions, we don't know when the
// function will be executed so assume there are no clinits already executed.
// In the case of script roots, when using modules, we can not assume the scripts are going
// to be executed in program order.
clinitsCalledAtBranch = stateStack.removeLast();
}
}
private void tryRemovingClinit(Node node) {
String clinitName = getClinitMethodName(node);
if (clinitName == null) {
return;
}
if (clinitsCalledAtBranch.add(clinitName)) {
// This is the first time we are seeing this clinit so cannot remove it.
return;
}
NodeUtil.deleteFunctionCall(node, compiler);
}
private boolean isNewControlBranch(Node n) {
return n != null
&& (NodeUtil.isControlStructure(n)
|| n.isDefaultValue()
|| n.isHook()
|| n.isAnd()
|| n.isOr()
|| n.isFunction());
}
}
// TODO(michaelthomas): Prune clinit calls in functions, if previous functions or the immediate
// next function guarantees clinit call. With that we won't need this pass.
/**
* Prunes clinit calls which immediately precede calls to a static function which calls the same
* clinit. e.g. "Foo.clinit(); return new Foo()" -> "return new Foo()"
*/
private final class LookAheadRedundantClinitPruner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (!node.isExprResult()) {
return;
}
// Find clinit calls.
String clinitName = getClinitMethodName(node.getFirstChild());
if (clinitName == null) {
return;
}
// Check for calls to a static method immediately following the clinit call.
Node callOrNewNode = getCallOrNewNode(node.getNext());
if (callOrNewNode == null || !callOrNewNode.getFirstChild().isName()) {
return;
}
// Check that the call isn't a recursive call to the same function.
Node enclosingFunction = NodeUtil.getEnclosingFunction(node);
if (enclosingFunction == null || callOrNewNode.getFirstChild().getString()
.equals(NodeUtil.getNearestFunctionName(enclosingFunction))) {
return;
}
// Find the definition of the function being called.
Var var = t.getScope().getVar(callOrNewNode.getFirstChild().getString());
if (var == null || var.getInitialValue() == null || !var.getInitialValue().isFunction()) {
return;
}
// Check that the clinit call is safe to prune.
Node staticFnNode = var.getInitialValue();
if (callsClinit(staticFnNode, clinitName) && hasSafeArguments(t, callOrNewNode)) {
node.detach();
compiler.reportChangeToEnclosingScope(parent);
}
}
/**
* Returns whether the arguments to the specified call/new node are "safe". i.e. they are only
* parameters to the enclosing function or literal values (and not e.g. a static field reference
* which might need the clinit we are trying to remove).
*/
private boolean hasSafeArguments(NodeTraversal t, Node callOrNewNode) {
Node child = callOrNewNode.getSecondChild();
while (child != null) {
if (!NodeUtil.isLiteralValue(child, /* includeFunctions= */ false)
&& !isParameter(t, child)) {
return false;
}
child = child.getNext();
}
return true;
}
/** Returns whether the specified node is defined as a parameter to its enclosing function. */
private boolean isParameter(NodeTraversal t, Node n) {
if (!n.isName()) {
return false;
}
Var var = t.getScope().getVar(n.getString());
return var.getParentNode().isParamList();
}
/**
* Returns the call node associated with the specified node if one exists, otherwise returns
* null.
*/
private @Nullable Node getCallOrNewNode(Node n) {
if (n == null) {
return null;
}
switch (n.getToken()) {
case EXPR_RESULT:
case RETURN:
return getCallOrNewNode(n.getFirstChild());
case CALL:
case NEW:
return n;
case CONST:
case LET:
case VAR:
return n.hasOneChild() ? getCallOrNewNode(n.getFirstFirstChild()) : null;
default:
return null;
}
}
/** Returns whether the specified function contains a call to the specified clinit. */
private boolean callsClinit(Node fnNode, String clinitName) {
checkNotNull(clinitName);
// TODO(michaelthomas): Consider checking all children, but watch out for return statements
// that could short-circuit the clinit.
Node child = fnNode.getLastChild().getFirstChild();
return child != null
&& child.isExprResult()
&& clinitName.equals(getClinitMethodName(child.getFirstChild()));
}
}
/** A traversal callback that removes the body of empty clinits. */
private final class EmptyClinitPruner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (!isClinitMethod(node)) {
return;
}
trySubstituteEmptyFunction(node);
}
/** Clears the body of any functions that are equivalent to empty functions. */
private void trySubstituteEmptyFunction(Node fnNode) {
String fnQualifiedName = getQualifiedNameOfFunction(fnNode);
// Ignore anonymous/constructor functions.
if (Strings.isNullOrEmpty(fnQualifiedName)) {
return;
}
Node body = fnNode.getLastChild();
if (!body.hasChildren()) {
return;
}
// Ensure that the first expression in the body is setting itself to the empty function
Node firstExpr = body.getFirstChild();
if (!isAssignToEmptyFn(firstExpr, fnQualifiedName)) {
return;
}
Node secondExpr = firstExpr.getNext();
Node replaceReferencesWith;
if (secondExpr == null) {
// There are no expressions in clinit so it is noop; remove all references.
replaceReferencesWith = null;
} else if (secondExpr.getNext() == null
&& secondExpr.isExprResult()
&& getClinitMethodName(secondExpr.getFirstChild()) != null) {
// Only expression in clinit is a call to another clinit. We can safely replace all
// references with the other clinit.
replaceReferencesWith = secondExpr.getFirstChild();
} else {
// Clinit is not empty.
return;
}
emptiedClinitMethods.put(fnQualifiedName, replaceReferencesWith);
NodeUtil.deleteNode(firstExpr, compiler);
}
private boolean isAssignToEmptyFn(Node node, String enclosingFnName) {
if (!NodeUtil.isExprAssign(node)) {
return false;
}
Node lhs = node.getFirstFirstChild();
if (lhs.isGetElem()) {
// This can happen if code has been inserted, for example for instrumentation.
return false;
}
checkState(lhs.isName() || lhs.isGetProp(), lhs);
Node rhs = node.getFirstChild().getLastChild();
return NodeUtil.isEmptyFunctionExpression(rhs)
&& Objects.equals(NodeUtil.getBestLValueName(lhs), enclosingFnName);
}
}
private static boolean isClinitMethod(Node node) {
return node.isFunction() && isClinitMethodName(getQualifiedNameOfFunction(node));
}
private static @Nullable String getClinitMethodName(Node node) {
if (node.isCall()) {
String fnName = NodeUtil.getBestLValueName(node.getFirstChild());
return isClinitMethodName(fnName) ? fnName : null;
}
return null;
}
private static boolean isClinitMethodName(String fnName) {
// The '.$clinit' case) only happens when collapseProperties is off.
return fnName != null && (fnName.endsWith("$$0clinit") || fnName.endsWith(".$clinit"));
}
/**
* A minimalist implelementation of a hierarchical Set where an item might exist in current set or
* any of its parents.
*/
private static class HierarchicalSet {
private final Set currentSet = new HashSet<>();
private final @Nullable HierarchicalSet parent;
public HierarchicalSet(@Nullable HierarchicalSet parent) {
this.parent = parent;
}
public boolean add(T o) {
return !parentsContains(o) && currentSet.add(o);
}
/** Returns true either my parent or any of its parents contains the item. */
private boolean parentsContains(T o) {
return parent != null && (parent.currentSet.contains(o) || parent.parentsContains(o));
}
}
/**
* Returns the qualifed name {@code function} is being assigned to.
*
* This implementation abstracts over the various ways of naming a function. ASSIGN need not be
* involved and there may not be a sequence of GETPROPs representing the name.
*
*
TODO(b/123354857): Delete this method when naming concepts have been unified. It was created
* as a temporary measure to support ES6 in this pass without becoming blocked on name APIs.
*/
private static String getQualifiedNameOfFunction(Node function) {
checkArgument(function.isFunction(), function);
// The node representing the name (e.g. GETPROP, MEMBER_FUNCTION_DEF, etc.).
Node lValue = NodeUtil.getBestLValue(function);
return NodeUtil.getBestLValueName(lValue);
}
}