com.google.javascript.jscomp.CrossModuleMethodMotion Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.frontend.js.minifier
Show all versions of com.liferay.frontend.js.minifier
Liferay Frontend JS Minifier
/*
* Copyright 2008 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.javascript.jscomp.AnalyzePrototypeProperties.NameInfo;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Property;
import com.google.javascript.jscomp.AnalyzePrototypeProperties.Symbol;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.Iterator;
/**
* Move prototype methods into later modules.
*
* @author [email protected] (Nick Santos)
*/
class CrossModuleMethodMotion implements CompilerPass {
// Internal errors
static final DiagnosticType NULL_COMMON_MODULE_ERROR = DiagnosticType.error(
"JSC_INTERNAL_ERROR_MODULE_DEPEND",
"null deepest common module");
private final AbstractCompiler compiler;
private final IdGenerator idGenerator;
private final AnalyzePrototypeProperties analyzer;
private final JSModuleGraph moduleGraph;
private final boolean noStubFunctions;
static final String STUB_METHOD_NAME = "JSCompiler_stubMethod";
static final String UNSTUB_METHOD_NAME = "JSCompiler_unstubMethod";
// Visible for testing
static final String STUB_DECLARATIONS =
"var JSCompiler_stubMap = [];" +
"function JSCompiler_stubMethod(JSCompiler_stubMethod_id) {" +
" return function() {" +
" return JSCompiler_stubMap[JSCompiler_stubMethod_id].apply(" +
" this, arguments);" +
" };" +
"}" +
"function JSCompiler_unstubMethod(" +
" JSCompiler_unstubMethod_id, JSCompiler_unstubMethod_body) {" +
" return JSCompiler_stubMap[JSCompiler_unstubMethod_id] = " +
" JSCompiler_unstubMethod_body;" +
"}";
/**
* Creates a new pass for moving prototype properties.
* @param compiler The compiler.
* @param idGenerator An id generator for method stubs.
* @param canModifyExterns If true, then we can move prototype
* properties that are declared in the externs file.
* @param noStubFunctions if true, we can move methods without
* stub functions in the parent module.
*/
CrossModuleMethodMotion(AbstractCompiler compiler, IdGenerator idGenerator,
boolean canModifyExterns, boolean noStubFunctions) {
this.compiler = compiler;
this.idGenerator = idGenerator;
this.moduleGraph = compiler.getModuleGraph();
this.analyzer =
new AnalyzePrototypeProperties(
compiler, moduleGraph, canModifyExterns, false /* anchorUnusedVars */, noStubFunctions);
this.noStubFunctions = noStubFunctions;
}
@Override
public void process(Node externRoot, Node root) {
// If there are < 2 modules, then we will never move anything,
// so we're done.
if (moduleGraph != null && moduleGraph.getModuleCount() > 1) {
analyzer.process(externRoot, root);
moveMethods(analyzer.getAllNameInfo());
}
}
/**
* Move methods deeper in the module graph when possible.
*/
private void moveMethods(Collection allNameInfo) {
boolean hasStubDeclaration = idGenerator.hasGeneratedAnyIds();
for (NameInfo nameInfo : allNameInfo) {
if (!nameInfo.isReferenced()) {
// The code below can't do anything with unreferenced name
// infos. They should be skipped to avoid NPE since their
// deepestCommonModuleRef is null.
continue;
}
if (nameInfo.readsClosureVariables()) {
continue;
}
JSModule deepestCommonModuleRef = nameInfo.getDeepestCommonModuleRef();
if (deepestCommonModuleRef == null) {
compiler.report(JSError.make(NULL_COMMON_MODULE_ERROR));
continue;
}
Iterator declarations =
nameInfo.getDeclarations().descendingIterator();
while (declarations.hasNext()) {
Symbol symbol = declarations.next();
if (!(symbol instanceof Property)) {
continue;
}
Property prop = (Property) symbol;
// We should only move a property across modules if:
// 1) We can move it deeper in the module graph, and
// 2) it's a function, and
// 3) it is not a GETTER_DEF or a SETTER_DEF, and
// 4) the class is available in the global scope.
//
// #1 should be obvious. #2 is more subtle. It's possible
// to copy off of a prototype, as in the code:
// for (var k in Foo.prototype) {
// doSomethingWith(Foo.prototype[k]);
// }
// This is a common way to implement pseudo-multiple inheritance in JS.
//
// So if we move a prototype method into a deeper module, we must
// replace it with a stub function so that it preserves its original
// behavior.
if (prop.getRootVar() == null || !prop.getRootVar().isGlobal()) {
continue;
}
Node value = prop.getValue();
// Only attempt to move normal functions.
if (!value.isFunction()
// A GET or SET can't be deferred like a normal
// FUNCTION property definition as a mix-in would get the result
// of a GET instead of the function itself.
|| value.getParent().isGetterDef()
|| value.getParent().isSetterDef()) {
continue;
}
if (moduleGraph.dependsOn(deepestCommonModuleRef, prop.getModule())) {
if (hasUnmovableRedeclaration(nameInfo, prop)) {
// If it has been redeclared on the same object, skip it.
continue;
}
Node valueParent = value.getParent();
/**
* The logic here moves methods from some starting script node to some other script node.
* Both scripts need to be marked as changed. Locally the removal point in the starting
* script node is called 'valueParent' and the insertion point in the destination script
* is sometimes called 'unstubParent' and sometimes 'destParent'. The change on
* 'valueParent' is being reported before the change occurs since the change is guaranteed
* to occur and since after the change the 'valueParent' node has sometimes already been
* detached.
*/
compiler.reportChangeToEnclosingScope(valueParent);
Node proto = prop.getPrototype();
int stubId = idGenerator.newId();
if (!noStubFunctions) {
// example: JSCompiler_stubMethod(id);
Node stubCall = IR.call(
IR.name(STUB_METHOD_NAME),
IR.number(stubId))
.useSourceInfoIfMissingFromForTree(value);
stubCall.putBooleanProp(Node.FREE_CALL, true);
// stub out the method in the original module
// A.prototype.b = JSCompiler_stubMethod(id);
valueParent.replaceChild(value, stubCall);
// unstub the function body in the deeper module
Node unstubParent = compiler.getNodeForCodeInsertion(
deepestCommonModuleRef);
Node unstubCall = IR.call(
IR.name(UNSTUB_METHOD_NAME),
IR.number(stubId),
value);
unstubCall.putBooleanProp(Node.FREE_CALL, true);
unstubParent.addChildToFront(
// A.prototype.b = JSCompiler_unstubMethod(id, body);
IR.exprResult(
IR.assign(
IR.getprop(
proto.cloneTree(),
IR.string(nameInfo.name)),
unstubCall))
.useSourceInfoIfMissingFromForTree(value));
compiler.reportChangeToEnclosingScope(unstubParent);
} else {
Node assignmentParent = valueParent.getParent();
valueParent.removeChild(value);
// remove Foo.prototype.bar = value
assignmentParent.detach();
Node destParent = compiler.getNodeForCodeInsertion(
deepestCommonModuleRef);
destParent.addChildToFront(
// A.prototype.b = value;
IR.exprResult(
IR.assign(
IR.getprop(
proto.cloneTree(),
IR.string(nameInfo.name)),
value))
.useSourceInfoIfMissingFromForTree(value));
compiler.reportChangeToEnclosingScope(destParent);
}
}
}
}
if (!noStubFunctions && !hasStubDeclaration && idGenerator
.hasGeneratedAnyIds()) {
// Declare stub functions in the top-most module.
Node declarations = compiler.parseSyntheticCode(STUB_DECLARATIONS);
NodeUtil.markNewScopesChanged(declarations, compiler);
Node firstScript = compiler.getNodeForCodeInsertion(null);
firstScript.addChildrenToFront(declarations.removeChildren());
compiler.reportChangeToEnclosingScope(firstScript);
}
}
static boolean hasUnmovableRedeclaration(NameInfo nameInfo, Property prop) {
for (Symbol symbol : nameInfo.getDeclarations()) {
if (!(symbol instanceof Property)) {
continue;
}
Property otherProp = (Property) symbol;
// It is possible to do better here if the dependencies are well defined
// but redefinitions are usually in optional modules so it isn't likely
// worth the effort to check.
if (prop != otherProp
&& prop.getRootVar() == otherProp.getRootVar()
&& prop.getModule() != otherProp.getModule()) {
return true;
}
}
return false;
}
}