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

com.google.javascript.jscomp.ExternExportsPass Maven / Gradle / Ivy

Go to download

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.

There is a newer version: v20240317
Show newest version
/*
 * 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.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 org.jspecify.nullness.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) { ImmutableList 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 (valueToExport.isClass() || (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 */ 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 JSDocInfo.builder().build(true); } private JSDocInfo buildNamespaceJSDoc() { JSDocInfo.Builder builder = JSDocInfo.builder(); 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 @Nullable 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() && (!lhs.hasChildren() || !lhs.getFirstChild().isThis())) { // qualified.name = value; // but not // this.prop = 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.isStringLit()) { 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.isStringLit()) { 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.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