com.google.javascript.jscomp.ExternExportsPass Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler 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 2009 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.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Creates an externs file containing all exported symbols and properties
* for later consumption.
*
* @author [email protected] (Devin Coughlin)
*/
final class ExternExportsPass extends NodeTraversal.AbstractPostOrderCallback
implements CompilerPass {
/** The exports found. */
private final List exports;
/** A map of all assigns to their parent nodes. */
private final Map definitionMap;
/** The parent compiler. */
private final AbstractCompiler compiler;
/** The AST root which holds the externs generated. */
private final Node externsRoot;
/** A mapping of internal paths to exported paths. */
private final Map mappedPaths;
/** A list of exported paths. */
private final Set alreadyExportedPaths;
/** A list of function names used to export symbols. */
private List exportSymbolFunctionNames;
/** A list of function names used to export properties. */
private List exportPropertyFunctionNames;
private abstract class Export {
protected final String symbolName;
protected final Node value;
Export(String symbolName, Node value) {
this.symbolName = checkNotNull(symbolName);
this.value = checkNotNull(value);
}
/**
* Generates the externs representation of this export and appends
* it to the externsRoot AST.
*/
void generateExterns() {
appendExtern(getExportedPath(), getValue());
}
/**
* Returns the path exported by this export.
*/
abstract String getExportedPath();
/**
* Appends the exported function and all paths necessary for the path to be
* declared. For example, for a property "a.b.c", the initializers for
* paths "a", "a.b" will be appended (if they have not already) and a.b.c
* will be initialized with the exported version of the function:
*
* var a = {};
* a.b = {};
* a.b.c = function(x,y) { }
*
*/
void appendExtern(String path, Node valueToExport) {
List pathPrefixes = computePathPrefixes(path);
for (int i = 0; i < pathPrefixes.size(); ++i) {
String pathPrefix = pathPrefixes.get(i);
/* The complete path (the last path prefix) must be emitted and
* it gets initialized to the externed version of the value.
*/
boolean isCompletePathPrefix = (i == pathPrefixes.size() - 1);
boolean skipPathPrefix = pathPrefix.endsWith(".prototype")
|| (alreadyExportedPaths.contains(pathPrefix)
&& !isCompletePathPrefix);
boolean exportedValueDefinesNewType = false;
if (valueToExport != null) {
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(valueToExport);
if (jsdoc != null && jsdoc.containsTypeDefinition()) {
exportedValueDefinesNewType = true;
}
}
if (!skipPathPrefix) {
Node initializer;
JSDocInfo jsdoc = null;
/* Namespaces get initialized to {}, functions to
* externed versions of their value, and if we can't
* figure out where the value came from we initialize
* it to {}.
*
* Since externs are always exported in sorted order,
* we know that if we export a.b = function() {} and later
* a.b.c = function then a.b will always be in alreadyExportedPaths
* when we emit a.b.c and thus we will never overwrite the function
* exported for a.b with a namespace.
*/
if (isCompletePathPrefix && valueToExport != null) {
if (valueToExport.isFunction()) {
initializer = createExternFunction(valueToExport);
} else {
checkState(valueToExport.isObjectLit());
initializer = createExternObjectLit(valueToExport);
}
} else if (!isCompletePathPrefix && exportedValueDefinesNewType) {
jsdoc = buildNamespaceJSDoc();
initializer = createExternObjectLit(IR.objectlit());
// Don't add the empty jsdoc here
initializer.setJSDocInfo(null);
} else {
initializer = IR.empty();
}
appendPathDefinition(pathPrefix, initializer, jsdoc);
}
}
}
/**
* Computes a list of the path prefixes constructed from the components
* of the path.
*
* E.g., if the path is:
* "a.b.c"
* then then path prefixes will be
* ["a","a.b","a.b.c"]:
*
*/
private List computePathPrefixes(String path) {
List pieces = Splitter.on('.').splitToList(path);
List pathPrefixes = new ArrayList<>();
for (int i = 0; i < pieces.size(); i++) {
pathPrefixes.add(Joiner.on(".").join(Iterables.limit(pieces, i + 1)));
}
return pathPrefixes;
}
private void appendPathDefinition(
String path, Node initializer, JSDocInfo jsdoc) {
Node pathDefinition;
if (!path.contains(".")) {
if (initializer.isEmpty()) {
pathDefinition = IR.var(IR.name(path));
} else {
pathDefinition = NodeUtil.newVarNode(path, initializer);
}
} else {
Node qualifiedPath = NodeUtil.newQName(compiler, path);
if (initializer.isEmpty()) {
pathDefinition = NodeUtil.newExpr(qualifiedPath);
} else {
pathDefinition = NodeUtil.newExpr(
IR.assign(qualifiedPath, initializer));
}
}
if (jsdoc != null) {
if (pathDefinition.isExprResult()) {
pathDefinition.getFirstChild().setJSDocInfo(jsdoc);
} else {
checkState(pathDefinition.isVar());
pathDefinition.setJSDocInfo(jsdoc);
}
}
externsRoot.addChildToBack(pathDefinition);
alreadyExportedPaths.add(path);
}
/**
* Given a function to export, create the empty function that
* will be put in the externs file. This extern function should have
* the same type as the original function and the same parameter
* name but no function body.
*
* We create a warning here if the the function to export is missing
* parameter or return types.
*/
private Node createExternFunction(Node exportedFunction) {
Node paramList = NodeUtil.getFunctionParameters(exportedFunction)
.cloneTree();
// Use the original parameter names so that the externs look pretty.
Node param = paramList.getFirstChild();
while (param != null && param.isName()) {
String originalName = param.getOriginalName();
if (originalName != null) {
param.setString(originalName);
}
param = param.getNext();
}
Node externFunction = IR.function(IR.name(""), paramList, IR.block());
if (exportedFunction.getTypeI() != null) {
externFunction.setTypeI(exportedFunction.getTypeI());
// When this function is printed, it will have a regular jsdoc, so we
// don't want inline jsdocs as well
deleteInlineJsdocs(externFunction);
}
return externFunction;
}
private void deleteInlineJsdocs(Node fn) {
checkArgument(fn.isFunction());
for (Node param : NodeUtil.getFunctionParameters(fn).children()) {
param.setJSDocInfo(null);
}
// Delete the inline return as well, if any
fn.getFirstChild().setJSDocInfo(null);
}
private JSDocInfo buildEmptyJSDoc() {
// TODO(johnlenz): share the JSDocInfo here rather than building
// a new one each time.
return new JSDocInfoBuilder(false).build(true);
}
private JSDocInfo buildNamespaceJSDoc() {
JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
builder.recordConstancy();
builder.recordSuppressions(ImmutableSet.of("const", "duplicate"));
return builder.build();
}
/**
* Given an object literal to export, create an object lit with all its
* string properties. We don't care what the values of those properties
* are because they are not checked.
*/
private Node createExternObjectLit(Node exportedObjectLit) {
Node lit = IR.objectlit();
lit.setTypeI(exportedObjectLit.getTypeI());
// This is an indirect way of telling the typed code generator
// "print the type of this"
lit.setJSDocInfo(buildEmptyJSDoc());
int index = 1;
for (Node child = exportedObjectLit.getFirstChild();
child != null;
child = child.getNext()) {
// TODO(dimvar): handle getters or setters?
if (child.isStringKey()) {
lit.addChildToBack(
IR.propdef(
IR.stringKey(child.getString()),
IR.number(index++)));
}
}
return lit;
}
/**
* If the given value is a qualified name which refers
* a function or object literal, the node is returned. Otherwise,
* {@code null} is returned.
*/
protected Node getValue() {
String qualifiedName = value.getQualifiedName();
if (qualifiedName == null) {
return null;
}
Node definitionParent = definitionMap.get(qualifiedName);
if (definitionParent == null) {
return null;
}
Node definition;
switch (definitionParent.getToken()) {
case ASSIGN:
definition = definitionParent.getLastChild();
break;
case VAR:
definition = definitionParent.getLastChild().getLastChild();
break;
case FUNCTION:
if (NodeUtil.isFunctionDeclaration(definitionParent)) {
definition = definitionParent;
} else {
return null;
}
break;
default:
return null;
}
if (!definition.isFunction() && !definition.isObjectLit()) {
return null;
}
return definition;
}
}
/**
* A symbol export.
*/
private class SymbolExport extends Export {
public SymbolExport(String symbolName, Node value) {
super(symbolName, value);
String qualifiedName = value.getQualifiedName();
if (qualifiedName != null) {
mappedPaths.put(qualifiedName, symbolName);
}
}
@Override
String getExportedPath() {
return symbolName;
}
}
/**
* A property export.
*/
private class PropertyExport extends Export {
private final String exportPath;
public PropertyExport(String exportPath, String symbolName, Node value) {
super(symbolName, value);
this.exportPath = checkNotNull(exportPath);
}
@Override
String getExportedPath() {
// Find the longest path that has been mapped (if any).
List pieces = Splitter.on('.').splitToList(exportPath);
for (int i = pieces.size(); i > 0; i--) {
// Find the path of the current length.
String cPath = Joiner.on(".").join(Iterables.limit(pieces, i));
// If this path is mapped, return the mapped path plus any remaining
// pieces.
if (mappedPaths.containsKey(cPath)) {
String newPath = mappedPaths.get(cPath);
if (i < pieces.size()) {
newPath += "." + Joiner.on(".").join(Iterables.skip(pieces, i));
}
return newPath + "." + symbolName;
}
}
return exportPath + "." + symbolName;
}
}
/**
* Creates an instance.
*/
ExternExportsPass(AbstractCompiler compiler) {
this.exports = new ArrayList<>();
this.compiler = compiler;
this.definitionMap = new HashMap<>();
this.externsRoot = IR.script();
this.alreadyExportedPaths = new HashSet<>();
this.mappedPaths = new HashMap<>();
initExportMethods();
}
private void initExportMethods() {
exportSymbolFunctionNames = new ArrayList<>();
exportPropertyFunctionNames = new ArrayList<>();
// From Closure:
// goog.exportSymbol = function(publicName, symbol)
// goog.exportProperty = function(object, publicName, symbol)
CodingConvention convention = compiler.getCodingConvention();
exportSymbolFunctionNames.add(convention.getExportSymbolFunction());
exportPropertyFunctionNames.add(convention.getExportPropertyFunction());
// Another common one used inside google:
exportSymbolFunctionNames.add("google_exportSymbol");
exportPropertyFunctionNames.add("google_exportProperty");
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, root, this);
// Sort by path length to ensure that the longer
// paths (which may depend on the shorter ones)
// come later.
Set sorted =
new TreeSet<>(new Comparator() {
@Override
public int compare(Export e1, Export e2) {
return e1.getExportedPath().compareTo(e2.getExportedPath());
}
});
sorted.addAll(exports);
for (Export export : sorted) {
export.generateExterns();
}
setGeneratedExternsOnCompiler();
}
private void setGeneratedExternsOnCompiler() {
CodePrinter.Builder builder = new CodePrinter.Builder(externsRoot)
.setPrettyPrint(true)
.setOutputTypes(true)
.setTypeRegistry(compiler.getTypeIRegistry());
compiler.setExternExports(Joiner.on("\n").join(
"/**",
" * @fileoverview Generated externs.",
" * @externs",
" */",
builder.build()));
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case NAME:
case GETPROP:
String name = n.getQualifiedName();
if (name == null) {
return;
}
if (parent.isAssign() || parent.isVar() || parent.isFunction()) {
definitionMap.put(name, parent);
}
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(n);
if (jsdoc != null && jsdoc.isExport()) {
handleExportDefinition(t, n);
}
// Only handle function calls. This avoids assignments
// that do not export items directly.
if (!parent.isCall()) {
return;
}
if (exportPropertyFunctionNames.contains(name)) {
handlePropertyExportCall(parent);
}
if (exportSymbolFunctionNames.contains(name)) {
handleSymbolExportCall(parent);
}
break;
default:
break;
}
}
private void handleSymbolExportCall(Node parent) {
// Ensure that we only check valid calls with the 2 arguments
// (plus the GETPROP node itself).
if (parent.getChildCount() != 3) {
return;
}
Node thisNode = parent.getFirstChild();
Node nameArg = thisNode.getNext();
Node valueArg = nameArg.getNext();
// Confirm the arguments are the expected types. If they are not,
// then we have an export that we cannot statically identify.
if (!nameArg.isString()) {
return;
}
// Add the export to the list.
this.exports.add(new SymbolExport(nameArg.getString(), valueArg));
}
private void handlePropertyExportCall(Node parent) {
// Ensure that we only check valid calls with the 3 arguments
// (plus the GETPROP node itself).
if (parent.getChildCount() != 4) {
return;
}
Node thisNode = parent.getFirstChild();
Node objectArg = thisNode.getNext();
Node nameArg = objectArg.getNext();
Node valueArg = nameArg.getNext();
// Confirm the arguments are the expected types. If they are not,
// then we have an export that we cannot statically identify.
if (!objectArg.isQualifiedName()) {
return;
}
if (!nameArg.isString()) {
return;
}
// Add the export to the list.
this.exports.add(
new PropertyExport(objectArg.getQualifiedName(),
nameArg.getString(),
valueArg));
}
private void handleExportDefinition(NodeTraversal t, Node definitionNode) {
// For now, only handle properties defined on this inside of a constructor
if (!definitionNode.isGetProp()
|| !definitionNode.getFirstChild().isThis()) {
// Not a property on THIS
return;
}
Node constructorNode = t.getEnclosingFunction();
JSDocInfo constructorJsdoc = NodeUtil.getBestJSDocInfo(constructorNode);
if (constructorJsdoc == null || !constructorJsdoc.isConstructor()) {
// Not inside a constructor
return;
}
String constructorName = NodeUtil.getName(constructorNode);
String propertyName = definitionNode.getLastChild().getString();
String prototypeName = constructorName + ".prototype";
Node propertyNameNode = NodeUtil.newQName(compiler, "this." + propertyName);
// Add the export to the list.
this.exports.add(new PropertyExport(prototypeName, propertyName, propertyNameNode));
}
}