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

com.google.javascript.jscomp.Es6RewriteModules 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 2014 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 com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_DESTRUCTURING_FORWARD_DECLARE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_FORWARD_DECLARE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_CALL_SCOPE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_TYPE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.MISSING_MODULE_OR_PROVIDE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.MODULE_USES_GOOG_MODULE_GET;
import static com.google.javascript.jscomp.ClosureRewriteModule.ILLEGAL_MODULE_RENAMING_CONFLICT;
import static com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature.MODULES;

import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.javascript.jscomp.ModuleRenaming.GlobalizedModuleName;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader;
import com.google.javascript.jscomp.modules.Binding;
import com.google.javascript.jscomp.modules.Module;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Rewrites a ES6 module into a form that can be safely concatenated. Note that we treat a file as
 * an ES6 module if it has at least one import or export statement.
 *
 * 

Also rewrites any goog.{require,requireType,forwardDeclare,goog.module.get} calls that are * either in an ES module or of an ES module using goog.declareModuleId. */ public final class Es6RewriteModules implements HotSwapCompilerPass, NodeTraversal.Callback { static final DiagnosticType LHS_OF_GOOG_REQUIRE_MUST_BE_CONST = DiagnosticType.error( "JSC_LHS_OF_GOOG_REQUIRE_MUST_BE_CONST", "The left side of a goog.require() or goog.requireType() " + "must use ''const'' (not ''let'' or ''var'')"); static final DiagnosticType REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST = DiagnosticType.error( "JSC_REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST", "goog.requireType alias for ES6 module should be const."); static final DiagnosticType FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST = DiagnosticType.error( "JSC_FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST", "goog.forwardDeclare alias for ES6 module should be const."); static final DiagnosticType SHOULD_IMPORT_ES6_MODULE = DiagnosticType.warning( "JSC_SHOULD_IMPORT_ES6_MODULE", "ES6 modules should import other ES6 modules rather than goog.require them."); private final AbstractCompiler compiler; private final AstFactory astFactory; private final JSType unknownType; private static final Splitter DOT_SPLITTER = Splitter.on("."); @Nullable private final PreprocessorSymbolTable preprocessorSymbolTable; /** * Local variable names that were goog.require'd to qualified name we need to line. * *

We need to inline all required names since there are certain well-known Closure symbols * (like goog.asserts) that later stages of the compiler check for and cannot handle aliases. * *

We use this to rewrite something like: * *

   *   import {x} from '';
   *   const {assert} = goog.require('goog.asserts');
   *   assert(x);
   * 
* * To: * *
   *   import {x} from '';
   *   goog.asserts.assert(x);
   * 
* * Because if we used an alias like below the assertion would not be recognized: * *
   *   import {x} from '';
   *   const {assert} = goog.asserts;
   *   assert(x);
   * 
*/ // TODO(johnplaisted): This is actually incorrect if the require'd thing is mutated. But we need // it so that things like goog.asserts work. Mutated closure symbols are a lot rarer than needing // to use asserts and the like. Until there's a better solution to finding aliases of well known // symbols we have to inline anything that is require'd. private Map namesToInlineByAlias; private Set typedefs; private final ModuleMetadataMap moduleMetadataMap; private final ModuleMap moduleMap; private final TypedScope globalTypedScope; /** * Creates a new Es6RewriteModules instance which can be used to rewrite ES6 modules to a * concatenable form. */ Es6RewriteModules( AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap, ModuleMap moduleMap, @Nullable PreprocessorSymbolTable preprocessorSymbolTable, @Nullable TypedScope globalTypedScope) { checkNotNull(moduleMetadataMap); this.compiler = compiler; this.astFactory = compiler.createAstFactory(); this.moduleMetadataMap = moduleMetadataMap; this.moduleMap = moduleMap; this.preprocessorSymbolTable = preprocessorSymbolTable; this.globalTypedScope = globalTypedScope; this.unknownType = compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE); } /** * Return whether or not the given script node represents an ES6 module file. */ public static boolean isEs6ModuleRoot(Node scriptNode) { checkArgument(scriptNode.isScript(), scriptNode); if (scriptNode.getBooleanProp(Node.GOOG_MODULE)) { return false; } return scriptNode.hasChildren() && scriptNode.getFirstChild().isModuleBody(); } @Override public void process(Node externs, Node root) { checkArgument(externs.isRoot(), externs); checkArgument(root.isRoot(), root); NodeTraversal.traverseRoots(compiler, this, externs, root); compiler.setFeatureSet(compiler.getFeatureSet().without(MODULES)); // This pass may add getters properties on module objects. GatherGetterAndSetterProperties.update(compiler, externs, root); } @Override public void hotSwapScript(Node scriptNode, Node originalRoot) { new RewriteRequiresForEs6Modules().rewrite(scriptNode); NodeTraversal.traverse(compiler, scriptNode, this); } private void clearPerFileState() { this.typedefs = new HashSet<>(); this.namesToInlineByAlias = new HashMap<>(); } /** * Checks for goog.require, goog.requireType, goog.module.get and goog.forwardDeclare calls that * are meant to import ES6 modules and rewrites them. */ private class RewriteRequiresForEs6Modules extends AbstractPostOrderCallback { private boolean transpiled = false; // An (s, old, new) entry indicates that occurrences of `old` in scope `s` should be rewritten // as `new`. This is used to rewrite namespaces that appear in calls to goog.requireType and // goog.forwardDeclare. private Table renameTable; void rewrite(Node scriptNode) { transpiled = false; renameTable = HashBasedTable.create(); NodeTraversal.traverse(compiler, scriptNode, this); if (transpiled) { scriptNode.putBooleanProp(Node.TRANSPILED, true); } if (!renameTable.isEmpty()) { NodeTraversal.traverse( compiler, scriptNode, new Es6RenameReferences(renameTable, /* typesOnly= */ true)); } } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isCall()) { return; } boolean isRequire = n.getFirstChild().matchesQualifiedName("goog.require"); boolean isRequireType = n.getFirstChild().matchesQualifiedName("goog.requireType"); boolean isGet = n.getFirstChild().matchesQualifiedName("goog.module.get"); boolean isForwardDeclare = n.getFirstChild().matchesQualifiedName("goog.forwardDeclare"); if (!isRequire && !isRequireType && !isGet && !isForwardDeclare) { return; } if (!n.hasTwoChildren() || !n.getLastChild().isString()) { if (isRequire) { t.report(n, INVALID_REQUIRE_NAMESPACE); } else if (isRequireType) { t.report(n, INVALID_REQUIRE_TYPE_NAMESPACE); } else if (isGet) { t.report(n, INVALID_GET_NAMESPACE); } else { t.report(n, INVALID_FORWARD_DECLARE_NAMESPACE); } return; } String name = n.getLastChild().getString(); ModuleMetadata moduleMetadata = moduleMetadataMap.getModulesByGoogNamespace().get(name); if (moduleMetadata == null || !moduleMetadata.isEs6Module()) { return; } // TODO(johnplaisted): Once we have an alternative to forwardDeclare / requireType that // doesn't require Closure Library warn about those too. // TODO(johnplaisted): Once we have import() support warn about goog.module.get. if (isRequire) { ModuleMetadata currentModuleMetadata = moduleMetadataMap.getModulesByPath().get(t.getInput().getPath().toString()); if (currentModuleMetadata != null && currentModuleMetadata.isEs6Module()) { t.report(n, SHOULD_IMPORT_ES6_MODULE); } } if (isGet && t.inGlobalHoistScope()) { t.report(n, INVALID_GET_CALL_SCOPE); return; } Node statementNode = NodeUtil.getEnclosingStatement(n); boolean importHasAlias = NodeUtil.isNameDeclaration(statementNode); if (importHasAlias) { if (statementNode.getFirstChild().isDestructuringLhs()) { if (isForwardDeclare) { // const {a, c:b} = goog.forwardDeclare('an.es6.namespace'); t.report(n, INVALID_DESTRUCTURING_FORWARD_DECLARE); return; } if (isRequireType) { if (!statementNode.isConst()) { t.report(statementNode, REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST); return; } // const {a, c:b} = goog.requireType('an.es6.namespace'); for (Node child : statementNode.getFirstFirstChild().children()) { checkState(child.isStringKey()); checkState(child.getFirstChild().isName()); renameTable.put( t.getScopeRoot(), child.getFirstChild().getString(), ModuleRenaming.getGlobalName(moduleMetadata, name) .getprop(child.getString()) .join()); } } else { // Work around a bug in the type checker where destructuring can create // too many layers of aliases and confuse the type checker. b/112061124. // const {a, c:b} = goog.require('an.es6.namespace'); // const a = module$es6.a; // const b = module$es6.c; for (Node child : statementNode.getFirstFirstChild().children()) { checkState(child.isStringKey()); checkState(child.getFirstChild().isName()); GlobalizedModuleName globalName = getGlobalNameAndType( moduleMetadata, name, /* isFromMissingModuleOrProvide= */ false) .getprop(child.getString()); Node constNode = astFactory.createSingleConstNameDeclaration( child.getFirstChild().getString(), globalName.toQname(astFactory)); constNode.useSourceInfoFromForTree(child); statementNode.getParent().addChildBefore(constNode, statementNode); } } statementNode.detach(); t.reportCodeChange(); } else { if (isForwardDeclare || isRequireType) { if (!statementNode.isConst()) { DiagnosticType diagnostic = isForwardDeclare ? FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST : REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST; t.report(statementNode, diagnostic); return; } // const namespace = goog.forwardDeclare('an.es6.namespace'); // const namespace = goog.requireType('an.es6.namespace'); renameTable.put( t.getScopeRoot(), statementNode.getFirstChild().getString(), ModuleRenaming.getGlobalName(moduleMetadata, name).join()); statementNode.detach(); t.reportCodeChange(); } else { // const module = goog.require('an.es6.namespace'); // const module = module$es6; n.replaceWith( astFactory .createName( ModuleRenaming.getGlobalName(moduleMetadata, name).getRoot(), n.getJSType()) .useSourceInfoFromForTree(n)); t.reportCodeChange(); } } } else { if (isForwardDeclare || isRequireType) { // goog.forwardDeclare('an.es6.namespace') // goog.requireType('an.es6.namespace') renameTable.put( t.getScopeRoot(), name, ModuleRenaming.getGlobalName(moduleMetadata, name).join()); statementNode.detach(); } else { // goog.require('an.es6.namespace') if (statementNode.isExprResult() && statementNode.getFirstChild() == n) { statementNode.detach(); } else { n.replaceWith( astFactory .createName( ModuleRenaming.getGlobalName(moduleMetadata, name).getRoot(), n.getJSType()) .useSourceInfoFromForTree(n)); } } t.reportCodeChange(); } transpiled = true; } } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (n.isScript()) { // Trigger creation of the global scope before inserting any synthetic code. nodeTraversal.getScope(); new RewriteRequiresForEs6Modules().rewrite(n); if (isEs6ModuleRoot(n)) { clearPerFileState(); n.putBooleanProp(Node.TRANSPILED, true); } else { return false; } } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isImport()) { maybeWarnExternModule(t, n, parent); visitImport(t, n, parent); } else if (n.isExport()) { maybeWarnExternModule(t, n, parent); visitExport(t, n, parent); } else if (n.isScript()) { visitScript(t, n); } else if (n.isCall()) { // TODO(johnplaisted): Consolidate on declareModuleId. if (n.getFirstChild().matchesQualifiedName("goog.declareModuleId")) { n.getParent().detach(); } } else if (n.isImportMeta()) { // We're choosing to not "support" import.meta because currently all the outputs from the // compiler are scripts and support for import.meta (only works in modules) would be // meaningless t.report(n, Es6ToEs3Util.CANNOT_CONVERT, "import.meta"); } } private void maybeWarnExternModule(NodeTraversal t, Node n, Node parent) { checkState(parent.isModuleBody()); if (parent.isFromExterns() && !NodeUtil.isFromTypeSummary(parent.getParent())) { t.report(n, Es6ToEs3Util.CANNOT_CONVERT_YET, "ES6 modules in externs"); } } private void visitImport(NodeTraversal t, Node importDecl, Node parent) { checkArgument(parent.isModuleBody(), parent); String importName = importDecl.getLastChild().getString(); boolean isNamespaceImport = importName.startsWith("goog:"); if (isNamespaceImport) { // Allow importing Closure namespace objects (e.g. from goog.provide or goog.module) as // import ... from 'goog:my.ns.Object'. String namespace = importName.substring("goog:".length()); ModuleMetadata m = moduleMetadataMap.getModulesByGoogNamespace().get(namespace); if (m == null) { t.report(importDecl, MISSING_MODULE_OR_PROVIDE, namespace); } else { checkState(m.isEs6Module() || m.isGoogModule() || m.isGoogProvide()); } } else { ModuleLoader.ModulePath modulePath = t.getInput() .getPath() .resolveJsModule( importName, importDecl.getSourceFileName(), importDecl.getLineno(), importDecl.getCharno()); if (modulePath == null) { // The module loader issues an error // Fall back to assuming the module is a file path modulePath = t.getInput().getPath().resolveModuleAsPath(importName); } maybeAddImportedFileReferenceToSymbolTable(importDecl.getLastChild(), modulePath.toString()); // TODO(johnplaisted): Use ModuleMetadata to ensure the path required is CommonJs or ES6 and // if not give a better error. } for (Node child : importDecl.children()) { if (child.isImportSpecs()) { for (Node grandChild : child.children()) { maybeAddAliasToSymbolTable(grandChild.getFirstChild(), t.getSourceName()); checkState(grandChild.hasTwoChildren()); } } else if (child.isImportStar()) { // import * as ns from "mod" maybeAddAliasToSymbolTable(child, t.getSourceName()); } } parent.removeChild(importDecl); t.reportCodeChange(); } private void visitExport(NodeTraversal t, Node export, Node parent) { checkArgument(parent.isModuleBody(), parent); if (export.getBooleanProp(Node.EXPORT_DEFAULT)) { // export default // If the thing being exported is a class or function that has a name, // extract it from the export statement, so that it can be referenced // from within the module. // // export default class X {} -> class X {}; ... moduleName.default = X; // export default function X() {} -> function X() {}; ... moduleName.default = X; // // Otherwise, create a local variable for it and export that. // // export default 'someExpression' // -> // var $jscompDefaultExport = 'someExpression'; // ... // moduleName.default = $jscompDefaultExport; Node child = export.getFirstChild(); String name = null; if (child.isFunction() || child.isClass()) { name = NodeUtil.getName(child); } if (name != null) { Node decl = child.detach(); parent.replaceChild(export, decl); } else { Node var = astFactory.createSingleVarNameDeclaration( ModuleRenaming.DEFAULT_EXPORT_VAR_PREFIX, export.removeFirstChild()); var.setJSDocInfo(child.getJSDocInfo()); child.setJSDocInfo(null); var.useSourceInfoIfMissingFromForTree(export); parent.replaceChild(export, var); } t.reportCodeChange(); } else if (export.getBooleanProp(Node.EXPORT_ALL_FROM) || export.hasTwoChildren() || export.getFirstChild().getToken() == Token.EXPORT_SPECS) { // export * from 'moduleIdentifier'; // export {x, y as z} from 'moduleIdentifier'; // export {Foo}; parent.removeChild(export); t.reportCodeChange(); } else { visitExportDeclaration(t, export, parent); } } private void visitExportNameDeclaration(Node declaration) { // export var Foo; // export let {a, b:[c,d]} = {}; List lhsNodes = NodeUtil.findLhsNodesInNode(declaration); for (Node lhs : lhsNodes) { checkState(lhs.isName()); String name = lhs.getString(); if (declaration.getJSDocInfo() != null && declaration.getJSDocInfo().hasTypedefType()) { typedefs.add(name); } } } private void visitExportDeclaration(NodeTraversal t, Node export, Node parent) { // export var Foo; // export function Foo() {} // etc. Node declaration = export.getFirstChild(); if (NodeUtil.isNameDeclaration(declaration)) { visitExportNameDeclaration(declaration); } parent.replaceChild(export, declaration.detach()); t.reportCodeChange(); } private void inlineModuleToGlobalScope(Node moduleNode) { checkState(moduleNode.isModuleBody()); Node scriptNode = moduleNode.getParent(); moduleNode.detach(); scriptNode.addChildrenToFront(moduleNode.removeChildren()); } private void visitScript(NodeTraversal t, Node script) { final Node moduleBody = script.getFirstChild(); // TypedScopeCreator sets the module object type on the MODULE_BODY during type checking. final JSType moduleObjectType = moduleBody.getJSType(); inlineModuleToGlobalScope(moduleBody); ClosureRewriteModule.checkAndSetStrictModeDirective(t, script); Module thisModule = moduleMap.getModule(t.getInput().getPath()); QualifiedName qualifiedName = ModuleRenaming.getGlobalName(thisModule.metadata(), /* googNamespace= */ null); checkState(qualifiedName.isSimple(), "Unexpected qualified name %s", qualifiedName); String moduleName = qualifiedName.getRoot(); Node moduleVar = createExportsObject(moduleName, t, script, moduleObjectType); // Rename vars to not conflict in global scope. NodeTraversal.traverse(compiler, script, new RenameGlobalVars(thisModule)); // Rename the exports object to something we can reference later. moduleVar.getFirstChild().setString(moduleName); moduleVar.makeNonIndexableRecursive(); declareGlobalVariable(moduleVar.getFirstChild(), t); // rewriteRequires is here (rather than being part of the main visit() method, because we only // want to rewrite the requires if this is an ES6 module. Note that we also want to do this // AFTER renaming all module scoped vars in the event that something that is goog.require'd is // a global, unqualified name (e.g. if "goog.provide('foo')" exists, we don't want to rewrite // "const foo = goog.require('foo')" to "const foo = foo". If we rewrite our module scoped names // first then we'll rewrite to "const foo$module$fudge = goog.require('foo')", then to // "const foo$module$fudge = foo". rewriteRequires(script); t.reportCodeChange(); } private Node createExportsObject( String moduleName, NodeTraversal t, Node script, JSType moduleObjectType) { Node moduleObject = astFactory.createObjectLit(moduleObjectType); // Going to get renamed by RenameGlobalVars, so the name we choose here doesn't matter as long // as it doesn't collide with an existing variable. (We can't use `moduleName` since then // RenameGlobalVars will rename all references to `moduleName` incorrectly). We'll fix the name // in visitScript after the global renaming to ensure it has a name that is deterministic from // the path. // // So after this method we'll have: // var $jscomp$tmp$exports$module$name = {}; // module$name.exportName = localName; // // After RenameGlobalVars: // var $jscomp$tmp$exports$module$nameglobalized = {}; // module$name.exportName = localName$globalized; // // After visitScript: // var module$name = {}; // module$name.exportName = localName$globalized; Node moduleVar = astFactory.createSingleVarNameDeclaration("$jscomp$tmp$exports$module$name", moduleObject); moduleVar.getFirstChild().putBooleanProp(Node.MODULE_EXPORT, true); // TODO(b/144593112): Stop adding JSDoc when this pass moves to always be after typechecking. JSDocInfoBuilder infoBuilder = new JSDocInfoBuilder(false); infoBuilder.recordConstancy(); moduleVar.setJSDocInfo(infoBuilder.build()); moduleVar.getFirstChild().setDeclaredConstantVar(true); script.addChildToBack(moduleVar.useSourceInfoIfMissingFromForTree(script)); Module thisModule = moduleMap.getModule(t.getInput().getPath()); for (Map.Entry entry : thisModule.namespace().entrySet()) { String exportedName = entry.getKey(); Binding binding = entry.getValue(); Node nodeForSourceInfo = binding.sourceNode(); boolean mutated = binding.isMutated(); QualifiedName boundVariableQualifiedName = ModuleRenaming.getGlobalName(binding); checkState( boundVariableQualifiedName.isSimple(), "unexpected qualified name: %s", boundVariableQualifiedName); String boundVariableName = boundVariableQualifiedName.getRoot(); Node getProp = astFactory.createGetProp( astFactory.createName(moduleName, moduleObjectType), exportedName); getProp.putBooleanProp(Node.MODULE_EXPORT, true); if (typedefs.contains(exportedName)) { // /** @typedef {foo} */ // moduleName.foo; JSDocInfoBuilder builder = new JSDocInfoBuilder(true); JSTypeExpression typeExpr = new JSTypeExpression( astFactory.createString(exportedName).srcref(nodeForSourceInfo), script.getSourceFileName()); builder.recordTypedef(typeExpr); JSDocInfo info = builder.build(); getProp.setJSDocInfo(info); Node exprResult = astFactory.exprResult(getProp).useSourceInfoIfMissingFromForTree(nodeForSourceInfo); script.addChildToBack(exprResult); } else if (mutated) { final Node globalExportName = astFactory.createName(boundVariableName, getProp.getJSType()); addGetterExport(script, nodeForSourceInfo, moduleObject, exportedName, globalExportName); NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.GETTER, compiler); } else { // Avoid the extra complexity of using getters when the property isn't mutated. // exports.foo = foo; Node assign = astFactory.createAssign( getProp, astFactory.createName(boundVariableName, getProp.getJSType())); // TODO(b/144593112): Stop adding JSDoc when this pass moves to always be after typechecking JSDocInfoBuilder builder = new JSDocInfoBuilder(true); builder.recordConstancy(); JSDocInfo info = builder.build(); assign.setJSDocInfo(info); script.addChildToBack( astFactory.exprResult(assign).useSourceInfoIfMissingFromForTree(nodeForSourceInfo)); } } return moduleVar; } private void addGetterExport( Node script, Node forSourceInfo, Node objLit, String exportedName, Node value) { Node getter = astFactory.createGetterDef(exportedName, value); getter.putBooleanProp(Node.MODULE_EXPORT, true); objLit.addChildToBack(getter); if (!astFactory.isAddingTypes()) { // TODO(b/143904518): Remove this code when this pass is permanently moved after type checking // Type checker doesn't infer getters so mark the return as unknown. // { /** @return {?} */ get foo() { return foo; } } JSDocInfoBuilder builder = new JSDocInfoBuilder(true); builder.recordReturnType( new JSTypeExpression( new Node(Token.QMARK).srcref(forSourceInfo), script.getSourceFileName())); getter.setJSDocInfo(builder.build()); } else { // For a property typed as number, synthesize a type `function(): number`. getter.setJSType(compiler.getTypeRegistry().createFunctionType(value.getJSType())); } getter.useSourceInfoIfMissingFromForTree(forSourceInfo); compiler.reportChangeToEnclosingScope(getter.getFirstChild().getLastChild()); compiler.reportChangeToEnclosingScope(getter); } private void rewriteRequires(Node script) { NodeTraversal.traversePostOrder( compiler, script, (NodeTraversal t, Node n, Node parent) -> { if (n.isCall()) { Node fn = n.getFirstChild(); if (fn.matchesQualifiedName("goog.require") || fn.matchesQualifiedName("goog.requireType")) { // TODO(tjgq): This will rewrite both type references and code references. For // goog.requireType, the latter are potentially broken because the symbols aren't // guaranteed to be available at run time. A separate pass needs to be added to // detect these incorrect uses of goog.requireType. visitRequireOrGet(t, n, parent, /* isRequire= */ true); } else if (fn.matchesQualifiedName("goog.module.get")) { visitGoogModuleGet(t, n, parent); } } }); NodeTraversal.traversePostOrder( compiler, script, (NodeTraversal t, Node n, Node parent) -> { JSDocInfo info = n.getJSDocInfo(); if (info != null) { for (Node typeNode : info.getTypeNodes()) { inlineAliasedTypes(t, typeNode); } } if (n.isName() && namesToInlineByAlias.containsKey(n.getString())) { Var v = t.getScope().getVar(n.getString()); if (v == null || v.getNameNode() != n) { GlobalizedModuleName replacementName = namesToInlineByAlias.get(n.getString()); Node replacement = replacementName.toQname(astFactory).useSourceInfoFromForTree(n); n.replaceWith(replacement); } } }); } private void inlineAliasedTypes(NodeTraversal t, Node typeNode) { if (typeNode.isString()) { String name = typeNode.getString(); List split = DOT_SPLITTER.limit(2).splitToList(name); // We've already removed the alias. if (t.getScope().getVar(split.get(0)) == null) { GlobalizedModuleName replacement = namesToInlineByAlias.get(split.get(0)); if (replacement != null) { String rest = ""; if (split.size() == 2) { rest = "." + split.get(1); } typeNode.setOriginalName(name); typeNode.setString(replacement.aliasName().join() + rest); t.reportCodeChange(); } } } for (Node child : typeNode.children()) { inlineAliasedTypes(t, child); } } private void visitGoogModuleGet(NodeTraversal t, Node getCall, Node parent) { if (!getCall.hasTwoChildren() || !getCall.getLastChild().isString()) { t.report(getCall, INVALID_GET_NAMESPACE); return; } // Module has already been turned into a script at this point. if (t.inGlobalHoistScope()) { t.report(getCall, MODULE_USES_GOOG_MODULE_GET); return; } visitRequireOrGet(t, getCall, parent, /* isRequire= */ false); } /** * Gets some made-up metadata for the given Closure namespace. * *

This is used when the namespace is not part of the input so that this pass can be fault * tolerant and still rewrite to something. Some tools don't care about rewriting correctly and * just want the type information of this module (e.g. clutz). */ private ModuleMetadata getFallbackMetadataForNamespace(String namespace) { // Assume a provide'd file to be consistent with goog.module rewriting. ModuleMetadata.Builder builder = ModuleMetadata.builder() .moduleType(ModuleMetadataMap.ModuleType.GOOG_PROVIDE) .usesClosure(true) .isTestOnly(false); builder.googNamespacesBuilder().add(namespace); return builder.build(); } private void visitRequireOrGet( NodeTraversal t, Node requireCall, Node parent, boolean isRequire) { if (!requireCall.hasTwoChildren() || !requireCall.getLastChild().isString()) { t.report(requireCall, INVALID_REQUIRE_NAMESPACE); return; } // Module has already been turned into a script at this point. if (isRequire && !t.getScope().isGlobal()) { t.report(requireCall, INVALID_CLOSURE_CALL_SCOPE_ERROR); return; } String namespace = requireCall.getLastChild().getString(); boolean isStoredInDeclaration = NodeUtil.isDeclaration(parent.getParent()); if (isStoredInDeclaration && !parent.getParent().isConst()) { compiler.report(JSError.make(parent.getParent(), LHS_OF_GOOG_REQUIRE_MUST_BE_CONST)); } ModuleMetadata m = moduleMetadataMap.getModulesByGoogNamespace().get(namespace); boolean isFromFallbackMetadata = m == null; if (isFromFallbackMetadata) { t.report(requireCall, MISSING_MODULE_OR_PROVIDE, namespace); m = getFallbackMetadataForNamespace(namespace); } if (isStoredInDeclaration) { if (isRequire) { Node toDetach; if (parent.isDestructuringLhs()) { checkState(parent.getFirstChild().isObjectPattern()); toDetach = parent.getParent(); for (Node child : parent.getFirstChild().children()) { checkState(child.isStringKey() && child.getFirstChild().isName(), child); GlobalizedModuleName rep = getGlobalNameAndType(m, namespace, isFromFallbackMetadata) .getprop(child.getString()); namesToInlineByAlias.put(child.getFirstChild().getString(), rep); } } else if (parent.isName()) { GlobalizedModuleName alias = getGlobalNameAndType(m, namespace, isFromFallbackMetadata); namesToInlineByAlias.put(parent.getString(), alias); toDetach = parent.getParent(); } else { checkState(parent.isExprResult()); toDetach = parent; } toDetach.detach(); } else { GlobalizedModuleName name = getGlobalNameAndType(m, namespace, isFromFallbackMetadata); Node replacement = name.toQname(astFactory).srcrefTree(requireCall); parent.replaceChild(requireCall, replacement); } } else { checkState(requireCall.getParent().isExprResult()); requireCall.getParent().detach(); } } /** * Looks up information about the globalized name and type of a given module * * @param metadata Required. The metadata for the module or provide being imported. * @param googNamespace Optional. * @param isFromMissingModuleOrProvide Whether the metadata is synthesized fallback metadata */ private GlobalizedModuleName getGlobalNameAndType( ModuleMetadata metadata, @Nullable String googNamespace, boolean isFromMissingModuleOrProvide) { if (isFromMissingModuleOrProvide) { // The missing namespace presumably does not have a corresponding type defined in the scope. // Use the unknownType instead of asking ModuleRenaming to look up the type. QualifiedName globalName = ModuleRenaming.getGlobalName(metadata, googNamespace); return GlobalizedModuleName.create(globalName, unknownType); } return GlobalizedModuleName.create(metadata, googNamespace, globalTypedScope); } /** * Traverses a node tree and * *

    *
  1. Appends a suffix to all global variable names defined in this module. *
  2. Changes references to imported values to access the exported variable. *
*/ private class RenameGlobalVars extends AbstractPostOrderCallback { private final Module thisModule; RenameGlobalVars(Module thisModule) { this.thisModule = thisModule; } @Override public void visit(NodeTraversal t, Node n, Node parent) { JSDocInfo info = n.getJSDocInfo(); if (info != null) { for (Node typeNode : info.getTypeNodes()) { fixTypeNode(t, typeNode); } } if (n.isName()) { String name = n.getString(); Var var = t.getScope().getVar(name); if (var != null && var.isGlobal()) { // Avoid polluting the global namespace. String newName = ModuleRenaming.getGlobalNameOfEsModuleLocalVariable(thisModule.metadata(), name) .join(); n.setString(newName); n.setOriginalName(name); t.reportCodeChange(n); if (NodeUtil.isDeclarationLValue(n)) { declareGlobalVariable(n, t); } } else if (var == null && thisModule.boundNames().containsKey(name)) { // Imports have been detached, so they won't show up in scope. Thus if we have a variable // not in scope that shares the name of an import it is the import. maybeAddAliasToSymbolTable(n, t.getSourceName()); Binding binding = thisModule.boundNames().get(name); Node replacement = replace(t.getScope(), n, binding); // `n.x()` may become `foo()` if (replacement.isName() && parent.isCall() && parent.getFirstChild() == n && parent.getBooleanProp(Node.FREE_CALL)) { parent.putBooleanProp(Node.FREE_CALL, true); } if (NodeUtil.isDeclarationLValue(n)) { declareGlobalVariable(n, t); } t.reportCodeChange(); } } } /** * Replaces the reference to a given binding. See {@link * ModuleRenaming#getGlobalNameForJsDoc(ModuleMap, Binding, List)} for a JS Doc version. * *

For example: * *

     *   // bar
     *   export let baz = {qux: 0};
     * 
* *
     *   // foo
     *   import * as bar from 'bar';
     *   export {bar};
     * 
* *
     *   import * as foo from 'foo';
     *   use(foo.bar.baz.qux);
     * 
* *

Should call this method with the binding and node for {@code foo}. In this example any of * these properties could also be modules. This method will replace as much as the GETPROP as it * can with module exported variables. Meaning in the above example this would return something * like "baz$$module$bar.qux", whereas if this method were called for just "foo.bar" it would * return "module$bar", as it refers to a module object itself. * * @param n the node to replace * @param binding the binding nameNode is a reference to */ private Node replace(Scope scope, Node n, Binding binding) { checkState(n.isName()); while (binding.isModuleNamespace() && binding.metadata().isEs6Module() && n.getParent().isGetProp()) { String propertyName = n.getParent().getSecondChild().getString(); Module m = moduleMap.getModule(binding.metadata().path()); if (m.namespace().containsKey(propertyName)) { binding = m.namespace().get(propertyName); n = n.getParent(); } else { // This means someone referenced an invalid export on a module object. This should be an // error, so just rewrite and let the type checker complain later. It isn't a super clear // error, but we're working on type checking modules soon. break; } } QualifiedName globalName = ModuleRenaming.getGlobalName(binding); final Node newNode; if (!globalName.isSimple()) { newNode = astFactory.createQName(scope, globalName.join()); } else { // Because this pass does not update the global scope with injected names, t.getScope() // will not contain a declaration for this global name. Fortunately, we already have the // JSType on the existing node to pass to AstFactory. newNode = astFactory.createName(globalName.getRoot(), n.getJSType()); } // For kythe: the new node only represents the last name it replaced, not all the names. // e.g. if we rewrite `a.b.c.d.e` to `x.d.e`, then `x` should map to `c`, not `a.b.c`. Node forSourceInfo = n.isGetProp() ? n.getSecondChild() : n; n.replaceWith(newNode); newNode.srcrefTree(forSourceInfo); newNode.setOriginalName(forSourceInfo.getString()); return newNode; } /** * Replace type name references. Change short names to fully qualified names with namespace * prefixes. Eg: {Foo} becomes {module$test.Foo}. */ private void fixTypeNode(NodeTraversal t, Node typeNode) { if (typeNode.isString()) { Module thisModule = moduleMap.getModule(t.getInput().getPath()); String name = typeNode.getString(); List splitted = DOT_SPLITTER.splitToList(name); String baseName = splitted.get(0); String rest = ""; if (splitted.size() > 1) { rest = name.substring(baseName.length()); } Var var = t.getScope().getVar(baseName); if (var != null && var.isGlobal()) { maybeSetNewName( t, typeNode, name, ModuleRenaming.getGlobalNameOfEsModuleLocalVariable(thisModule.metadata(), baseName) .join() + rest); } else if (var == null && thisModule.boundNames().containsKey(baseName)) { // Imports have been detached, so they won't show up in scope. Thus if we have a variable // not in scope that shares the name of an import it is the import. Binding binding = thisModule.boundNames().get(baseName); String globalName = ModuleRenaming.getGlobalNameForJsDoc( moduleMap, binding, splitted.subList(1, splitted.size())); maybeSetNewName(t, typeNode, name, globalName); if (preprocessorSymbolTable != null) { // Jsdoc type node is a single STRING node that spans the whole type. For example // STRING node "bar.Foo". ES6 import rewrite replaces only "module" // part of the type: "bar.Foo" => "module$full$path$bar$Foo". We have to record // "bar" as alias. Node onlyBaseName = Node.newString(baseName).useSourceInfoFrom(typeNode); onlyBaseName.setLength(baseName.length()); maybeAddAliasToSymbolTable(onlyBaseName, t.getSourceName()); } } typeNode.setOriginalName(name); } for (Node child = typeNode.getFirstChild(); child != null; child = child.getNext()) { fixTypeNode(t, child); } } private void maybeSetNewName(NodeTraversal t, Node node, String name, String newName) { if (!name.equals(newName)) { node.setString(newName); node.setOriginalName(name); t.reportCodeChange(); } } } /** * Add alias nodes to the symbol table as they going to be removed by rewriter. Example aliases: * *

   *   import * as foo from './foo';
   *   import {doBar} from './bar';
   *
   *   console.log(doBar);
   * 
* * @param n Alias node. In the example above alias nodes are foo, doBar, and doBar. * @param module Name of the module currently being processed. */ private void maybeAddAliasToSymbolTable(Node n, String module) { if (preprocessorSymbolTable == null) { return; } n.putBooleanProp(Node.MODULE_ALIAS, true); // Alias can be used in js types. Types have node type STRING and not NAME so we have to // use their name as string. String nodeName = n.isString() || n.isImportStar() ? n.getString() : preprocessorSymbolTable.getQualifiedName(n); // We need to include module as part of the name because aliases are local to current module. // Aliases with the same name from different module should be completely different entities. String name = "alias_" + module + "_" + nodeName; preprocessorSymbolTable.addReference(n, name); } /** * Add reference to a file that current module imports. Example: * *
   * import * as qux from '../some/file.js';
   * 
* *

Will add a reference to file.js on the string node `'../some/file.js'`. * * @param importStringNode String node from the import statement that references imported file. In * the example above it is the '../some/file.js' STRING node. * @param importedFilePath Absolute path to the imported file. In the example above it can be * myproject/folder/some/file.js */ private void maybeAddImportedFileReferenceToSymbolTable( Node importStringNode, String importedFilePath) { if (preprocessorSymbolTable == null) { return; } // If this if the first import that mentions importedFilePath then we need to create a SCRIPT // node for the imported file. if (preprocessorSymbolTable.getSlot(importedFilePath) == null) { Node scriptNode = compiler.getScriptNode(importedFilePath); if (scriptNode != null) { preprocessorSymbolTable.addReference(scriptNode, importedFilePath); } } preprocessorSymbolTable.addReference(importStringNode, importedFilePath); } private void declareGlobalVariable(Node n, NodeTraversal t) { checkState(n.isName()); if (!astFactory.isAddingTypes()) { return; } checkNotNull(this.globalTypedScope); String name = n.getString(); if (this.globalTypedScope.hasOwnSlot(name)) { t.report(t.getCurrentScript(), ILLEGAL_MODULE_RENAMING_CONFLICT, name); } else { JSType type = checkNotNull(n.getJSType()); this.globalTypedScope.declare(name, n, type, t.getInput(), false); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy