All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
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 = Preconditions.checkNotNull(symbolName);
      this.value = Preconditions.checkNotNull(value);
    }

    /**
     * Generates the externs representation of this export and appends
     * it to the externsRoot AST.
     */
    void generateExterns() {
      appendExtern(getExportedPath(), getValue(value));
    }

    /**
     * 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 { Preconditions.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 { Preconditions.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.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) { Preconditions.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(Node qualifiedNameNode) { 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 = Preconditions.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.block(); this.externsRoot.setIsSyntheticBlock(true); 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(); } } /** * Returns the generated externs. */ public String getGeneratedExterns() { CodePrinter.Builder builder = new CodePrinter.Builder(externsRoot) .setPrettyPrint(true) .setOutputTypes(true) .setTypeRegistry(compiler.getTypeIRegistry()); return "/**" + "\n * @fileoverview Generated externs." + "\n * @externs" + "\n */" + "\n" + 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)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy