
com.google.javascript.jscomp.ExternExportsPass Maven / Gradle / Ivy
/*
* 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 static java.util.Comparator.comparing;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
/**
* 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 {
private static final Joiner Q_NAME_JOINER = Joiner.on('.');
private static final Splitter Q_NAME_SPLITTER = Splitter.on('.');
/** 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);
if (skipPathPrefix) {
continue;
}
boolean exportedValueDefinesNewType = false;
if (valueToExport != null) {
JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(valueToExport);
if (jsdoc != null && jsdoc.containsTypeDefinition()) {
exportedValueDefinesNewType = true;
}
}
// 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.
final Node initializer;
JSDocInfo jsdoc = null;
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);
}
}
private void appendPathDefinition(
String path, Node initializer, JSDocInfo jsdoc) {
final Node pathDefinition;
if (path.contains(".")) {
Node qualifiedPath = NodeUtil.newQName(compiler, path);
if (initializer.isEmpty()) {
pathDefinition = NodeUtil.newExpr(qualifiedPath);
} else {
pathDefinition = NodeUtil.newExpr(IR.assign(qualifiedPath, initializer));
}
} else {
if (initializer.isEmpty()) {
pathDefinition = IR.var(IR.name(path));
} else {
pathDefinition = NodeUtil.newVarNode(path, 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.
for (Node param = paramList.getFirstChild();
param != null && param.isName();
param = param.getNext()) {
String originalName = param.getOriginalName();
if (originalName != null) {
param.setString(originalName);
}
}
Node externFunction = IR.function(IR.name(""), paramList, IR.block());
if (exportedFunction.getJSType() != null) {
externFunction.setJSType(exportedFunction.getJSType());
// 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.setJSType(exportedObjectLit.getJSType());
// 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;
}
final 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).
for (String currentPath : Lists.reverse(computePathPrefixes(exportPath))) {
checkState(currentPath.length() > 0);
// If this path is mapped, return the mapped path plus any remaining pieces.
@Nullable String mappedPath = mappedPaths.get(currentPath);
if (mappedPath == null) {
continue;
}
// Append the remaining path segments, including a leading separator.
mappedPath += exportPath.substring(currentPath.length());
return Q_NAME_JOINER.join(mappedPath, symbolName);
}
return Q_NAME_JOINER.join(exportPath, symbolName);
}
}
/**
* 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 static ImmutableList computePathPrefixes(String path) {
List pieces = Q_NAME_SPLITTER.splitToList(path);
ImmutableList.Builder pathPrefixes = ImmutableList.builder();
String partial = pieces.get(0); // There will always be at least 1.
pathPrefixes.add(partial);
for (int i = 1; i < pieces.size(); i++) {
partial = Q_NAME_JOINER.join(partial, pieces.get(i));
pathPrefixes.add(partial);
}
return pathPrefixes.build();
}
/**
* 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.traverse(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<>(comparing(Export::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.getTypeRegistry());
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;
}
switch (parent.getToken()) {
case ASSIGN:
case FUNCTION:
case VAR:
definitionMap.put(name, parent);
break;
default:
break;
}
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));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy