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

com.google.javascript.jscomp.modules.ClosureModuleProcessor Maven / Gradle / Ivy

/*
 * Copyright 2019 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.modules;

import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.modules.ModuleMapCreator.DOES_NOT_HAVE_EXPORT_WITH_DETAILS;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.modules.ClosureRequireProcessor.Require;
import com.google.javascript.jscomp.modules.ModuleMapCreator.ModuleProcessor;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Processor for goog.module
 *
 * 

The namespace of a goog.module contains all named exports, e.g. {@code exports.x = 0}, and any * 'default export's that assign directly to the `exports` object, e.g. {@code exports = class {}}). * *

The bound names include any names imported through a goog.require(Type)/forwardDeclare. */ final class ClosureModuleProcessor implements ModuleProcessor { private static class UnresolvedGoogModule extends UnresolvedModule { private final ModuleMetadata metadata; private final String srcFileName; @Nullable private final ModulePath path; private final ImmutableMap namespace; private final ImmutableMap requiresByLocalName; private final AbstractCompiler compiler; private Module resolved = null; UnresolvedGoogModule( ModuleMetadata metadata, String srcFileName, ModulePath path, ImmutableMap namespace, ImmutableMap requires, AbstractCompiler compiler) { this.metadata = metadata; this.srcFileName = srcFileName; this.path = path; this.namespace = namespace; this.requiresByLocalName = requires; this.compiler = compiler; } @Nullable @Override public ResolveExportResult resolveExport( ModuleRequestResolver moduleRequestResolver, String exportName) { if (namespace.containsKey(exportName)) { return ResolveExportResult.of(namespace.get(exportName)); } return ResolveExportResult.NOT_FOUND; } @Nullable @Override public ResolveExportResult resolveExport( ModuleRequestResolver moduleRequestResolver, @Nullable String moduleSpecifier, String exportName, Set resolveSet, Set exportStarSet) { return resolveExport(moduleRequestResolver, exportName); } @Override public Module resolve( ModuleRequestResolver moduleRequestResolver, @Nullable String moduleSpecifier) { if (resolved == null) { // Every import creates a locally bound name. Map boundNames = new LinkedHashMap<>(getAllResolvedImports(moduleRequestResolver)); resolved = Module.builder() .path(path) .metadata(metadata) .namespace(namespace) .boundNames(ImmutableMap.copyOf(boundNames)) .localNameToLocalExport(ImmutableMap.of()) .closureNamespace(Iterables.getOnlyElement(metadata.googNamespaces())) .unresolvedModule(this) .build(); } return resolved; } /** A map from import bound name to binding. */ Map getAllResolvedImports(ModuleRequestResolver moduleRequestResolver) { Map imports = new HashMap<>(); for (String name : requiresByLocalName.keySet()) { ResolveExportResult b = resolveImport(moduleRequestResolver, name); if (b.resolved()) { imports.put(name, b.getBinding()); } } return imports; } ResolveExportResult resolveImport(ModuleRequestResolver moduleRequestResolver, String name) { Require require = requiresByLocalName.get(name); Import importRecord = require.importRecord(); UnresolvedModule requested = moduleRequestResolver.resolve(importRecord); if (requested == null) { return ResolveExportResult.ERROR; } else if (importRecord.importName().equals(Export.NAMESPACE)) { // Return a binding based on the other module's metadata. return ResolveExportResult.of( Binding.from( requested.metadata(), importRecord.nameNode(), importRecord.moduleRequest(), require.createdBy())); } else { ResolveExportResult result = requested.resolveExport( moduleRequestResolver, importRecord.moduleRequest(), importRecord.importName(), new HashSet<>(), new HashSet<>()); if (!result.found() && !result.hadError()) { reportInvalidDestructuringRequire(requested, importRecord); return ResolveExportResult.ERROR; } Node forSourceInfo = importRecord.nameNode() == null ? importRecord.importNode() : importRecord.nameNode(); return result.copy(forSourceInfo, require.createdBy()); } } @Override ModuleMetadata metadata() { return metadata; } @Override public ImmutableSet getExportedNames(ModuleRequestResolver moduleRequestResolver) { // Unsupported until such time as it becomes useful throw new UnsupportedOperationException(); } @Override public ImmutableSet getExportedNames( ModuleRequestResolver moduleRequestResolver, Set visited) { throw new UnsupportedOperationException(); } @Override void reset() { resolved = null; } /** Reports an error given an invalid destructuring require. */ private void reportInvalidDestructuringRequire( UnresolvedModule requested, Import importRecord) { String additionalInfo = ""; if (requested instanceof UnresolvedGoogModule) { // Detect some edge cases and given more helpful error messages. Map exports = ((UnresolvedGoogModule) requested).namespace; if (exports.containsKey(Export.NAMESPACE)) { // Can't use destructuring imports on a goog.module with a default export like // exports = class { // (even if there is an assignment like `exports.Bar = 0;` later) additionalInfo = Strings.lenientFormat( // Use Strings.lenientFormat for GWT/J2CL compatability "\n" + "The goog.module \"%s\" cannot be destructured as it contains a default" + " export, not named exports. See %s.", importRecord.moduleRequest(), "https://github.com/google/closure-library/wiki/goog.module%3A-an-ES6-module-like-alternative-to-goog.provide#destructuring-imports"); if (mayBeAccidentalDefaultExport(importRecord.importName(), exports)) { // Give the user a more detailed error message, since this is a tricky edge case. additionalInfo += Strings.lenientFormat( "\n" + "Either use a non-destructuring require or rewrite the goog.module" + " \"%s\" to support destructuring requires. For example, consider" + " replacing\n" + " exports = {%s: [, ...]};\n" + "with individual named export assignments like\n" + " exports.%s = ;\n", importRecord.moduleRequest(), importRecord.importName(), importRecord.importName()); } } } compiler.report( JSError.make( srcFileName, importRecord.importNode().getLineno(), importRecord.importNode().getCharno(), DOES_NOT_HAVE_EXPORT_WITH_DETAILS, importRecord.importName(), additionalInfo)); } } /** * Returns whether the user appears to have confused a default export of an object literal with * the named export object literal shorthand. * *

Basically, `exports = {foo: 0};`, does /not/ create a 'named export' of 'foo' because 0 is * not a name. So users cannot destructuring-require `const {foo} = goog.require('the.module');`. * However, if instead of `0` the user exported a name like `bar`, the user could use a * destructuring require. */ private static boolean mayBeAccidentalDefaultExport( String importName, Map exports) { Node defaultExport = exports.get(Export.NAMESPACE).originatingExport().exportNode(); checkState( defaultExport.matchesName("exports") && defaultExport.getParent().isAssign(), defaultExport); Node exportedValue = defaultExport.getNext(); if (!exportedValue.isObjectLit()) { return false; } // Look for `importName` in the exported object literal. for (Node key : exportedValue.children()) { if (key.isStringKey() && key.getString().equals(importName)) { return true; } } return false; } private final AbstractCompiler compiler; public ClosureModuleProcessor(AbstractCompiler compiler) { this.compiler = compiler; } @Override public UnresolvedModule process(ModuleMetadata metadata, ModulePath path, Node script) { Preconditions.checkArgument( script.isScript() || script.isCall(), "Unexpected module root %s", script); Preconditions.checkArgument( script.isCall() || path != null, "Non goog.loadModules must have a path"); ModuleProcessingCallback moduleProcessingCallback = new ModuleProcessingCallback(metadata); NodeTraversal.traverse(compiler, script, moduleProcessingCallback); return new UnresolvedGoogModule( metadata, script.getSourceFileName(), path, ImmutableMap.copyOf(moduleProcessingCallback.namespace), ImmutableMap.copyOf(moduleProcessingCallback.requiresByLocalName), compiler); } /** Traverses a subtree rooted at a module, gathering all exports and requires */ private static class ModuleProcessingCallback extends AbstractPreOrderCallback { private final ModuleMetadata metadata; /** The Closure namespace 'a.b.c' from the `goog.module('a.b.c');` statement */ private final String closureNamespace; // Note: the following two maps are mutable because in some cases, we need to check if a key has // already been added before trying to add a second. /** All named exports and explicit assignments of the `exports` object */ private final Map namespace; /** All required/forwardDeclared local names */ private final Map requiresByLocalName; /** Whether we've come across an "exports = ..." assignment */ private boolean seenExportsAssignment; ModuleProcessingCallback(ModuleMetadata metadata) { this.metadata = metadata; this.namespace = new LinkedHashMap<>(); this.requiresByLocalName = new LinkedHashMap<>(); this.closureNamespace = Iterables.getOnlyElement(metadata.googNamespaces()); this.seenExportsAssignment = false; } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case MODULE_BODY: case SCRIPT: case CALL: // Traverse into goog.loadModule calls. case BLOCK: return true; case FUNCTION: // Only traverse into functions that are the argument of a goog.loadModule call, which is // the module root. Avoid traversing function declarations like: // goog.module('a.b'); function (exports) { exports.x = 0; } return parent.isCall() && parent == metadata.rootNode(); case EXPR_RESULT: Node expr = n.getFirstChild(); if (expr.isAssign()) { maybeInitializeExports(expr); } else if (expr.isGetProp()) { maybeInitializeExportsStub(expr); } return false; case CONST: case VAR: case LET: // Note that `let` is valid only for `goog.forwardDeclare`. maybeInitializeRequire(n); return false; default: return false; } } /** If an assignment is to 'exports', adds it to the list of Exports */ private void maybeInitializeExports(Node assignment) { Node lhs = assignment.getFirstChild(); Node rhs = assignment.getSecondChild(); if (lhs.isName() && lhs.getString().equals("exports")) { // This may be a 'named exports' or may be a default export. // It is a 'named export' if and only if it is assigned an object literal w/ string keys, // whose values are all names. if (isNamedExportsLiteral(rhs)) { initializeNamedExportsLiteral(rhs); } else { seenExportsAssignment = true; } markExportsAssignmentInNamespace(lhs); } else if (lhs.isGetProp() && lhs.getFirstChild().isName() && lhs.getFirstChild().getString().equals("exports")) { String exportedId = lhs.getSecondChild().getString(); addPropertyExport(exportedId, lhs); } } /** Adds stub export declarations `exports.Foo;` to the list of Exports */ private void maybeInitializeExportsStub(Node qname) { Node owner = qname.getFirstChild(); if (owner.isName() && owner.getString().equals("exports")) { Node prop = qname.getSecondChild(); String exportedId = prop.getString(); addPropertyExport(exportedId, qname); } } /** * Adds an explicit namespace export. * *

Note that all goog.modules create an 'exports' object, but this object is only added to * the Module namespace if there is an explicit' exports = ...' assignment */ private void markExportsAssignmentInNamespace(Node exportsNode) { namespace.put( Export.NAMESPACE, Binding.from( Export.builder() .exportName(Export.NAMESPACE) .exportNode(exportsNode) .moduleMetadata(metadata) .closureNamespace(closureNamespace) .modulePath(metadata.path()) .build(), exportsNode)); } private void initializeNamedExportsLiteral(Node objectLit) { for (Node key : objectLit.children()) { addPropertyExport(key.getString(), key); } } /** Adds a named export to the list of Exports */ private void addPropertyExport(String exportedId, Node propNode) { if (seenExportsAssignment) { // We've seen an assignment "exports = ...", so this is not a named export. return; } else if (namespace.containsKey(exportedId)) { // Ignore duplicate exports - this is an error but checked elsewhere. return; } namespace.put( exportedId, Binding.from( Export.builder() .exportName(exportedId) .exportNode(propNode) .moduleMetadata(metadata) .closureNamespace(closureNamespace) .modulePath(metadata.path()) .build(), propNode)); } /** Adds a goog.require(Type) or forwardDeclare to the list of {@code requiresByLocalName} */ private void maybeInitializeRequire(Node nameDeclaration) { for (Require require : ClosureRequireProcessor.getAllRequires(nameDeclaration)) { requiresByLocalName.putIfAbsent(require.localName(), require); } } } /** * Whether this is an assignment to 'exports' that creates named exports. * *

    *
  • exports = {a, b}; // named exports *
  • exports = 0; // namespace export *
  • exports = {a: 0, b}; // namespace export *
*/ private static boolean isNamedExportsLiteral(Node objLit) { if (!objLit.isObjectLit() || !objLit.hasChildren()) { return false; } for (Node key : objLit.children()) { if (!key.isStringKey() || key.isQuotedString()) { return false; } if (!key.getFirstChild().isName()) { return false; } } return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy