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 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 com.google.javascript.rhino.jstype.JSType;
import java.util.ArrayList;
import java.util.Collections;
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.
 */
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 ImmutableSet exportSymbolFunctionNames;

  /** A list of function names used to export properties. */
  private ImmutableSet 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 if (valueToExport.isClass()) { initializer = createExternFunctionForEs6Class(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 = createExternsParamListFromOriginalFunction(exportedFunction); Node externFunction = IR.function(IR.name(""), paramList, IR.block()); externFunction.setJSType(exportedFunction.getJSType()); return externFunction; } /** * Creates a PARAM_LIST to store in the AST we'll use to generate externs for a function with * the given type. * *

If the NODE defining the original function is available, it would be better to use * createExternsParamListFromOriginalFunction(), because that one will keep the parameter names * the same instead of generating arbitrary parameter names. * * @param exportedFunction FUNCTION Node of the original function * @return */ private Node createExternsParamListFromOriginalFunction(Node exportedFunction) { final Node originalParamList = NodeUtil.getFunctionParameters(exportedFunction); // First get all of the original positional parameter list names we can. // Place empty stings in the positions where we'll need to generate names. List originalParamNames = new ArrayList<>(); for (Node originalParam = originalParamList.getFirstChild(); originalParam != null; originalParam = originalParam.getNext()) { // We'll get an empty string for a destructuring pattern. // Also if originalParamList came from a FunctionType instead of an actual FUNCTION node, // then all of the NAME nodes in it will have empty strings, so we'll end up generating // names for all of them. originalParamNames.add(getOriginalNameForParam(originalParam)); } return createExternsParamListFromOriginalParamList(originalParamNames); } /** * Creates a PARAM_LIST to store in the AST we'll use to generate externs for a function with * the given type. * *

If the NODE defining the original function is available, it would be better to use * createExternsParamListFromOriginalFunction(), because that one will keep the parameter names * the same instead of generating arbitrary parameter names. * * @param functionType JSType read from the FUNCTION (or possibly CLASS) node */ private Node createExternsParamListFromFunctionType(JSType functionType) { // Place empty stings in the positions where we'll need to generate names. List emptyParamNames = Collections.nCopies(functionType.assertFunctionType().getParameters().size(), ""); return createExternsParamListFromOriginalParamList(emptyParamNames); } /** * Creates a PARAM_LIST to store in the AST we'll use to generate externs for a function. * * @param originalParamNames names for the parameters, possibly synthetic. */ private Node createExternsParamListFromOriginalParamList(List originalParamNames) { final Node paramList = IR.paramList(); NameGenerator nameGenerator = new DefaultNameGenerator( ImmutableSet.copyOf(originalParamNames), "", /* reservedCharacters= */ null); for (String originalParamName : originalParamNames) { String externParamName = originalParamName.isEmpty() ? nameGenerator.generateNextName() : originalParamName; paramList.addChildToBack(IR.name(externParamName)); } return paramList; } /** * @param paramNode expected to be a node in a PARAM_LIST * @return original name of the parameter, if possible, otherwise an empty string. */ private String getOriginalNameForParam(Node paramNode) { final Node nameOrPatternNode; if (paramNode.isRest()) { // get name or pattern from `...nameOrPattern` nameOrPatternNode = paramNode.getOnlyChild(); } else if (paramNode.isDefaultValue()) { // get name or pattern from `nameOrPattern = defaultValue` nameOrPatternNode = paramNode.getFirstChild(); } else { nameOrPatternNode = paramNode; } if (nameOrPatternNode.isName()) { String originalName = nameOrPatternNode.getOriginalName(); return (originalName != null) ? originalName : nameOrPatternNode.getString(); } else { checkState(nameOrPatternNode.isDestructuringPattern(), nameOrPatternNode); return ""; } } /** * Given a class 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. * *

TODO(b/123352214): It would be nice if we could put ES6 classes in the generated externs, * but we'd have to fix some things first. */ private Node createExternFunctionForEs6Class(Node exportedClass) { Node constructorMethodDefinition = NodeUtil.getEs6ClassConstructorMemberFunctionDef(exportedClass); if (constructorMethodDefinition == null) { // no constructor for the class, so just create an empty function with parameters // to match the parameters indicated in the JSType, which should have inherited parameters // from the superclass, if any. JSType classJSType = exportedClass.getJSType(); Node paramList = createExternsParamListFromFunctionType(classJSType); Node externFunction = IR.function(IR.name(""), paramList, IR.block()); externFunction.setJSType(classJSType); return externFunction; } else { // The JSType on the constructor function definition is the same as the JSType on the whole // class, so we can just pretend that the function is an ES5 constructor function. return createExternFunction(constructorMethodDefinition.getOnlyChild()); } } 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) { // We expect to see // goog.exportSymbol('exportedName', some.path); // goog.exportProperty(some.path, 'exportedName', some.path.prop); // // In either case `value` will be the last argument, which we expect to be a qualified name // If it isn't we won't include any type information in the output externs. // It would be very strange to use a literal value as the final argument, since it wouldn't // then be accessible by any non-exported name. return null; } Node definition = definitionMap.get(qualifiedName); if (definition == null) { // Couldn't find any assignment to the qualified name return null; } if (definition.isFunction() || definition.isClass() || definition.isObjectLit()) { // We can generate good type information for all of these cases. return definition; } // value was something unusual, so we won't return any node from which to get type // information. return null; } } /** * 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() { CodingConvention convention = compiler.getCodingConvention(); exportSymbolFunctionNames = ImmutableSet.of( convention.getExportSymbolFunction(), // goog.exportSymbol(name, value) "google_exportSymbol"); // used within Google exportPropertyFunctionNames = ImmutableSet.of( convention.getExportPropertyFunction(), // goog.exportProperty(owner, name, value) "google_exportProperty"); // used within Google } @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) { lookForQnameDefinition(n); lookForAtExportOnThisDotProperty(t, n); lookForSymbolExportCall(n); lookForPropertyExportCall(n); } private void lookForQnameDefinition(Node n) { // TODO(b/123725559): There are lots of cases where this could fail to find the right // definition or be fooled by there being multiple definitions. if (n.isClass()) { if (NodeUtil.isClassDeclaration(n)) { // class Foo {...} definitionMap.put(n.getFirstChild().getString(), n); } } else if (n.isFunction()) { if (NodeUtil.isFunctionDeclaration(n)) { // function foo() {...} definitionMap.put(n.getFirstChild().getString(), n); } } else if (n.isAssign()) { // TODO(b/123718645): Add support for destructuring assignments Node lhs = n.getFirstChild(); if (lhs.isQualifiedName()) { // qualified.name = value; definitionMap.put(lhs.getQualifiedName(), n.getLastChild()); } } else if (n.isName()) { // TODO(b/123718645): Add support for destructuring declarations Node parent = checkNotNull(n.getParent(), n); if (NodeUtil.isNameDeclaration(parent)) { Node value = n.getFirstChild(); if (value != null) { // const foo = value; definitionMap.put(n.getString(), value); } } } else if (n.isMemberFunctionDef()) { // Try to find a fully qualified name for the method String lvalueName = NodeUtil.getBestLValueName(n); if (lvalueName != null) { // Store the function as the value definitionMap.put(lvalueName, n.getOnlyChild()); } } // TODO(b/123725422): Getters and setters? } private void lookForSymbolExportCall(Node n) { if (!isCallToOneOf(n, exportSymbolFunctionNames)) { return; // not a call to goog.exportSymbol() } // TODO(b/123725716): We should report errors for malformed calls instead of just ignoring them. // Ensure that we only check valid calls with the 2 arguments // (plus the GETPROP node itself). if (!n.hasXChildren(3)) { return; } Node thisNode = n.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 lookForPropertyExportCall(Node n) { if (!isCallToOneOf(n, this.exportPropertyFunctionNames)) { return; // not a call to goog.exportProperty() } // TODO(b/123725716): We should report errors for malformed calls instead of just ignoring them. // Ensure that we only check valid calls with the 3 arguments // (plus the GETPROP node itself). if (!n.hasXChildren(4)) { return; } Node thisNode = n.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 boolean isCallToOneOf(Node n, ImmutableSet functionQnames) { if (!n.isCall()) { return false; } else { Node callee = n.getFirstChild(); return callee.isQualifiedName() && functionQnames.contains(callee.getQualifiedName()); } } private void lookForAtExportOnThisDotProperty(NodeTraversal t, Node thisDotPropName) { if (!thisDotPropName.isGetProp() || !thisDotPropName.getFirstChild().isThis()) { return; // not this.propName } JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(thisDotPropName); if (jsdoc == null || !jsdoc.isExport()) { return; // no @export on this.propName } Node constructorNode = t.getEnclosingFunction(); if (!NodeUtil.isConstructor(constructorNode)) { return; // @export on this.propName only works within a constructor } Node classNode = NodeUtil.isEs6Constructor(constructorNode) ? NodeUtil.getEnclosingClass(constructorNode) : constructorNode; String className = NodeUtil.getName(classNode); String propertyName = thisDotPropName.getLastChild().getString(); String prototypeName = className + ".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