com.google.javascript.jscomp.J2clConstantHoisterPass 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.checkState;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* An optimization pass for J2CL-generated code to hoist some constant assignments out clinit method
* to declaration phase so they could be used by other optimization passes for static evaluation.
*/
public class J2clConstantHoisterPass implements CompilerPass {
private final AbstractCompiler compiler;
J2clConstantHoisterPass(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
return;
}
final Multimap fieldAssignments = ArrayListMultimap.create();
final Set hoistableFunctions = new HashSet<>();
NodeTraversal.traversePostOrder(
compiler,
root,
(NodeTraversal t, Node node, Node parent) -> {
// TODO(stalcup): don't gather assignments ourselves, switch to a persistent
// DefinitionUseSiteFinder instead.
if (parent != null && NodeUtil.isLValue(node)) {
fieldAssignments.put(node.getQualifiedName(), parent);
}
// TODO(stalcup): convert to a persistent index of hoistable functions.
if (isHoistableFunction(t, node)) {
hoistableFunctions.add(node);
}
});
for (Collection assignments : fieldAssignments.asMap().values()) {
maybeHoistClassField(assignments, hoistableFunctions);
}
}
/**
* Returns whether the specified rValue is a function which does not receive any variables from
* its containing scope, and is thus 'hoistable'.
*/
private static boolean isHoistableFunction(NodeTraversal t, Node node) {
// TODO(michaelthomas): This could be improved slightly by not assuming that any variable in the
// outer scope is used in the function.
return node.isFunction() && t.getScope().getVarCount() == 0;
}
private void maybeHoistClassField(
Collection assignments, Collection hoistableFunctions) {
// The field is only assigned twice:
if (assignments.size() != 2) {
return;
}
Node first = Iterables.get(assignments, 0);
Node second = Iterables.get(assignments, 1);
// One of them is the top level declaration and the other is the assignment in clinit.
Node topLevelDeclaration = isClassFieldDeclaration(first) ? first : second;
Node clinitAssignment = isClinitFieldAssignment(first) ? first : second;
if (!isClassFieldDeclaration(topLevelDeclaration)
|| !isClinitFieldAssignment(clinitAssignment)) {
return;
}
// And it is assigned to a literal value; hence could be used in static eval and safe to move:
Node assignmentRhs = clinitAssignment.getSecondChild();
if (!NodeUtil.isLiteralValue(assignmentRhs, true /* includeFunctions */)
|| (assignmentRhs.isFunction() && !hoistableFunctions.contains(assignmentRhs))) {
return;
}
// And the assignment are in the same script:
if (NodeUtil.getEnclosingScript(clinitAssignment)
!= NodeUtil.getEnclosingScript(topLevelDeclaration)) {
return;
}
// At this point the only case some could observe the declaration value is the when you have
// cycle between clinits and the field is accessed before initialization; which is almost always
// a bug and GWT never assumed this state is observable in its optimization, yet nobody
// complained. So it is safe to upgrade it to a constant.
hoistConstantLikeField(clinitAssignment, topLevelDeclaration);
}
private void hoistConstantLikeField(Node clinitAssignment, Node topLevelDeclaration) {
Node clinitAssignedValue = clinitAssignment.getSecondChild();
Node declarationInClass = topLevelDeclaration.getFirstChild();
Node declarationAssignedValue = declarationInClass.getFirstChild();
Node clinitChangeScope = NodeUtil.getEnclosingChangeScopeRoot(clinitAssignment);
// Remove the clinit initialization
NodeUtil.removeChild(clinitAssignment.getParent(), clinitAssignment);
clinitAssignedValue.detach();
compiler.reportChangeToChangeScope(clinitChangeScope);
if (declarationAssignedValue == null) {
// Add the value from clinit to the stub declaration
declarationInClass.addChildToFront(clinitAssignedValue);
compiler.reportChangeToEnclosingScope(topLevelDeclaration);
} else if (!declarationAssignedValue.isEquivalentTo(clinitAssignedValue)) {
checkState(NodeUtil.isLiteralValue(declarationAssignedValue, false /* includeFunctions */));
// Replace the assignment in declaration with the value from clinit
declarationInClass.replaceChild(declarationAssignedValue, clinitAssignedValue);
compiler.reportChangeToEnclosingScope(topLevelDeclaration);
}
declarationInClass.setDeclaredConstantVar(true);
}
/**
* Matches literal value declarations {@code var foo = 3;} or stub declarations like
* {@code var bar;}.
*/
private static boolean isClassFieldDeclaration(Node node) {
return node.getParent().isScript()
&& node.isVar()
&& (!node.getFirstChild().hasChildren()
|| (node.getFirstFirstChild() != null
&& NodeUtil.isLiteralValue(
node.getFirstFirstChild(), false /* includeFunctions */)));
}
private static boolean isClinitFieldAssignment(Node node) {
return node.getParent().isExprResult()
&& node.getGrandparent().isBlock()
&& isClinitMethod(node.getGrandparent().getParent());
}
// TODO(goktug): Create a utility to share this logic and start using getQualifiedOriginalName.
private static boolean isClinitMethod(Node fnNode) {
if (!fnNode.isFunction()) {
return false;
}
String fnName = NodeUtil.getName(fnNode);
return fnName != null && isClinitMethodName(fnName);
}
private static boolean isClinitMethodName(String methodName) {
return methodName != null
&& (methodName.endsWith("$$0clinit") || methodName.endsWith(".$clinit"));
}
}