com.google.javascript.jscomp.J2clPass Maven / Gradle / Ivy
Show all versions of closure-compiler Show documentation
/*
* Copyright 2015 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 com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.FunctionInjector.InliningMode;
import com.google.javascript.jscomp.FunctionInjector.Reference;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Rewrites/inlines some J2CL constructs to be more optimizable.
*/
public class J2clPass implements CompilerPass {
private static final String ALL_CLASS_FILE_NAMES = "*";
private final AbstractCompiler compiler;
private final Supplier safeNameIdSupplier;
private class GetDefineRewriter extends AbstractPostOrderCallback {
private final Set defines;
GetDefineRewriter(Set defines) {
this.defines = defines;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (isUtilGetDefineCall(n)) {
substituteUtilGetDefine(t, n);
}
}
private void substituteUtilGetDefine(NodeTraversal t, Node callNode) {
Node firstExpr = callNode.getSecondChild();
Node secondExpr = callNode.getLastChild();
if (secondExpr != firstExpr) {
secondExpr.detach();
} else {
// There is no secondExpr; default to null.
secondExpr = IR.nullNode();
}
Node replacement = getDefineReplacement(firstExpr, secondExpr);
replacement.useSourceInfoIfMissingFromForTree(callNode);
callNode.replaceWith(replacement);
t.reportCodeChange();
}
private Node getDefineReplacement(Node firstExpr, Node secondExpr) {
if (defines.contains(firstExpr.getString())) {
Node define = NodeUtil.newQName(compiler, firstExpr.getString());
Node defineStringValue = NodeUtil.newCallNode(IR.name("String"), define);
return IR.comma(secondExpr, defineStringValue);
} else {
return secondExpr;
}
}
private boolean isUtilGetDefineCall(Node n) {
return n.isCall() && isUtilGetDefineMethodName(n.getFirstChild().getQualifiedName());
}
private boolean isUtilGetDefineMethodName(String fnName) {
// TODO: Switch this to the filename + property name heuristic which is less brittle.
return fnName != null && fnName.endsWith(".$getDefine") && fnName.contains("Util");
}
}
/**
* Forcefully inlines aliases to native types to allow further optimizations.
*
* e.g. "let $RegExp = window.RegExp; var foo = new $RegExp;" becomes "var foo = new RegExp;"
*/
private static class NativeAliasInliner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName()
&& !NodeUtil.isNameDeclaration(parent)
&& !parent.isAssign()) {
Node declaringNode = getDeclaringNode(t, n);
if (declaringNode != null
&& declaringNode.getFirstChild() != n
&& isNativeAlias(declaringNode)) {
parent.replaceChild(n, declaringNode.getFirstFirstChild().cloneTree());
t.reportCodeChange();
}
}
}
private Node getDeclaringNode(NodeTraversal t, Node nameNode) {
Var var = t.getScope().getVar(nameNode.getString());
return var == null ? null : var.getParentNode();
}
private boolean isNativeAlias(Node n) {
return NodeUtil.isNameDeclaration(n)
&& n.getParent().isScript()
&& n.getJSDocInfo() != null
&& n.getJSDocInfo().isConstructor()
&& n.getFirstFirstChild() != null
&& n.getFirstFirstChild().isGetProp()
&& n.getFirstFirstChild().getQualifiedName().matches("window.[A-Z][A-Za-z]+");
}
}
/**
* Collects references to certain function definitions in a certain class and then inlines fully
* qualified static method calls to those functions anywhere in the program.
*
*
Assumes that the set of provided short function names will not collide with any of the
* collected fully qualified function names once the module prefix has been added.
*/
private class ClassStaticFunctionsInliner {
private final String classFileName;
private final Set fnNamesToInline;
private final InliningMode inliningMode;
private final Map fnsToInlineByQualifiedName = new HashMap<>();
private final FunctionInjector injector;
private final Node root;
private ClassStaticFunctionsInliner(
Node root, String classFileName, Set fnNamesToInline, InliningMode inliningMode) {
this.root = root;
this.classFileName = classFileName;
this.fnNamesToInline = fnNamesToInline;
this.inliningMode = inliningMode;
this.injector = new FunctionInjector(compiler, safeNameIdSupplier, true, true, true);
this.injector.setKnownConstants(fnNamesToInline);
}
private void run() {
NodeTraversal.traverseEs6(compiler, root, new FunctionDefsCollector());
NodeTraversal.traverseEs6(compiler, root, new StaticCallInliner());
}
private class FunctionDefsCollector implements Callback {
@Override
public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
// Only look inside the referenced class file.
return !n.isScript()
|| n.getSourceFileName().endsWith(classFileName)
|| classFileName.equals(ALL_CLASS_FILE_NAMES);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// If we arrive here then we're already inside the desired script.
// Only look at named function declarations
if (!n.isAssign() || !n.getLastChild().isFunction()) {
return;
}
// ... that are fully qualified
Node qualifiedNameNode = n.getFirstChild();
if (!qualifiedNameNode.isGetProp() || !qualifiedNameNode.isQualifiedName()) {
return;
}
Node fnNode = n.getLastChild();
String qualifiedFnName = qualifiedNameNode.getQualifiedName();
String fnName = qualifiedNameNode.getLastChild().getString();
if (fnNamesToInline.contains(fnName)) {
// Then store a reference to it.
fnsToInlineByQualifiedName.put(qualifiedFnName, fnNode);
}
}
}
private class StaticCallInliner extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// Only look at method calls
if (!n.isCall()) {
return;
}
// ... that are fully qualified
Node qualifiedNameNode = n.getFirstChild();
if (!qualifiedNameNode.isGetProp() || !qualifiedNameNode.isQualifiedName()) {
return;
}
// ... and that reference a function definition we want to inline
String qualifiedFnName = qualifiedNameNode.getQualifiedName();
String fnName = qualifiedNameNode.getLastChild().getString();
Node fnImpl = fnsToInlineByQualifiedName.get(qualifiedFnName);
if (fnImpl == null) {
return;
}
// Otherwise inline the call.
Node inlinedCall =
injector.inline(
new Reference(n, t.getScope(), t.getModule(), inliningMode), fnName, fnImpl);
t.getCompiler().reportChangeToEnclosingScope(inlinedCall);
}
}
}
public J2clPass(AbstractCompiler compiler) {
this.compiler = compiler;
this.safeNameIdSupplier = compiler.getUniqueNameIdSupplier();
}
@Override
public void process(Node externs, Node root) {
if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
return;
}
/*
* Re-writes Util.getDefine to make it work for compiled mode.
*/
Set defines =
new ProcessDefines(compiler, null, true).collectDefines(externs, root).keySet();
NodeTraversal.traverseEs6(compiler, root, new GetDefineRewriter(defines));
/*
* Inlines Arrays.$create(), Arrays.$init(), Arrays.$instanceIsOfType(), Arrays.$castTo() and
* Casts.$to() so that all references to Object.$isInstance() functions will be fully qualified
* and easy to strip.
*/
inlineFunctionsInFile(
root,
"Arrays.impl.java.js",
ImmutableSet.of("$create", "$init", "$instanceIsOfType", "$castTo", "$stampType"),
InliningMode.DIRECT);
inlineFunctionsInFile(
root, "Casts.impl.java.js", ImmutableSet.of("$to"), InliningMode.DIRECT);
/*
* Inlines all Interface.$markImplementor(FooClass) metaclass calls so that FooClass and others
* like it are not unnecessarily retained and so that static analysis of interface instanceof
* calls becomes possible.
*
* Note that his pass should NOT be restricted to j2cl .java.js files because JavaScript code
* implementing Java interfaces (not recommended but widely used in xplat) needs calls to
* $markImplementor.
*
*/
inlineFunctionsInFile(
root, ALL_CLASS_FILE_NAMES, ImmutableSet.of("$markImplementor"), InliningMode.BLOCK);
/*
* Inlines class metadata calls so they become optimizable and avoids escaping of constructor.
*/
inlineFunctionsInFile(
root,
"Util.impl.java.js",
ImmutableSet.of(
"$setClassMetadata",
"$setClassMetadataForInterface",
"$setClassMetadataForEnum",
"$setClassMetadataForPrimitive"),
InliningMode.BLOCK);
NodeTraversal.traverseEs6(compiler, root, new NativeAliasInliner());
}
private void inlineFunctionsInFile(
Node root, String classFileName, Set fnNamesToInline, InliningMode inliningMode) {
new ClassStaticFunctionsInliner(root, classFileName, fnNamesToInline, inliningMode).run();
}
}