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

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

/*
 * 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.common.collect.ImmutableList.toImmutableList;
import static com.google.javascript.jscomp.AstFactory.type;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_FORWARD_DECLARE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_DYNAMIC;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_TYPE_NAMESPACE;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 * Process aliases in goog.modules.
 *
 * 
 * goog.module('foo.Bar');
 * var Baz = goog.require('foo.Baz');
 * class Bar extends Baz {}
 * exports = Bar;
 * 
* * becomes * *
 * class module$contents$foo$Bar_Bar extends module$exports$foo$Baz {}
 * var module$exports$foo$Bar = module$contents$foo$Bar_Bar;
 * 
* * and * *
 * goog.loadModule(function(exports) {
 *   goog.module('foo.Bar');
 *   var Baz = goog.require('foo.Baz');
 *   class Bar extends Baz {}
 *   exports = Bar;
 *   return exports;
 * })
 * 
* * becomes * *
 * class module$contents$foo$Bar_Bar extends module$exports$foo$Baz {}
 * var module$exports$foo$Bar = module$contents$foo$Bar_Bar;
 * 
*/ final class ClosureRewriteModule implements CompilerPass { static final DiagnosticType INVALID_MODULE_ID_ARG = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_MODULE_ID_ARG", "goog.module parameter must be a string literal"); static final DiagnosticType INVALID_PROVIDE_NAMESPACE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_PROVIDE_NAMESPACE", "goog.provide parameter must be a string literal."); static final DiagnosticType INVALID_PROVIDE_CALL = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_PROVIDE_CALL", "goog.provide can not be called in goog.module."); static final DiagnosticType INVALID_GET_ALIAS = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_GET_ALIAS", "goog.module.get should not be aliased."); static final DiagnosticType INVALID_EXPORT_COMPUTED_PROPERTY = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_EXPORT_COMPUTED_PROPERTY", "Computed properties are not yet supported in goog.module exports."); static final DiagnosticType USELESS_USE_STRICT_DIRECTIVE = DiagnosticType.disabled( "JSC_USELESS_USE_STRICT_DIRECTIVE", "'use strict' is unnecessary in goog.module files."); static final DiagnosticType IMPORT_INLINING_SHADOWS_VAR = DiagnosticType.error( "JSC_IMPORT_INLINING_SHADOWS_VAR", "Inlining of reference to import \"{1}\" shadows var \"{0}\"."); static final DiagnosticType ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT = DiagnosticType.error( "JSC_ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT", "Destructuring import only allowed for importing module with named exports.\n" + "See https://github.com/google/closure-compiler/wiki/goog.module-style"); static final DiagnosticType ILLEGAL_DESTRUCTURING_NOT_EXPORTED = DiagnosticType.error( "JSC_ILLEGAL_DESTRUCTURING_NOT_EXPORTED", "Destructuring import reference to name \"{0}\" was not exported in module {1}"); static final DiagnosticType LOAD_MODULE_FN_MISSING_RETURN = DiagnosticType.error( "JSC_LOAD_MODULE_FN_MISSING_RETURN", "goog.loadModule function should end with 'return exports;'"); static final DiagnosticType ILLEGAL_MODULE_RENAMING_CONFLICT = DiagnosticType.error( "JSC_ILLEGAL_MODULE_RENAMING_CONFLICT", "Internal compiler error: rewritten module global name {0} is already in use."); static final String MODULE_EXPORTS_PREFIX = "module$exports$"; private static final String MODULE_CONTENTS_PREFIX = "module$contents$"; // Prebuilt Nodes to speed up Node.matchesQualifiedName() calls private static final Node GOOG_FORWARDDECLARE = IR.getprop(IR.name("goog"), "forwardDeclare"); private static final Node GOOG_LOADMODULE = IR.getprop(IR.name("goog"), "loadModule"); private static final Node GOOG_MODULE = IR.getprop(IR.name("goog"), "module"); private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE = IR.getprop(GOOG_MODULE, "declareLegacyNamespace"); private static final Node GOOG_MODULE_GET = IR.getprop(GOOG_MODULE.cloneTree(), "get"); private static final Node GOOG_PROVIDE = IR.getprop(IR.name("goog"), "provide"); private static final Node GOOG_REQUIRE = IR.getprop(IR.name("goog"), "require"); private static final Node GOOG_REQUIRETYPE = IR.getprop(IR.name("goog"), "requireType"); private static final Node GOOG_REQUIREDYNAMIC = IR.getprop(IR.name("goog"), "requireDynamic"); private static final String GOOG_REQUIREDYNAMIC_NAME = "goog.requireDynamic"; private static final String IMPORT_HANDLER_NAME = "importHandler_"; private final AbstractCompiler compiler; private final AstFactory astFactory; private final PreprocessorSymbolTable preprocessorSymbolTable; private final boolean preserveSugar; private final LinkedHashMap syntheticExterns = new LinkedHashMap<>(); private @Nullable Scope globalScope = null; // non-final because it must be set after process() is called /** Indicates where new nodes should be added in relation to some other node. */ private static enum AddAt { BEFORE, AFTER } private static enum ScopeType { EXEC_CONTEXT, BLOCK } /** * Describes the context of an "unrecognized require" scenario so that it will be possible to * categorize and report it as either a "not provided yet" or "not provided at all" error at the * end. */ private static final class UnrecognizedRequire { // A goog.require() call, or a goog.module.get() call. final Node requireNode; final String namespaceId; UnrecognizedRequire(Node requireNode, String namespaceId, boolean mustBeOrdered) { this.requireNode = requireNode; this.namespaceId = namespaceId; } } private static final class ExportDefinition { // Null if the export is a default export (exports = expr) @Nullable String exportName; // Null if the export is of a @typedef @Nullable Node rhs; // Null if the export is of anything other than a name @Nullable Var nameDecl; @Override public String toString() { return MoreObjects.toStringHelper(this) .add("exportName", exportName) .add("rhs", rhs) .add("nameDecl", nameDecl) .omitNullValues() .toString(); } private static final ImmutableSet INLINABLE_NAME_PARENTS = Sets.immutableEnumSet(Token.VAR, Token.CONST, Token.LET, Token.FUNCTION, Token.CLASS); static ExportDefinition newDefaultExport(NodeTraversal t, Node rhs) { return newNamedExport(t, null, rhs); } static ExportDefinition newNamedExport(NodeTraversal t, @Nullable String name, Node rhs) { ExportDefinition newExport = new ExportDefinition(); newExport.exportName = name; newExport.rhs = rhs; if (rhs != null && (rhs.isName() || rhs.isStringKey())) { newExport.nameDecl = t.getScope().getVar(rhs.getString()); } return newExport; } String getExportPostfix() { if (exportName == null) { return ""; } return "." + exportName; } boolean hasInlinableName(Set exportedNames) { if (nameDecl == null || exportedNames.contains(nameDecl) || !INLINABLE_NAME_PARENTS.contains(nameDecl.getParentNode().getToken()) || NodeUtil.isFunctionDeclaration(nameDecl.getParentNode())) { return false; } Node initialValue = nameDecl.getInitialValue(); if (initialValue == null || !initialValue.isCall()) { return true; } Node method = initialValue.getFirstChild(); if (!method.isGetProp()) { return true; } Node maybeGoog = method.getFirstChild(); if (!maybeGoog.isName() || !maybeGoog.getString().equals("goog")) { return true; } String name = method.getString(); return !name.equals("require") && !name.equals("forwardDeclare") && !name.equals("getMsg"); } @Nullable String getLocalName() { return nameDecl != null ? nameDecl.getName() : null; } } private static class AliasName { final String newName; final @Nullable String namespaceId; // non-null only if this is an alias of a module itself AliasName(String newName, @Nullable String namespaceId) { this.newName = newName; this.namespaceId = namespaceId; } } private static final class ScriptDescription { boolean isModule; boolean declareLegacyNamespace; String namespaceId; // "a.b.c" String contentsPrefix; // "module$contents$a$b$c_ final Set topLevelNames = new LinkedHashSet<>(); // For prefixed content renaming. final Deque childScripts = new ArrayDeque<>(); final Map namesToInlineByAlias = new LinkedHashMap<>(); // For alias inlining. /** Transient state. */ boolean willCreateExportsObject; boolean hasCreatedExportObject; ExportDefinition defaultExport; @Nullable String defaultExportLocalName; final Set namedExports = new LinkedHashSet<>(); final Map exportsToInline = new LinkedHashMap<>(); // The root of the module. The MODULE_BODY node that contains the module contents. // For recognizing top level names. Node rootNode; public void addChildScript(ScriptDescription childScript) { childScripts.addLast(childScript); } public ScriptDescription removeFirstChildScript() { return childScripts.removeFirst(); } // "module$exports$a$b$c" for non-legacy modules @Nullable String getBinaryNamespace() { if (!this.isModule || this.declareLegacyNamespace) { return null; } return getBinaryModuleNamespace(namespaceId); } @Nullable String getExportedNamespace() { if (this.declareLegacyNamespace) { return this.namespaceId; } return this.getBinaryNamespace(); } } static String getBinaryModuleNamespace(String namespaceId) { return MODULE_EXPORTS_PREFIX + namespaceId.replace('.', '$'); } private class ScriptPreprocessor extends NodeTraversal.AbstractPreOrderCallback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case ROOT: case MODULE_BODY: return true; case SCRIPT: if (NodeUtil.isGoogModuleFile(n)) { checkAndSetStrictModeDirective(t, n); } return true; case NAME: preprocessExportDeclaration(t, n); return true; default: // Don't traverse into non-module scripts. return !parent.isScript(); } } } private class ScriptRecorder implements NodeTraversal.Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case MODULE_BODY: recordModuleBody(n); break; case CALL: Node method = n.getFirstChild(); if (!method.isGetProp()) { break; } if (method.matchesQualifiedName(GOOG_MODULE)) { recordGoogModule(t, n); } else if (method.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) { recordGoogDeclareLegacyNamespace(); } else if (method.matchesQualifiedName(GOOG_PROVIDE)) { recordGoogProvide(t, n); } else if (method.matchesQualifiedName(GOOG_REQUIRE)) { recordGoogRequire(t, n, /* mustBeOrdered= */ true); } else if (method.matchesQualifiedName(GOOG_REQUIRETYPE)) { recordGoogRequireType(t, n); } else if (method.matchesQualifiedName(GOOG_REQUIREDYNAMIC)) { recordGoogRequireDynamic(t, n); } else if (method.matchesQualifiedName(GOOG_FORWARDDECLARE) && !parent.isExprResult()) { recordGoogForwardDeclare(t, n); } else if (method.matchesQualifiedName(GOOG_MODULE_GET)) { recordGoogModuleGet(t, n); } break; case CLASS: case FUNCTION: if (isTopLevel(t, n, ScopeType.BLOCK)) { recordTopLevelClassOrFunctionName(n); } break; case CONST: case LET: case VAR: if (isTopLevel(t, n, n.isVar() ? ScopeType.EXEC_CONTEXT : ScopeType.BLOCK)) { recordTopLevelVarNames(n); } break; case GETPROP: if (isExportPropertyAssignment(t, n)) { recordExportsPropertyAssignment(t, n); } break; case NAME: maybeRecordExportDeclaration(t, n); break; default: break; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isModuleBody()) { popScript(); } } } private class ScriptUpdater implements NodeTraversal.Callback { final Deque scriptDescriptions; ScriptUpdater(Deque scriptDescriptions) { this.scriptDescriptions = scriptDescriptions; } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case SCRIPT: ScriptDescription currentDescription = scriptDescriptions.removeFirst(); checkState(currentDescription.rootNode == n); if (n.isFromExterns() && !NodeUtil.isFromTypeSummary(n)) { return false; } checkState(scriptStack.isEmpty()); pushScript(currentDescription); // Capture the scope before doing any rewriting to the scope. t.getScope(); // Capture the global scope for later reference. if (globalScope == null) { globalScope = t.getScope().getGlobalScope(); } break; case MODULE_BODY: if (parent.getBooleanProp(Node.GOOG_MODULE)) { updateModuleBodyEarly(n); } else { return false; } break; case CALL: Node method = n.getFirstChild(); if (!method.isGetProp()) { break; } if (method.matchesQualifiedName(GOOG_MODULE)) { updateGoogModule(t, n); } else if (method.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) { updateGoogDeclareLegacyNamespace(n); } else if (method.matchesQualifiedName(GOOG_REQUIRE) || method.matchesQualifiedName(GOOG_REQUIRETYPE)) { updateGoogRequire(t, n); } else if (method.matchesQualifiedName(GOOG_FORWARDDECLARE) && !parent.isExprResult()) { updateGoogForwardDeclare(t, n); } break; case GETPROP: if (isExportPropertyAssignment(t, n)) { updateExportsPropertyAssignment(n, t); } break; default: break; } if (n.getJSDocInfo() != null) { rewriteJsdoc(n.getJSDocInfo(), t.getScope()); } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case MODULE_BODY: updateModuleBody(n); break; case NAME: maybeUpdateTopLevelName(t, n); maybeUpdateExportDeclaration(t, n); t.getScope(); // Creating the scope here has load-bearing side-effects. maybeUpdateExportNameRef(t, n); break; case SCRIPT: checkState(currentScript.rootNode == n); popScript(); break; default: break; } } } /** * Rewrites JsDoc type references to match AST changes resulting from imported alias inlining, * module content renaming of top level constructor functions and classes, and module renaming * from fully qualified legacy namespace to its binary name. */ private void rewriteJsdoc(JSDocInfo info, Scope scope) { ReplaceJsDocRefs replacer = new ReplaceJsDocRefs(scope); for (Node typeNode : info.getTypeNodes()) { NodeUtil.visitPreOrder(typeNode, replacer); } } /** * Rewrites JsDoc type references to match AST changes resulting from imported alias inlining, * module content renaming of top level constructor functions and classes, and module renaming * from fully qualified legacy namespace to its binary name. */ private final class ReplaceJsDocRefs implements NodeUtil.Visitor { private final Scope scope; ReplaceJsDocRefs(Scope scope) { this.scope = scope; } @Override public void visit(Node typeRefNode) { if (!typeRefNode.isStringLit()) { return; } // A type name that might be simple like "Foo" or qualified like "foo.Bar". final String typeName = typeRefNode.getString(); int dot = typeName.indexOf('.'); String rootOfType = dot == -1 ? typeName : typeName.substring(0, dot); Var rootVar = this.scope.getVar(rootOfType); if (rootVar != null && !rootVar.getScope().getClosestHoistScope().isGlobal() && !rootVar.getScope().isModuleScope()) { // this is a variable inside a local scope, not a module local or imported alias. return; } // Rewrite the type node if any of the following hold, in priority order: // - the root of the type name is in our set of aliases to inline // - the root of the type name is a name defined in the module scope // - a prefix of the type name matches a Closure namespace // AND the following is false: // - the root of the type name is defined in an inner scope // If the name is an alias for an imported namespace rewrite from // "{Foo}" to // "{module$exports$bar$Foo}" or // "{bar.Foo}" if (currentScript.namesToInlineByAlias.containsKey(rootOfType)) { if (preprocessorSymbolTable != null) { // Jsdoc type node is a single STRING node that spans the whole type. For example // STRING node "bar.Foo". When rewriting modules potentially replace only "module" // part of the type: "bar.Foo" => "module$exports$bar$Foo". So we need to remember // that "bar" as alias. To do that we clone type node and make "bar" node from it. Node moduleOnlyNode = typeRefNode.cloneNode(); safeSetString(moduleOnlyNode, rootOfType); moduleOnlyNode.setLength(rootOfType.length()); maybeAddAliasToSymbolTable(moduleOnlyNode, currentScript.namespaceId); } String aliasedNamespace = currentScript.namesToInlineByAlias.get(rootOfType).newName; String remainder = dot == -1 ? "" : typeName.substring(dot); safeSetString(typeRefNode, aliasedNamespace + remainder); } else if (currentScript.isModule && currentScript.topLevelNames.contains(rootOfType)) { // If this is a module and the type name is the name of a top level var/function/class // defined in this script then that var will have been previously renamed from Foo to // module$contents$Foo_Foo. Update the JsDoc reference to match. safeSetString(typeRefNode, currentScript.contentsPrefix + typeName); } else if (currentScript.isModule && rootOfType.equals("exports")) { // rewrite a type reference to the implicit "exports" variable // e.g. /** @type {exports.Foo} */ -> /** @type {module$exports$my$mod.Foo} */ String namespace = currentScript.getBinaryNamespace(); String remainder = dot == -1 ? "" : typeName.substring(dot); safeSetString(typeRefNode, namespace + remainder); } else { rewriteIfClosureNamespaceRef(typeName, typeRefNode); } } /** * Tries to match the longest possible prefix of this type to a Closure namespace * *

If the longest prefix match is a legacy module or provide, this is a no-op. If the longest * prefix match is a non-legacy module, this method rewrites the type node. */ private void rewriteIfClosureNamespaceRef(String typeName, Node typeRefNode) { // Tries to rename progressively shorter type prefixes like "foo.Bar.Baz", then "foo.Bar", // then "foo". String prefixTypeName = typeName; String suffix = ""; while (true) { String binaryNamespaceIfModule = rewriteState.getBinaryNamespace(prefixTypeName); if (legacyScriptNamespacesAndPrefixes.contains(prefixTypeName) && binaryNamespaceIfModule == null) { // This thing is definitely coming from a legacy script and so the fully qualified // type name will always resolve as is. return; } // If the typeName is a reference to a fully qualified legacy namespace like // "foo.bar.Baz" of something that is actually a module then rewrite the JsDoc reference // to "module$exports$Bar". // Note: we may want to ban this pattern in the future. See b/133501660. if (binaryNamespaceIfModule != null) { safeSetString(typeRefNode, binaryNamespaceIfModule + suffix); return; } if (prefixTypeName.contains(".")) { prefixTypeName = prefixTypeName.substring(0, prefixTypeName.lastIndexOf('.')); suffix = typeName.substring(prefixTypeName.length()); } else { break; } } } } // Per script state needed for rewriting. private final Deque scriptStack = new ArrayDeque<>(); private @Nullable ScriptDescription currentScript = null; // Global state tracking an association between the dotted names of goog.module()s and whether // the goog.module declares itself as a legacy namespace. // Allows for detecting duplicate goog.module()s and for rewriting fully qualified // JsDoc type references to goog.module() types in legacy scripts. private static final class GlobalRewriteState { private final Map scriptDescriptionsByGoogModuleNamespace = new LinkedHashMap<>(); private final Multimap namespaceIdsByScriptNode = HashMultimap.create(); private final Set providedNamespaces = new LinkedHashSet<>(); boolean containsModule(String namespaceId) { return scriptDescriptionsByGoogModuleNamespace.containsKey(namespaceId); } boolean isLegacyModule(String namespaceId) { checkArgument(containsModule(namespaceId)); return scriptDescriptionsByGoogModuleNamespace.get(namespaceId).declareLegacyNamespace; } @Nullable String getBinaryNamespace(String namespaceId) { ScriptDescription script = scriptDescriptionsByGoogModuleNamespace.get(namespaceId); return script == null ? null : script.getBinaryNamespace(); } /** Returns the type of a goog.require of the given goog.module, or null if not a module. */ @Nullable JSType getGoogModuleNamespaceType(String namespaceId) { ScriptDescription googModule = scriptDescriptionsByGoogModuleNamespace.get(namespaceId); return googModule == null ? null : googModule.rootNode.getJSType(); } private @Nullable String getExportedNamespaceOrScript(String namespaceId) { if (providedNamespaces.contains(namespaceId)) { return namespaceId; } ScriptDescription script = scriptDescriptionsByGoogModuleNamespace.get(namespaceId); return script == null ? null : script.getExportedNamespace(); } } private final GlobalRewriteState rewriteState = new GlobalRewriteState(); // All prefix namespaces from goog.provides and legacy goog.modules. private final Set legacyScriptNamespacesAndPrefixes = new LinkedHashSet<>(); private final List unrecognizedRequires = new ArrayList<>(); private final ArrayList googModuleGetCalls = new ArrayList<>(); private final ArrayList googRequireDynamicCalls = new ArrayList<>(); private final @Nullable TypedScope globalTypedScope; ClosureRewriteModule( AbstractCompiler compiler, PreprocessorSymbolTable preprocessorSymbolTable, @Nullable TypedScope globalTypedScope) { checkArgument(globalTypedScope == null || globalTypedScope.isGlobal()); this.compiler = compiler; this.astFactory = compiler.createAstFactory(); this.preprocessorSymbolTable = preprocessorSymbolTable; this.preserveSugar = compiler.getOptions().shouldPreserveGoogModule(); this.globalTypedScope = globalTypedScope; } private class UnwrapGoogLoadModule extends NodeTraversal.AbstractPreOrderCallback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case ROOT: case SCRIPT: return true; case EXPR_RESULT: Node call = n.getFirstChild(); if (NodeUtil.isCallTo(call, GOOG_LOADMODULE) && call.getLastChild().isFunction()) { parent.putBooleanProp(Node.GOOG_MODULE, true); Node functionNode = call.getLastChild(); compiler.reportFunctionDeleted(functionNode); Node moduleBody = functionNode.getLastChild().detach(); moduleBody.setToken(Token.MODULE_BODY); Node exportsParameter = NodeUtil.getFunctionParameters(functionNode).getOnlyChild(); moduleBody.setJSType(exportsParameter.getJSType()); n.replaceWith(moduleBody); Node returnNode = moduleBody.getLastChild(); if (!returnNode.isReturn()) { compiler.report(JSError.make(moduleBody, LOAD_MODULE_FN_MISSING_RETURN)); } else { returnNode.detach(); } t.reportCodeChange(); } return false; default: return false; } } } @Override public void process(Node externs, Node root) { // Record all the scripts first so that the googModuleNamespaces global state can be complete // before doing any updating also queue up scriptDescriptions for later use in ScriptUpdater // runs. Deque scriptDescriptions = new ArrayDeque<>(); for (Node parent : ImmutableList.of(externs, root)) { for (Node script = parent.getFirstChild(); script != null; script = script.getNext()) { checkState(script.isScript(), script); NodeTraversal.traverse(compiler, script, new UnwrapGoogLoadModule()); pushScript(new ScriptDescription()); // sets currentScript currentScript.rootNode = script; scriptDescriptions.addLast(currentScript); NodeTraversal.traverse(compiler, script, new ScriptPreprocessor()); NodeTraversal.traverse(compiler, script, new ScriptRecorder()); popScript(); } } reportUnrecognizedRequires(); if (compiler.hasHaltingErrors()) { return; } // Update scripts using the now complete googModuleNamespaces global state and unspool the // scriptDescriptions that were queued up by all the recording. NodeTraversal.traverseRoots(compiler, new ScriptUpdater(scriptDescriptions), externs, root); declareSyntheticExterns(); this.googModuleGetCalls.forEach(this::updateGoogModuleGetCall); this.googRequireDynamicCalls.forEach(this::updateGoogRequireDynamicCall); } /** * Declares `var foo;` in the externs for all {@code synthetic_externs} names that aren't already * in the global scope. * *

Only add externs in the error case where there is an unrecognized goog.require or * goog.module.get. Clutz depends on the compiler deleting local name declarations that alias or * destructure the invalid call from this file. In order to preserve AST validity, we instead * declare the deleted name in the externs. (which is fine with Clutz, as it just ignores the * synthetic externs) */ private void declareSyntheticExterns() { ImmutableList vars = syntheticExterns.values().stream() // Skip roots of goog.provide or goog.module.declareLegacyNamespace(); .filter((lhs) -> !isNameInGlobalScope(lhs.getString())) .map( (lhs) -> IR.var(astFactory.createNameWithUnknownType(lhs.getString())).srcrefTree(lhs)) .collect(toImmutableList()); if (vars.isEmpty()) { return; } Node root = compiler.getSynthesizedExternsInput().getAstRoot(compiler); vars.forEach(root::addChildToBack); } /** * Returns whether the name is declared in the global scope, either explicitly with var/const/let * or implicitly by a goog.provide or legacy goog.module. */ private boolean isNameInGlobalScope(String name) { return legacyScriptNamespacesAndPrefixes.contains(name) || globalScope.getVar(name) != null; } /** * Rewrites object literal exports to the standard named exports style. i.e. exports = {Foo, Bar} * to exports.Foo = Foo; exports.Bar = Bar; This makes the module exports into a more standard * format for later passes. */ private void preprocessExportDeclaration(NodeTraversal t, Node n) { if (!isGoogModuleExportsRef(t, n) || !isAssignTarget(n) || !n.getGrandparent().isExprResult()) { return; } checkState(currentScript.defaultExport == null); Node exportRhs = n.getNext(); if (NodeUtil.isNamedExportsLiteral(exportRhs)) { Node insertionPoint = n.getGrandparent(); for (Node key = exportRhs.getFirstChild(); key != null; key = key.getNext()) { String exportName = key.getString(); JSDocInfo jsdoc = key.getJSDocInfo(); Node rhs = key.removeFirstChild(); Node lhs = astFactory .createGetProp(astFactory.createName("exports", type(n)), exportName, type(rhs)) .srcrefTree(key); Node newExport = IR.exprResult(astFactory.createAssign(lhs, rhs).srcref(key).setJSDocInfo(jsdoc)) .srcref(key); newExport.insertAfter(insertionPoint); insertionPoint = newExport; } n.getGrandparent().detach(); } } private void recordModuleBody(Node moduleRoot) { pushScript(new ScriptDescription()); currentScript.rootNode = moduleRoot; currentScript.isModule = true; } private void recordGoogModule(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); if (!namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_MODULE_ID_ARG); return; } String namespaceId = namespaceIdNode.getString(); currentScript.namespaceId = namespaceId; currentScript.contentsPrefix = toModuleContentsPrefix(namespaceId); Node scriptNode = NodeUtil.getEnclosingScript(currentScript.rootNode); rewriteState.scriptDescriptionsByGoogModuleNamespace.put(namespaceId, currentScript); rewriteState.namespaceIdsByScriptNode.put(scriptNode, namespaceId); } private void recordGoogDeclareLegacyNamespace() { currentScript.declareLegacyNamespace = true; updateLegacyScriptNamespacesAndPrefixes(currentScript.namespaceId); } private void updateLegacyScriptNamespacesAndPrefixes(String namespace) { legacyScriptNamespacesAndPrefixes.add(namespace); for (int dot = namespace.lastIndexOf('.'); dot != -1; dot = namespace.lastIndexOf('.')) { namespace = namespace.substring(0, dot); legacyScriptNamespacesAndPrefixes.add(namespace); } } private void recordGoogProvide(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); if (!namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_PROVIDE_NAMESPACE); return; } String namespaceId = namespaceIdNode.getString(); if (currentScript.isModule) { t.report(namespaceIdNode, INVALID_PROVIDE_CALL); } Node scriptNode = NodeUtil.getEnclosingScript(call); // Log legacy namespaces and prefixes. rewriteState.providedNamespaces.add(namespaceId); rewriteState.namespaceIdsByScriptNode.put(scriptNode, namespaceId); updateLegacyScriptNamespacesAndPrefixes(namespaceId); } private void recordGoogRequire(NodeTraversal t, Node call, boolean mustBeOrdered) { maybeSplitMultiVar(call); Node namespaceIdNode = call.getLastChild(); if (!namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_REQUIRE_NAMESPACE); return; } String namespaceId = namespaceIdNode.getString(); // Maybe report an error if there is an attempt to import something that is expected to be a // goog.module() but no such goog.module() has been defined. boolean targetIsAModule = rewriteState.containsModule(namespaceId); boolean targetIsALegacyScript = rewriteState.providedNamespaces.contains(namespaceId); if (currentScript.isModule && !targetIsAModule && !targetIsALegacyScript) { unrecognizedRequires.add(new UnrecognizedRequire(call, namespaceId, mustBeOrdered)); } } private void recordGoogRequireType(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); if (!namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_REQUIRE_TYPE_NAMESPACE); return; } // For purposes of import collection, goog.requireType is the same as goog.require but // a goog.requireType call is not required to appear after the corresponding namespace // definition. recordGoogRequire(t, call, /* mustBeOrdered= */ false); } private void recordGoogForwardDeclare(NodeTraversal t, Node call) { Node namespaceNode = call.getLastChild(); if (!call.hasTwoChildren() || !namespaceNode.isStringLit()) { t.report(namespaceNode, INVALID_FORWARD_DECLARE_NAMESPACE); return; } // modules already require that goog.forwardDeclare() and goog.module.get() occur in matched // pairs. If a "missing module" error were to occur here it would also occur in the matching // goog.module.get(). To avoid reporting the error twice suppress it here. boolean mustBeOrdered = false; // For purposes of import collection, goog.forwardDeclare is the same as goog.require. recordGoogRequire(t, call, mustBeOrdered); } private void recordGoogRequireDynamic(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); if (!namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_REQUIRE_DYNAMIC); return; } String namespaceId = namespaceIdNode.getString(); if (!rewriteState.containsModule(namespaceId)) { unrecognizedRequires.add( new UnrecognizedRequire(call, namespaceId, /* mustBeOrdered= */ false)); } this.googRequireDynamicCalls.add(call); } private void recordGoogModuleGet(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); if (!call.hasTwoChildren() || !namespaceIdNode.isStringLit()) { t.report(namespaceIdNode, INVALID_GET_NAMESPACE); return; } String namespaceId = namespaceIdNode.getString(); if (!rewriteState.containsModule(namespaceId)) { unrecognizedRequires.add( new UnrecognizedRequire(call, namespaceId, /* mustBeOrdered= */ false)); } this.googModuleGetCalls.add(call); Node maybeAssign = call.getParent(); boolean isFillingAnAlias = maybeAssign.isAssign() && maybeAssign.getFirstChild().isName() && maybeAssign.getParent().isExprResult(); if (!isFillingAnAlias || !currentScript.isModule) { return; } String aliasName = call.getParent().getFirstChild().getString(); // If the assignment isn't into a var in our scope then it's not ok. Var aliasVar = t.getScope().getVar(aliasName); if (aliasVar == null) { // Reported in CheckClosureImports return; } // Even if it was to a var in our scope it should still only rewrite if the var looked like: // let x = goog.forwardDeclare('a.namespace'); Node aliasVarNodeRhs = NodeUtil.getRValueOfLValue(aliasVar.getNode()); if (aliasVarNodeRhs == null || !NodeUtil.isCallTo(aliasVarNodeRhs, GOOG_FORWARDDECLARE) || !namespaceId.equals(aliasVarNodeRhs.getLastChild().getString())) { // Reported in CheckClosureImports return; } // Each goog.module.get() calling filling an alias will have the alias importing logic // handled at the goog.forwardDeclare call, and the corresponding goog.module.get can simply // be removed. compiler.reportChangeToEnclosingScope(maybeAssign); maybeAssign.getParent().detach(); this.googModuleGetCalls.remove(this.googModuleGetCalls.size() - 1); } private void recordTopLevelClassOrFunctionName(Node classOrFunctionNode) { Node nameNode = classOrFunctionNode.getFirstChild(); if (nameNode.isName() && !Strings.isNullOrEmpty(nameNode.getString())) { String name = nameNode.getString(); currentScript.topLevelNames.add(name); } } private void recordTopLevelVarNames(Node varNode) { NodeUtil.visitLhsNodesInNode( varNode, (lhs) -> currentScript.topLevelNames.add(lhs.getString())); } private void maybeRecordExportDeclaration(NodeTraversal t, Node n) { if (!currentScript.isModule || !isGoogModuleExportsRef(t, n) || !isAssignTarget(n)) { return; } // ClosureCheckModule reports an error for duplicate 'exports = ' assignments, but that error // may be suppressed. If so then use the final assignment as the canonical one. if (currentScript.defaultExport != null) { ExportDefinition previousExport = currentScript.defaultExport; String localName = previousExport.getLocalName(); if (localName != null && currentScript.namesToInlineByAlias.containsKey(localName)) { currentScript.namesToInlineByAlias.remove(localName); } currentScript.defaultExportLocalName = null; } Node exportRhs = n.getNext(); // Exports object should have already been converted in ScriptPreprocess step. checkState( !NodeUtil.isNamedExportsLiteral(exportRhs), "Exports object should have been converted already"); currentScript.willCreateExportsObject = true; ExportDefinition defaultExport = ExportDefinition.newDefaultExport(t, exportRhs); currentScript.defaultExport = defaultExport; if (!currentScript.declareLegacyNamespace && defaultExport.hasInlinableName(currentScript.exportsToInline.keySet())) { String localName = defaultExport.getLocalName(); currentScript.defaultExportLocalName = localName; recordExportToInline(defaultExport); } return; } private void updateModuleBodyEarly(Node moduleScopeRoot) { pushScript(currentScript.removeFirstChildScript()); currentScript.rootNode = moduleScopeRoot; } private void updateGoogModule(NodeTraversal t, Node call) { if (!currentScript.isModule) { compiler.reportChangeToEnclosingScope(call); Node undefined = astFactory.createVoid(astFactory.createNumber(0)).srcrefTree(call); call.replaceWith(undefined); return; } // If it's a goog.module() with a legacy namespace. if (currentScript.declareLegacyNamespace) { // Rewrite "goog.module('Foo');" as "goog.provide('Foo');". call.getFirstChild().setString("provide"); compiler.reportChangeToEnclosingScope(call); } // If this script file isn't going to eventually create it's own exports object, then we know // we'll need to do it ourselves, and so we might as well create it as early as possible to // avoid ordering issues with goog.define(). if (!currentScript.willCreateExportsObject) { checkState(!currentScript.hasCreatedExportObject, currentScript); exportTheEmptyBinaryNamespaceAt(NodeUtil.getEnclosingStatement(call), AddAt.AFTER, t); } if (!currentScript.declareLegacyNamespace && !preserveSugar) { // Otherwise it's a regular module and the goog.module() line can be removed. compiler.reportChangeToEnclosingScope(call); NodeUtil.getEnclosingStatement(call).detach(); } } private static void updateGoogDeclareLegacyNamespace(Node call) { NodeUtil.getEnclosingStatement(call).detach(); } private void updateGoogRequire(NodeTraversal t, Node call) { Node namespaceIdNode = call.getLastChild(); Node statementNode = NodeUtil.getEnclosingStatement(call); String namespaceId = namespaceIdNode.getString(); boolean targetIsNonLegacyGoogModule = rewriteState.containsModule(namespaceId) && !rewriteState.isLegacyModule(namespaceId); boolean importHasAlias = NodeUtil.isNameDeclaration(statementNode); boolean isDestructuring = statementNode.getFirstChild().isDestructuringLhs(); // If the current script is a module or the require statement has a return value that is stored // in an alias then the require is goog.module() style. boolean currentScriptIsAModule = currentScript.isModule; // "var Foo = goog.require("bar.Foo");" or "const {Foo} = goog.require('bar');" style. boolean requireDirectlyStoredInAlias = NodeUtil.isNameDeclaration(call.getGrandparent()); if (currentScriptIsAModule && requireDirectlyStoredInAlias && isTopLevel(t, statementNode, ScopeType.EXEC_CONTEXT)) { // Record alias -> exportedNamespace associations for later inlining. Node lhs = call.getParent(); String exportedNamespace = rewriteState.getExportedNamespaceOrScript(namespaceId); if (exportedNamespace == null) { // There's nothing to inline. The missing provide/module will be reported elsewhere. } else if (lhs.isName()) { // `var Foo` case String aliasName = statementNode.getFirstChild().getString(); recordNameToInline(aliasName, exportedNamespace, namespaceId); maybeAddAliasToSymbolTable(statementNode.getFirstChild(), currentScript.namespaceId); } else if (lhs.isDestructuringLhs() && lhs.getFirstChild().isObjectPattern()) { // `const {Foo}` case maybeWarnForInvalidDestructuring(t, lhs.getParent(), namespaceId); for (Node importSpec = lhs.getFirstFirstChild(); importSpec != null; importSpec = importSpec.getNext()) { checkState(importSpec.hasChildren(), importSpec); String importedProperty = importSpec.getString(); Node aliasNode = importSpec.getFirstChild(); String aliasName = aliasNode.getString(); String fullName = exportedNamespace + "." + importedProperty; recordNameToInline(aliasName, fullName, /* namespaceId= */ null); // Record alias before we rename node. maybeAddAliasToSymbolTable(aliasNode, currentScript.namespaceId); // Need to rename node otherwise it will stay global and messes up index if there are // other files that use the same destructuring alias. safeSetString(aliasNode, currentScript.contentsPrefix + aliasName); } } else { throw new RuntimeException("Illegal goog.module import: " + lhs); } } if (currentScript.isModule || targetIsNonLegacyGoogModule) { if (isDestructuring) { if (!preserveSugar) { // Delete the goog.require() because we're going to inline its alias later. compiler.reportChangeToEnclosingScope(statementNode); statementNode.detach(); } } else if (targetIsNonLegacyGoogModule) { checkState( isTopLevel(t, statementNode, ScopeType.EXEC_CONTEXT), "Unexpected non-top-level require at %s", call); if (importHasAlias || !rewriteState.isLegacyModule(namespaceId)) { if (!preserveSugar) { // Delete the goog.require() because we're going to inline its alias later. compiler.reportChangeToEnclosingScope(statementNode); statementNode.detach(); } } } else { // TODO(bangert): make this compatible with preserveSugar. const B = goog.require('b') runs // into problems because the type checker cannot handle const. // Rewrite // "var B = goog.require('B');" to // "goog.require('B');" // because even though we're going to inline the B alias, // ProcessClosurePrimitives is going to want to see this legacy require. call.detach(); statementNode.replaceWith(IR.exprResult(call)); compiler.reportChangeToEnclosingScope(call); } } } // These restrictions are in place to make it easier to migrate goog.modules to ES6 modules, // by structuring the imports/exports in a consistent way. private void maybeWarnForInvalidDestructuring( NodeTraversal t, Node importNode, String importedNamespace) { checkArgument(importNode.getFirstChild().isDestructuringLhs(), importNode); ScriptDescription importedModule = rewriteState.scriptDescriptionsByGoogModuleNamespace.get(importedNamespace); if (importedModule == null) { // Don't know enough to give a good warning here. return; } if (importedModule.defaultExport != null) { t.report(importNode, ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT); return; } Node objPattern = importNode.getFirstFirstChild(); for (Node key = objPattern.getFirstChild(); key != null; key = key.getNext()) { String exportName = key.getString(); if (!importedModule.namedExports.contains(exportName)) { t.report(importNode, ILLEGAL_DESTRUCTURING_NOT_EXPORTED, exportName, importedNamespace); } } } private void updateGoogForwardDeclare(NodeTraversal t, Node call) { // For import rewriting purposes and when taking into account previous moduleAlias versus // namespaceId import categorization, goog.forwardDeclare is much the same as goog.require. updateGoogRequire(t, call); } private void updateGoogRequireDynamicCall(Node call) { final Node parent = call.getParent(); checkState( parent.isAwait() || (parent.isGetProp() && parent.getString().equals("then")), "goog.requireDynamic() in only allowed in await/then expression"); if (parent.isAwait()) { updateGoogRequireDynamicCallInAwait(call); } else { updateGoogRequireDynamicCallInThen(call); } } // Rewrite // goog.requireDynamic('a.b.c').then( ({Foo}) => { new Foo().render(); }); // to // goog.importHandler_('h4sh').then(() => { // const {Foo} = module$exports$a$b$c; // new Foo().render(); // }); // Note that rewriting works for both destructuring pattern and name, i.e., `{Foo}` or `foo`. private void updateGoogRequireDynamicCallInThen(Node call) { Node namespaceIdNode = call.getSecondChild(); String namespaceId = namespaceIdNode.getString(); String exportedNamespace = rewriteState.getExportedNamespaceOrScript(namespaceId); checkState( exportedNamespace != null, "Exported namespace for goog.requireDynamic() canot be null"); // `goog.requireDynamic('a.b.c').then()` final Node getProp = call.getParent(); final Node thenCallNode = getProp.getParent(); checkState(thenCallNode != null && thenCallNode.isCall(), "must be a 'then' call expression"); // Create a string literal containing the ID of the chunk containing the module we want Node argNode = this.astFactory.createString(namespaceIdToXid(namespaceId)).srcref(namespaceIdNode); // `goog.requireDynamic('a.b.c')` -> `goog.requireDynamic('chunkId')` namespaceIdNode.replaceWith(argNode); // `goog.requireDynamic` -> `goog.importHandler_` Node calleeNode = call.getFirstChild(); checkState(calleeNode.matchesQualifiedName(GOOG_REQUIREDYNAMIC_NAME), calleeNode); calleeNode.setString(IMPORT_HANDLER_NAME); // `module$exports$a$b$c` Node exportedNamespaceNameNode = this.astFactory.createName(this.globalTypedScope, exportedNamespace).srcrefTree(call); exportedNamespaceNameNode.setOriginalName(namespaceId); // Get `{Foo}` from `goog.requireDynamic().then( ({Foo}) => { } )`; Node functionNode = thenCallNode.getSecondChild(); checkState(functionNode != null && functionNode.isFunction(), "must be a function in `then`"); // Get param list of the callback function Node paramListNode = functionNode.getSecondChild(); checkState(paramListNode.getChildCount() == 1, "function must have only one parameter"); // Get the callback function body Node functionBody = paramListNode.getNext(); // Get parameter, object pattern or name Node objectPatternOrNameNode = paramListNode.getOnlyChild(); checkState( objectPatternOrNameNode.isObjectPattern() || objectPatternOrNameNode.isName(), "parameter of callback function must be object pattern or name"); objectPatternOrNameNode.detach(); // `({Foo}) => {}` -> `() => {}` paramListNode.removeChildren(); // Dynamic require is not allowed for JS that is too old to have `const` (< ES6), // We don't expect to ever see `--language_in=ES5` in combination with dynamic require. final LanguageMode languageIn = compiler.getOptions().getLanguageIn(); checkState( languageIn.toFeatureSet().contains(Feature.CONST_DECLARATIONS), "'%s' does not contain '%s'", languageIn, Feature.CONST_DECLARATIONS); Node enclosingScript = NodeUtil.getEnclosingScript(call); NodeUtil.addFeatureToScript(enclosingScript, Feature.CONST_DECLARATIONS, compiler); // Create `const {Foo} = module$exports$a$b$c;` // Or `const foo = module$exports$a$b$c;' Node declarationNode; if (objectPatternOrNameNode.isObjectPattern()) { declarationNode = this.astFactory .createSingleConstObjectPatternDeclaration( objectPatternOrNameNode, exportedNamespaceNameNode) .srcrefTreeIfMissing(call); } else { declarationNode = this.astFactory .createSingleConstNameDeclaration( objectPatternOrNameNode.getString(), exportedNamespaceNameNode) .srcrefTreeIfMissing(call); } // If right hand side of the callback arrow function is an expression instead of BLOCK, // e.g., `({Foo}) => Foo` instead of `() => { return Foo; }`, create a BLOCK to host the // expression. if (functionBody.isBlock()) { functionBody.addChildToFront(declarationNode); compiler.reportChangeToEnclosingScope(declarationNode); } else { Node returnStmt = this.astFactory.createReturn(functionBody.detach()); Node newBlock = this.astFactory.createBlock(returnStmt).srcrefTree(call); newBlock.insertAfter(paramListNode); newBlock.addChildToFront(declarationNode); compiler.reportChangeToEnclosingScope(newBlock); } compiler.reportChangeToEnclosingScope(call); } // Rewrite // const {Foo} = await goog.requireDynamic('a.b.c')` // to // await goog.importHandler_('tJJovc'); // const {Foo} = module$exports$a$b$c; private void updateGoogRequireDynamicCallInAwait(Node call) { Node namespaceIdNode = call.getSecondChild(); String namespaceId = namespaceIdNode.getString(); String exportedNamespace = rewriteState.getExportedNamespaceOrScript(namespaceId); checkState( exportedNamespace != null, "Exported namespace for goog.requireDynamic() cannot be null"); // `await goog.requireDynamic('a.b.c')` final Node existingAwait = call.getParent(); checkState( existingAwait.isAwait(), "Only goog.requireDynamic() in await expression is supported now"); // `{Foo} = await goog.requireDynamic('a.b.c')` Node awaitParent = existingAwait.getParent(); checkState( awaitParent != null && (awaitParent.isDestructuringLhs() || awaitParent.isName()), "LHS must be a destructing LHS or name"); // `const {Foo} = await goog.requireDynamic('a.b.c')` Node declarationStatement = awaitParent.getParent(); // Reject non-declarations or declarations in a for loop. checkState( NodeUtil.isNameDeclaration(declarationStatement) && NodeUtil.isStatement(declarationStatement), "must be a declaration statement"); // `module$exports$a$b$c` Node exportedNamespaceNameNode = this.astFactory.createQName(this.globalTypedScope, exportedNamespace).srcrefTree(call); exportedNamespaceNameNode.setJSType(rewriteState.getGoogModuleNamespaceType(namespaceId)); exportedNamespaceNameNode.setOriginalName(namespaceId); // Create a string literal containing the ID of the chunk containing the module we want Node argNode = this.astFactory.createString(namespaceIdToXid(namespaceId)).srcref(namespaceIdNode); // `goog.requireDynamic('a.b.c')` -> `goog.requireDynamic('chunkId')` namespaceIdNode.replaceWith(argNode); // `goog.requireDynamic` -> `goog.importHandler_` Node calleeNode = call.getFirstChild(); checkState(calleeNode.matchesQualifiedName(GOOG_REQUIREDYNAMIC_NAME), calleeNode); calleeNode.setString(IMPORT_HANDLER_NAME); // Replace the await with the module object // `const {Foo} = module$exports$a$b$c;` existingAwait.replaceWith(exportedNamespaceNameNode); // Insert the await as a statement before the declaration final Node awaitStatement = astFactory.exprResult(existingAwait).srcref(existingAwait); awaitStatement.insertBefore(declarationStatement); compiler.reportChangeToEnclosingScope(call); } private String namespaceIdToXid(String namespaceId) { Xid.HashFunction hashFunction = this.compiler.getOptions().chunkIdHashFunction; Xid xid = hashFunction == null ? new Xid() : new Xid(hashFunction); return xid.get(namespaceId); } private void updateGoogModuleGetCall(Node call) { Node namespaceIdNode = call.getSecondChild(); String namespaceId = namespaceIdNode.getString(); // Remaining calls to goog.module.get() are not alias updates, // and should be replaced by a reference to the proper name. // Replace "goog.module.get('pkg.Foo')" with either "pkg.Foo" or "module$exports$pkg$Foo". String exportedNamespace = rewriteState.getExportedNamespaceOrScript(namespaceId); if (exportedNamespace != null) { compiler.reportChangeToEnclosingScope(call); Node exportedNamespaceName = this.astFactory .createQNameFromTypedScope(this.globalTypedScope, exportedNamespace) .srcrefTree(call); exportedNamespaceName.setJSType(rewriteState.getGoogModuleNamespaceType(namespaceId)); exportedNamespaceName.setOriginalName(namespaceId); call.replaceWith(exportedNamespaceName); } } private void recordExportsPropertyAssignment(NodeTraversal t, Node getpropNode) { if (!currentScript.isModule) { return; } Node parent = getpropNode.getParent(); checkState(parent.isAssign() || parent.isExprResult(), parent); Node exportsNameNode = getpropNode.getFirstChild(); checkState(exportsNameNode.getString().equals("exports"), exportsNameNode); if (t.inModuleScope()) { String exportName = getpropNode.getString(); currentScript.namedExports.add(exportName); Node exportRhs = getpropNode.getNext(); ExportDefinition namedExport = ExportDefinition.newNamedExport(t, exportName, exportRhs); if (!currentScript.declareLegacyNamespace && currentScript.defaultExport == null && namedExport.hasInlinableName(currentScript.exportsToInline.keySet())) { recordExportToInline(namedExport); parent.getParent().detach(); } } } private void updateExportsPropertyAssignment(Node getpropNode, NodeTraversal t) { if (!currentScript.isModule) { return; } Node parent = getpropNode.getParent(); checkState(parent.isAssign() || parent.isExprResult(), parent); // Update "exports.foo = Foo" to "module$exports$pkg$Foo.foo = Foo"; Node exportsNameNode = getpropNode.getFirstChild(); checkState(exportsNameNode.getString().equals("exports")); String exportedNamespace = currentScript.getExportedNamespace(); safeSetMaybeQualifiedString(exportsNameNode, exportedNamespace, /* isModuleNamespace= */ false); Node jsdocNode = parent.isAssign() ? parent : getpropNode; markConstAndCopyJsDoc(jsdocNode, jsdocNode); // When seeing the first "exports.foo = ..." line put a "var module$exports$pkg$Foo = {};" // before it. if (!currentScript.hasCreatedExportObject) { exportTheEmptyBinaryNamespaceAt(NodeUtil.getEnclosingStatement(parent), AddAt.BEFORE, t); } } /** * Rewrites top level var names from "var foo; console.log(foo);" to "var module$contents$Foo_foo; * console.log(module$contents$Foo_foo);" */ private void maybeUpdateTopLevelName(NodeTraversal t, Node nameNode) { String name = nameNode.getString(); if (!currentScript.isModule || !currentScript.topLevelNames.contains(name)) { return; } Var var = t.getScope().getVar(name); // If the name refers to a var that is not from the top level scope. if (var == null || var.getScope().getRootNode() != currentScript.rootNode) { // Then it shouldn't be renamed. return; } // If the name is part of a destructuring import, the import rewriting will take care of it if (var.getNameNode() == nameNode && nameNode.getParent().isStringKey() && nameNode.getGrandparent().isObjectPattern()) { Node destructuringLhsNode = nameNode.getGrandparent().getParent(); if (NodeUtil.isCallTo(destructuringLhsNode.getLastChild(), GOOG_REQUIRE) || NodeUtil.isCallTo(destructuringLhsNode.getLastChild(), GOOG_REQUIRETYPE)) { return; } } // If the name is an alias for an imported namespace or an exported local, rewrite from // "new Foo;" to "new module$exports$Foo;" or "new Foo" to "new module$contents$bar$Foo". boolean nameIsAnAlias = currentScript.namesToInlineByAlias.containsKey(name); if (nameIsAnAlias && var.getNode() != nameNode) { maybeAddAliasToSymbolTable(nameNode, currentScript.namespaceId); AliasName inline = currentScript.namesToInlineByAlias.get(name); String namespaceToInline = inline.newName; if (namespaceToInline.equals(currentScript.getBinaryNamespace())) { currentScript.hasCreatedExportObject = true; } boolean isModuleNamespace = inline.namespaceId != null && rewriteState.scriptDescriptionsByGoogModuleNamespace.containsKey( inline.namespaceId) && !rewriteState.scriptDescriptionsByGoogModuleNamespace.get(inline.namespaceId) .willCreateExportsObject; safeSetMaybeQualifiedString(nameNode, namespaceToInline, isModuleNamespace); // Make sure this action won't shadow a local variable. if (namespaceToInline.indexOf('.') != -1) { String firstQualifiedName = namespaceToInline.substring(0, namespaceToInline.indexOf('.')); Var shadowedVar = t.getScope().getVar(firstQualifiedName); if (shadowedVar == null || shadowedVar.isGlobal() || shadowedVar.getScope().isModuleScope()) { return; } t.report( shadowedVar.getNode(), IMPORT_INLINING_SHADOWS_VAR, shadowedVar.getName(), namespaceToInline); } return; } // For non-import alias names rewrite from // "var foo; console.log(foo);" to // "var module$contents$Foo_foo; console.log(module$contents$Foo_foo);" safeSetString(nameNode, currentScript.contentsPrefix + name); } /** * For exports like "exports = {prop: value}" update the declarations to enforce @const ness * (and typedef exports). * *

TODO(blickly): Remove as much of this functionality as possible, now that these style of * exports are rewritten in ScriptPreprocess step. */ private void maybeUpdateExportObjectLiteral(NodeTraversal t, Node n) { if (!currentScript.isModule) { return; } Node parent = n.getParent(); Node rhs = parent.getLastChild(); if (rhs.isObjectLit()) { for (Node c = rhs.getFirstChild(); c != null; c = c.getNext()) { if (c.isComputedProp()) { t.report(c, INVALID_EXPORT_COMPUTED_PROPERTY); } else if (c.isStringKey()) { Node value = c.getFirstChild(); maybeUpdateExportDeclToNode(t, c, value); } } } } private void maybeUpdateExportDeclToNode(NodeTraversal t, Node target, Node value) { if (!currentScript.isModule) { return; } // If the RHS is a typedef, clone the declaration. // Hack alert: clone the typedef declaration if one exists // this is a simple attempt that covers the common case of the // exports being in the same scope as the typedef declaration. // Otherwise the type name might be invalid. if (value.isName()) { Scope currentScope = t.getScope(); Var v = t.getScope().getVar(value.getString()); if (v != null) { AbstractScope varScope = v.getScope(); if (varScope.getDepth() == currentScope.getDepth()) { JSDocInfo info = v.getJSDocInfo(); if (info != null && info.hasTypedefType()) { JSDocInfo.Builder builder = JSDocInfo.Builder.copyFrom(info); target.setJSDocInfo(builder.build()); return; } } } } markConstAndCopyJsDoc(target, target); } /** In module "foo.Bar", rewrite "exports = Bar" to "var module$exports$foo$Bar = Bar". */ private void maybeUpdateExportDeclaration(NodeTraversal t, Node n) { if (!currentScript.isModule || !isGoogModuleExportsRef(t, n) || !isAssignTarget(n)) { return; } Node assignNode = n.getParent(); Node rhs = assignNode.getLastChild(); if (rhs != currentScript.defaultExport.rhs) { // This script has duplicate 'exports = ' assignments. Preserve the rhs as an expression but // don't declare it as a global variable. assignNode.replaceWith(rhs.detach()); return; } if (!currentScript.declareLegacyNamespace && currentScript.defaultExportLocalName != null) { assignNode.getParent().detach(); Node binaryNamespaceName = astFactory.createName(currentScript.getBinaryNamespace(), type(n)); this.declareGlobalVariable(binaryNamespaceName, t); return; } // Rewrite "exports = ..." as "var module$exports$foo$Bar = ..." Node jsdocNode; if (currentScript.declareLegacyNamespace) { Node legacyQname = this.astFactory .createQName(this.globalTypedScope, currentScript.namespaceId) .srcrefTree(n); legacyQname.setJSType(n.getJSType()); n.replaceWith(legacyQname); jsdocNode = assignNode; } else { rhs.detach(); Node exprResultNode = assignNode.getParent(); Node binaryNamespaceName = astFactory.createName(currentScript.getBinaryNamespace(), type(n)); binaryNamespaceName.setOriginalName("exports"); this.declareGlobalVariable(binaryNamespaceName, t); Node exportsObjectCreationNode = IR.var(binaryNamespaceName, rhs); exportsObjectCreationNode.srcrefTreeIfMissing(exprResultNode); exportsObjectCreationNode.putBooleanProp(Node.IS_NAMESPACE, true); exprResultNode.replaceWith(exportsObjectCreationNode); jsdocNode = exportsObjectCreationNode; currentScript.hasCreatedExportObject = true; } markConstAndCopyJsDoc(assignNode, jsdocNode); compiler.reportChangeToEnclosingScope(jsdocNode); maybeUpdateExportObjectLiteral(t, rhs); return; } private void maybeUpdateExportNameRef(NodeTraversal t, Node n) { if (!currentScript.isModule || !isGoogModuleExportsRef(t, n) || n.getParent() == null) { return; } if (n.getParent().isParamList()) { return; } if (currentScript.declareLegacyNamespace) { Node legacyQname = this.astFactory .createQName(this.globalTypedScope, currentScript.namespaceId) .srcrefTree(n); legacyQname.setJSType(n.getJSType()); n.replaceWith(legacyQname); compiler.reportChangeToEnclosingScope(legacyQname); return; } safeSetString(n, currentScript.getBinaryNamespace()); // Either this module is going to create it's own exports object at some point or else if it's // going to be defensively created automatically then that should have occurred at the top of // the file and been done by now. checkState(currentScript.willCreateExportsObject || currentScript.hasCreatedExportObject); } void updateModuleBody(Node moduleBody) { checkArgument( moduleBody.isModuleBody() && moduleBody.getParent().getBooleanProp(Node.GOOG_MODULE), moduleBody); moduleBody.setToken(Token.BLOCK); NodeUtil.tryMergeBlock(moduleBody, true); for (ExportDefinition export : currentScript.exportsToInline.values()) { Node nameNode = export.nameDecl.getNameNode(); safeSetMaybeQualifiedString( nameNode, currentScript.getBinaryNamespace() + export.getExportPostfix(), false); } checkState(currentScript.isModule, currentScript); checkState( currentScript.declareLegacyNamespace || currentScript.hasCreatedExportObject, currentScript); popScript(); } /** * Record the provided script as the current script at top of the script stack and add it as a * child of the previous current script if there was one. * *

Keeping track of the current script facilitates aggregation of accurate script state so that * rewriting can run properly. Handles scripts and nested goog.modules. */ private void pushScript(ScriptDescription newCurrentScript) { currentScript = newCurrentScript; if (!scriptStack.isEmpty()) { ScriptDescription parentScript = scriptStack.peek(); parentScript.addChildScript(currentScript); } scriptStack.addFirst(currentScript); } private void popScript() { scriptStack.removeFirst(); currentScript = scriptStack.peekFirst(); } /** Add the missing "var module$exports$pkg$Foo = {};" line. */ private void exportTheEmptyBinaryNamespaceAt(Node atNode, AddAt addAt, NodeTraversal t) { if (currentScript.declareLegacyNamespace) { return; } String binaryNamespaceString = currentScript.getBinaryNamespace(); AstFactory.Type moduleType = type(currentScript.rootNode); Node binaryNamespaceName = astFactory.createName(binaryNamespaceString, moduleType); binaryNamespaceName.setOriginalName(currentScript.namespaceId); this.declareGlobalVariable(binaryNamespaceName, t); Node binaryNamespaceExportNode = IR.var(binaryNamespaceName, astFactory.createObjectLit()); if (addAt == AddAt.BEFORE) { binaryNamespaceExportNode.insertBefore(atNode); } else if (addAt == AddAt.AFTER) { binaryNamespaceExportNode.insertAfter(atNode); } binaryNamespaceExportNode.putBooleanProp(Node.IS_NAMESPACE, true); binaryNamespaceExportNode.srcrefTree(atNode); markConst(binaryNamespaceExportNode); compiler.reportChangeToEnclosingScope(binaryNamespaceExportNode); currentScript.hasCreatedExportObject = true; } static void checkAndSetStrictModeDirective(NodeTraversal t, Node n) { checkState(n.isScript(), n); if (n.isUseStrict()) { t.report(n, USELESS_USE_STRICT_DIRECTIVE); } else { n.setUseStrict(true); } } private static void markConst(Node n) { JSDocInfo.Builder builder = JSDocInfo.Builder.maybeCopyFrom(n.getJSDocInfo()); builder.recordConstancy(); n.setJSDocInfo(builder.build()); } private static void maybeSplitMultiVar(Node rhsNode) { Node statementNode = rhsNode.getGrandparent(); if (!statementNode.isVar() || !statementNode.hasMoreThanOneChild()) { return; } Node nameNode = rhsNode.getParent(); nameNode.detach(); rhsNode.detach(); IR.var(nameNode, rhsNode).insertBefore(statementNode); } private static void markConstAndCopyJsDoc(Node from, Node target) { JSDocInfo info = from.getJSDocInfo(); JSDocInfo.Builder builder = JSDocInfo.Builder.maybeCopyFrom(info); builder.recordConstancy(); target.setJSDocInfo(builder.build()); } private void recordExportToInline(ExportDefinition exportDefinition) { checkState( exportDefinition.hasInlinableName(currentScript.exportsToInline.keySet()), "exportDefinition: %s\n\nexportsToInline keys: %s", exportDefinition, currentScript.exportsToInline.keySet()); checkState( null == currentScript.exportsToInline.put(exportDefinition.nameDecl, exportDefinition), "Already found a mapping for inlining export: %s", exportDefinition.nameDecl); String localName = exportDefinition.getLocalName(); String fullExportedName = currentScript.getBinaryNamespace() + exportDefinition.getExportPostfix(); recordNameToInline(localName, fullExportedName, /* namespaceId= */ null); } private void recordNameToInline(String aliasName, String newName, @Nullable String namespaceId) { checkNotNull(aliasName); checkNotNull(newName); // This intentionally overwrites a possibly pre-existing alias of the same name. // User code might import the same name twice, with the same variable name. That's an error // (duplicate variable definition, reported in TypeValidator), but this code still shouldn't // crash on it. currentScript.namesToInlineByAlias.put(aliasName, new AliasName(newName, namespaceId)); } /** * Examines queue'ed unrecognizedRequires to categorize and report them as either missing module, * missing namespace or late provide. */ private void reportUnrecognizedRequires() { for (UnrecognizedRequire unrecognizedRequire : unrecognizedRequires) { String namespaceId = unrecognizedRequire.namespaceId; Node requireNode = unrecognizedRequire.requireNode; boolean targetGoogModuleExists = rewriteState.containsModule(namespaceId); boolean targetLegacyScriptExists = rewriteState.providedNamespaces.contains(namespaceId); if (targetGoogModuleExists || targetLegacyScriptExists) { // The required thing actually was available somewhere in the program but just wasn't // available as early as the require statement would have liked. continue; } // Remove the require node so this problem isn't reported again in ProcessClosurePrimitives. if (preserveSugar) { continue; } if (NodeUtil.getEnclosingScript(requireNode) == null) { continue; // It's already been removed; nothing to do. } compiler.reportChangeToEnclosingScope(requireNode); Node enclosingStatement = NodeUtil.getEnclosingStatement(requireNode); // To make compilation with partial source information work for Clutz, delete any name // declarations in the enclosing statement completely. For non-declarations, simply replace // the invalid require with null. if (!NodeUtil.isNameDeclaration(enclosingStatement)) { requireNode.replaceWith(astFactory.createNull().srcref(requireNode)); continue; } enclosingStatement.detach(); NodeUtil.visitLhsNodesInNode( enclosingStatement, (lhs) -> syntheticExterns.putIfAbsent(lhs.getString(), lhs)); } } private void safeSetString(Node n, String newString) { if (n.getString().equals(newString)) { return; } String originalName = n.getString(); n.setString(newString); if (n.getOriginalName() == null) { n.setOriginalName(originalName); } // TODO(blickly): It would be better not to be renaming detached nodes Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(n); if (changeScope != null) { compiler.reportChangeToChangeScope(changeScope); } } /** Replaces an identifier with a potentially qualified name */ private void safeSetMaybeQualifiedString( Node nameNode, String newString, boolean isModuleNamespace) { if (!newString.contains(".")) { safeSetString(nameNode, newString); Node parent = nameNode.getParent(); if (isModuleNamespace && parent.isGetProp() && nameNode.getGrandparent().isCall() && parent.isFirstChildOf(nameNode.getGrandparent())) { // In cases where we're calling a function off a module namespace, don't pass the module // namespace as `this`. nameNode.getGrandparent().putBooleanProp(Node.FREE_CALL, true); } return; } // When replacing with a dotted fully qualified name it's already better than an original // name. Node nameParent = nameNode.getParent(); Node newQualifiedName = this.astFactory .createQNameFromTypedScope(this.globalTypedScope, newString) .srcrefTree(nameNode); // Sometimes the typechecker gave `nameNode` the correct type, but we can't infer the right type // for `newQualifiedName`. If so, giving `newQualifiedName` the same type typechecking used for // `nameNode` is less confusing. newQualifiedName.setJSType(nameNode.getJSType()); boolean replaced = safeSetStringIfDeclaration(nameParent, nameNode, newQualifiedName); if (replaced) { return; } nameNode.replaceWith(newQualifiedName); // Given import "var Bar = goog.require('foo.Bar');" here we replace a usage of Bar with // foo.Bar if Bar is goog.provided. 'foo' node is generated and never visible to user. // Because of that we should mark all such nodes as non-indexable leaving only Bar indexable. // Given that replacement is GETPROP node, prefix is first child. It's also possible that // replacement is single-part namespace. Like goog.provide('Bar') in that case replacement // won't have children. if (newQualifiedName.hasChildren()) { newQualifiedName.getFirstChild().makeNonIndexableRecursive(); } compiler.reportChangeToEnclosingScope(newQualifiedName); } /** Sets the string if given a declaration, return whether or not the name was changed */ private static boolean safeSetStringIfDeclaration( Node nameParent, Node nameNode, Node newQualifiedName) { JSDocInfo jsdoc = nameParent.getJSDocInfo(); switch (nameParent.getToken()) { case FUNCTION: case CLASS: if (!NodeUtil.isStatement(nameParent) || nameParent.getFirstChild() != nameNode) { return false; } Node placeholder = IR.empty(); nameParent.replaceWith(placeholder); Node newDeclaration = NodeUtil.getDeclarationFromName(newQualifiedName, nameParent, Token.VAR, jsdoc); if (NodeUtil.isExprAssign(newDeclaration)) { Node assign = newDeclaration.getOnlyChild(); assign.setJSType(nameNode.getJSType()); updateSourceInfoForExportedTopLevelVariable(assign, nameNode); } nameParent.setJSDocInfo(null); newDeclaration.srcrefTreeIfMissing(nameParent); placeholder.replaceWith(newDeclaration); NodeUtil.removeName(nameParent); return true; case VAR: case LET: case CONST: Node rhs = nameNode.hasChildren() ? nameNode.getLastChild().detach() : null; if (jsdoc == null) { // Get inline JSDocInfo if there is no JSDoc on the actual declaration. jsdoc = nameNode.getJSDocInfo(); } Node newStatement = NodeUtil.getDeclarationFromName(newQualifiedName, rhs, Token.VAR, jsdoc); if (NodeUtil.isExprAssign(newStatement)) { Node assign = newStatement.getOnlyChild(); assign.setJSType(nameNode.getJSType()); updateSourceInfoForExportedTopLevelVariable(assign, nameNode); if (nameParent.isConst()) { // When replacing `const name = ...;` with `some.prop = ...`, ensure that `some.prop` // is annotated @const. JSDocInfo.Builder jsdocBuilder = JSDocInfo.Builder.maybeCopyFrom(jsdoc); jsdocBuilder.recordConstancy(); jsdoc = jsdocBuilder.build(); assign.setJSDocInfo(jsdoc); } } newStatement.srcrefTreeIfMissing(nameParent); NodeUtil.replaceDeclarationChild(nameNode, newStatement); return true; case OBJECT_PATTERN: case ARRAY_PATTERN: case PARAM_LIST: throw new RuntimeException("Not supported"); default: break; } return false; } /** * If we had something like `const FOO = "text"` and we export `FOO`, change the source location * information for the rewritten FOO. The replacement should be something like MOD.FOO = "text", * so we look for MOD.FOO and replace the source location for FOO to the original location of FOO. */ private static void updateSourceInfoForExportedTopLevelVariable(Node assign, Node sourceName) { checkState(assign.isAssign()); checkState(sourceName.isName()); // ASSIGN always has two children. Node getProp = assign.getFirstChild(); if (!getProp.isGetProp()) { return; } String name = sourceName.getOriginalName(); if (name == null) { name = sourceName.getString(); } // The source range of this NAME includes its declared value, which we don't want. sourceName = sourceName.cloneNode(); sourceName.setLength(name.length()); // Receiver and prop string should both use the position of the source name. getProp.srcrefTree(sourceName); } private boolean isTopLevel(NodeTraversal t, Node n, ScopeType scopeType) { if (scopeType == ScopeType.EXEC_CONTEXT) { return t.inGlobalScope() || t.getClosestHoistScopeRoot() == currentScript.rootNode; } else { // Must be ScopeType.BLOCK; return n.getParent() == currentScript.rootNode; } } private static String toModuleContentsPrefix(String namespaceId) { return MODULE_CONTENTS_PREFIX + namespaceId.replace('.', '$') + "_"; } public static boolean isModuleExport(String name) { return name.startsWith(MODULE_EXPORTS_PREFIX); } public static boolean isModuleContent(String name) { return name.startsWith(MODULE_CONTENTS_PREFIX); } /** * Returns whether this is a) a reference to the name "exports" and b) based on scoping, actually * refers to the implicit goog.module exports object. */ private static boolean isGoogModuleExportsRef(NodeTraversal t, Node target) { if (!target.isName() || !target.matchesName("exports")) { return false; } Var exportsVar = t.getScope().getVar("exports"); // note: exportsVar may be null for third-party code using UMD patterns like // if ("object"==typeof exports) { [...] // and if null, is not in a goog.module => not a goog.module exports var return exportsVar != null && exportsVar.isGoogModuleExports(); } /** * @return Whether the getprop is used as an assignment target, and that target represents a * module export. Note: that "export.name = value" is an export, while "export.name.foo = * value" is not (it is an assignment to a property of an exported value). */ private static boolean isExportPropertyAssignment(NodeTraversal t, Node n) { Node target = n.getFirstChild(); return (isAssignTarget(n) || isTypedefTarget(n)) && isGoogModuleExportsRef(t, target); } private static boolean isAssignTarget(Node n) { Node parent = n.getParent(); return parent.isAssign() && parent.getFirstChild() == n; } private static boolean isTypedefTarget(Node n) { Node parent = n.getParent(); return parent.isExprResult() && parent.getFirstChild() == n; } /** * Add alias nodes to the symbol table as they going to be removed by rewriter. Example aliases: * *

const Foo = goog.require('my.project.Foo'); const bar = goog.require('my.project.baz'); * const {baz} = goog.require('my.project.utils'); */ private void maybeAddAliasToSymbolTable(Node n, String module) { if (preprocessorSymbolTable != null) { 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.isStringLit() ? 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); } } private void declareGlobalVariable(Node n, NodeTraversal t) { checkState(n.isName()); if (this.globalTypedScope == null) { return; } 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 - 2025 Weber Informatics LLC | Privacy Policy