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 com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.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;
 * 
* * @author [email protected] (John Lenz) */ final class ClosureRewriteModule implements HotSwapCompilerPass { // TODO(johnlenz): handle non-namespace module identifiers aka 'foo/bar' static final DiagnosticType INVALID_MODULE_NAMESPACE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_MODULE_NAMESPACE", "goog.module parameter must be string literals"); 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_REQUIRE_NAMESPACE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_REQUIRE_NAMESPACE", "goog.require parameter must be a string literal."); static final DiagnosticType INVALID_FORWARD_DECLARE_NAMESPACE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_FORWARD_DECLARE_NAMESPACE", "goog.forwardDeclare parameter must be a string literal."); static final DiagnosticType INVALID_GET_NAMESPACE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_GET_NAMESPACE", "goog.module.get 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_CALL_SCOPE = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_GET_CALL_SCOPE", "goog.module.get can not be called in global scope."); 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 DUPLICATE_MODULE = DiagnosticType.error( "JSC_DUPLICATE_MODULE", "Duplicate module: {0}"); static final DiagnosticType DUPLICATE_NAMESPACE = DiagnosticType.error( "JSC_DUPLICATE_NAMESPACE", "Duplicate namespace: {0}"); static final DiagnosticType MISSING_MODULE_OR_PROVIDE = DiagnosticType.error( "JSC_MISSING_MODULE_OR_PROVIDE", "Required namespace \"{0}\" never defined."); static final DiagnosticType LATE_PROVIDE_ERROR = DiagnosticType.error( "JSC_LATE_PROVIDE_ERROR", "Required namespace \"{0}\" not provided yet."); 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 QUALIFIED_REFERENCE_TO_GOOG_MODULE = DiagnosticType.error( "JSC_QUALIFIED_REFERENCE_TO_GOOG_MODULE", "Fully qualified reference to name ''{0}'' provided by a goog.module.\n" + "Either use short import syntax or" + " convert module to use goog.module.declareLegacyNamespace."); static final DiagnosticType ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT = DiagnosticType.error( "JSC_ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT", "Destructuring import cannot refer to a module with default export." + " Please use standard (non-destructuring) import instead."); 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}"); private static final ImmutableSet USE_STRICT_ONLY = ImmutableSet.of("use strict"); private static final String MODULE_EXPORTS_PREFIX = "module$exports$"; private static final String MODULE_CONTENTS_PREFIX = "module$contents$"; private final AbstractCompiler compiler; private final PreprocessorSymbolTable preprocessorSymbolTable; /** * 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 { final Node requireNode; final String legacyNamespace; final boolean mustBeOrdered; UnrecognizedRequire(Node requireNode, String legacyNamespace, boolean mustBeOrdered) { this.requireNode = requireNode; this.legacyNamespace = legacyNamespace; this.mustBeOrdered = mustBeOrdered; } } 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; private static final Set INLINABLE_NAME_PARENTS = ImmutableSet.of(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, 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() { if (nameDecl == null || !INLINABLE_NAME_PARENTS.contains(nameDecl.getParentNode().getToken())) { return false; } Node initialValue = nameDecl.getInitialValue(); if (initialValue == null) { return true; } return !NodeUtil.isCallTo(initialValue, "goog.require") && !NodeUtil.isCallTo(initialValue, "goog.forwardDeclare") && !NodeUtil.isCallTo(initialValue, "goog.getMsg"); } String getLocalName() { if (hasInlinableName()) { return nameDecl.getName(); } return null; } } private static final class ScriptDescription { boolean isModule; boolean declareLegacyNamespace; String legacyNamespace; // "a.b.c" String contentsPrefix; // "module$contents$a$b$c_ final Set topLevelNames = new HashSet<>(); // For prefixed content renaming. final Deque childScripts = new LinkedList<>(); // For goog.loadModule() final Map namesToInlineByAlias = new HashMap<>(); // For alias inlining. /** * Transient state. */ boolean willCreateExportsObject; boolean hasCreatedExportObject; Node defaultExportRhs; String defaultExportLocalName; Set namedExports = new HashSet<>(); Map exportsToInline = new HashMap<>(); // The root of the module. The SCRIPT node (or for goog.loadModule, the body of the // function) that contains the module contents. For recognizing top level names. Changes when // unwrapping a goog.loadModule() call. 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 MODULE_EXPORTS_PREFIX + this.legacyNamespace.replace('.', '$'); } @Nullable String getExportedNamespace() { if (this.declareLegacyNamespace) { return this.legacyNamespace; } return this.getBinaryNamespace(); } } private class ScriptRecorder extends AbstractPreOrderCallback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isGoogModuleFile(n)) { n.putBooleanProp(Node.GOOG_MODULE, true); inlineModuleIntoGlobal(n); compiler.reportCodeChange(); checkAndSetStrictModeDirective(t, n); } switch (n.getToken()) { case CALL: if (NodeUtil.isCallTo(n, "goog.loadModule") && n.getLastChild().isFunction()) { recordGoogLoadModule(n); } if (NodeUtil.isCallTo(n, "goog.module")) { recordGoogModule(t, n); } if (NodeUtil.isCallTo(n, "goog.module.declareLegacyNamespace")) { recordGoogDeclareLegacyNamespace(); } if (NodeUtil.isCallTo(n, "goog.provide")) { recordGoogProvide(t, n); } if (NodeUtil.isCallTo(n, "goog.require")) { recordGoogRequire(t, n, true /** mustBeOrdered */); } if (NodeUtil.isCallTo(n, "goog.forwardDeclare") && !parent.isExprResult()) { recordGoogForwardDeclare(t, n); } if (NodeUtil.isCallTo(n, "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(n)) { recordExportsPropertyAssignment(t, n); } break; case STRING_KEY: // Short objects must be converted first, so that we can rewrite module-global names. if (currentScript.isModule) { rewriteShortObjectKey(n); } break; case NAME: maybeRecordExportDeclaration(t, n); break; case RETURN: if (isTopLevel(t, n, ScopeType.BLOCK)) { recordModuleReturn(); } break; default: break; } return true; } } private class ScriptUpdater implements Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case EXPR_RESULT: if (isGoogLoadModuleStatement(n)) { updateGoogLoadModuleEarly(n); } break; case CALL: if (NodeUtil.isCallTo(n, "goog.module")) { updateGoogModule(n); } if (NodeUtil.isCallTo(n, "goog.module.declareLegacyNamespace")) { updateGoogDeclareLegacyNamespace(n); } if (NodeUtil.isCallTo(n, "goog.require")) { updateGoogRequire(t, n); } if (NodeUtil.isCallTo(n, "goog.forwardDeclare") && !parent.isExprResult()) { updateGoogForwardDeclare(t, n); } if (NodeUtil.isCallTo(n, "goog.module.get")) { updateGoogModuleGetCall(n); } break; case GETPROP: if (isExportPropertyAssignment(n)) { updateExportsPropertyAssignment(n); } else if (n.isQualifiedName()) { checkQualifiedName(t, n); } break; case RETURN: if (isTopLevel(t, n, ScopeType.EXEC_CONTEXT)) { updateModuleReturn(n); } break; default: break; } if (n.getJSDocInfo() != null) { rewriteJsdoc(n.getJSDocInfo()); } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case EXPR_RESULT: if (isGoogLoadModuleStatement(n)) { updateGoogLoadModuleLate(n); } break; case SCRIPT: updateEndScript(); break; case NAME: maybeUpdateTopLevelName(t, n); maybeUpdateExportDeclaration(t, n); maybeUpdateExportNameRef(n); break; default: break; } } } private static boolean isGoogLoadModuleStatement(Node exprResult) { Preconditions.checkArgument(exprResult.isExprResult()); Node call = exprResult.getFirstChild(); return call != null && NodeUtil.isCallTo(call, "goog.loadModule") && call.getLastChild().isFunction(); } /** * Checks that imports of goog.module provided files are used correctly. */ private void checkQualifiedName(NodeTraversal t, Node qnameNode) { String qname = qnameNode.getQualifiedName(); if (rewriteState.containsModule(qname) && !rewriteState.isLegacyModule(qname)) { t.report(qnameNode, QUALIFIED_REFERENCE_TO_GOOG_MODULE, qname); } } /** * 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) { for (Node typeNode : info.getTypeNodes()) { NodeUtil.visitPreOrder(typeNode, replaceJsDocRefs, Predicates.alwaysTrue()); } } /** * 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 NodeUtil.Visitor replaceJsDocRefs = new NodeUtil.Visitor() { @Override public void visit(Node typeRefNode) { if (!typeRefNode.isString()) { return; } // A type name that might be simple like "Foo" or qualified like "foo.Bar". String typeName = typeRefNode.getString(); // Tries to rename progressively shorter type prefixes like "foo.Bar.Baz", "foo.Bar", // "foo". String prefixTypeName = typeName; String suffix = ""; do { // If the name is an alias for an imported namespace rewrite from // "{Foo}" to // "{module$exports$bar$Foo}" or // "{bar.Foo}" boolean nameIsAnAlias = currentScript.namesToInlineByAlias.containsKey(prefixTypeName); if (nameIsAnAlias) { String aliasedNamespace = currentScript.namesToInlineByAlias.get(prefixTypeName); safeSetString(typeRefNode, aliasedNamespace + suffix); return; } // 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. if (currentScript.isModule && currentScript.topLevelNames.contains(prefixTypeName)) { safeSetString(typeRefNode, currentScript.contentsPrefix + typeName); return; } 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". if (binaryNamespaceIfModule != null) { safeSetString(typeRefNode, binaryNamespaceIfModule + suffix); return; } if (prefixTypeName.contains(".")) { prefixTypeName = prefixTypeName.substring(0, prefixTypeName.lastIndexOf('.')); suffix = typeName.substring(prefixTypeName.length(), typeName.length()); } else { return; } } while (true); } }; // Per script state needed for rewriting including nested goog.loadModule() calls. private Deque scriptStack = new LinkedList<>(); private 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. static class GlobalRewriteState { private Map scriptDescriptionsByGoogModuleNamespace = new HashMap<>(); private Multimap legacyNamespacesByScriptNode = HashMultimap.create(); private Set legacyScriptNamespaces = new HashSet<>(); boolean containsModule(String legacyNamespace) { return scriptDescriptionsByGoogModuleNamespace.containsKey(legacyNamespace); } boolean isLegacyModule(String legacyNamespace) { Preconditions.checkArgument(containsModule(legacyNamespace)); return scriptDescriptionsByGoogModuleNamespace.get(legacyNamespace).declareLegacyNamespace; } @Nullable String getBinaryNamespace(String legacyNamespace) { ScriptDescription script = scriptDescriptionsByGoogModuleNamespace.get(legacyNamespace); return script == null ? null : script.getBinaryNamespace(); } @Nullable private String getExportedNamespaceOrScript(String legacyNamespace) { if (legacyScriptNamespaces.contains(legacyNamespace)) { return legacyNamespace; } ScriptDescription script = scriptDescriptionsByGoogModuleNamespace.get(legacyNamespace); return script == null ? null : script.getExportedNamespace(); } void removeRoot(Node toRemove) { if (legacyNamespacesByScriptNode.containsKey(toRemove)) { scriptDescriptionsByGoogModuleNamespace .keySet() .removeAll(legacyNamespacesByScriptNode.removeAll(toRemove)); } } } private GlobalRewriteState rewriteState; private Set legacyScriptNamespacesAndPrefixes = new HashSet<>(); private List unrecognizedRequires = new ArrayList<>(); ClosureRewriteModule( AbstractCompiler compiler, PreprocessorSymbolTable preprocessorSymbolTable, GlobalRewriteState moduleRewriteState) { this.compiler = compiler; this.preprocessorSymbolTable = preprocessorSymbolTable; this.rewriteState = moduleRewriteState != null ? moduleRewriteState : new GlobalRewriteState(); } @Override public void process(Node externs, Node root) { Deque scriptDescriptions = new LinkedList<>(); processAllFiles(scriptDescriptions, externs); processAllFiles(scriptDescriptions, root); } private void processAllFiles(Deque scriptDescriptions, Node scriptParent) { if (scriptParent == null) { return; } // 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. for (Node c = scriptParent.getFirstChild(); c != null; c = c.getNext()) { Preconditions.checkState(c.isScript()); pushScript(new ScriptDescription()); currentScript.rootNode = c; scriptDescriptions.addLast(currentScript); NodeTraversal.traverseEs6(compiler, c, 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. for (Node c = scriptParent.getFirstChild(); c != null; c = c.getNext()) { pushScript(scriptDescriptions.removeFirst()); NodeTraversal.traverseEs6(compiler, c, new ScriptUpdater()); popScript(); } } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { Preconditions.checkState(scriptRoot.isScript()); rewriteState.removeRoot(originalRoot); pushScript(new ScriptDescription()); currentScript.rootNode = scriptRoot; NodeTraversal.traverseEs6(compiler, scriptRoot, new ScriptRecorder()); if (compiler.hasHaltingErrors()) { return; } NodeTraversal.traverseEs6(compiler, scriptRoot, new ScriptUpdater()); popScript(); reportUnrecognizedRequires(); } private void recordGoogLoadModule(Node call) { pushScript(new ScriptDescription()); currentScript.rootNode = NodeUtil.getEnclosingStatement(call).getParent(); currentScript.isModule = true; } private void recordGoogModule(NodeTraversal t, Node call) { Node legacyNamespaceNode = call.getLastChild(); if (!legacyNamespaceNode.isString()) { t.report(legacyNamespaceNode, INVALID_MODULE_NAMESPACE); return; } String legacyNamespace = legacyNamespaceNode.getString(); currentScript.isModule = true; currentScript.rootNode = NodeUtil.getEnclosingStatement(call).getParent(); currentScript.legacyNamespace = legacyNamespace; currentScript.contentsPrefix = toModuleContentsPrefix(legacyNamespace); // If some other script is advertising itself as a goog.module() with this same namespace. if (rewriteState.containsModule(legacyNamespace)) { t.report(call, DUPLICATE_MODULE, legacyNamespace); } if (rewriteState.legacyScriptNamespaces.contains(legacyNamespace)) { t.report(call, DUPLICATE_NAMESPACE, legacyNamespace); } rewriteState.scriptDescriptionsByGoogModuleNamespace.put(legacyNamespace, currentScript); rewriteState.legacyNamespacesByScriptNode.put(NodeUtil.getEnclosingScript(currentScript.rootNode), legacyNamespace); } private void recordGoogDeclareLegacyNamespace() { currentScript.declareLegacyNamespace = true; } private void recordGoogProvide(NodeTraversal t, Node call) { Node legacyNamespaceNode = call.getLastChild(); if (!legacyNamespaceNode.isString()) { t.report(legacyNamespaceNode, INVALID_PROVIDE_NAMESPACE); return; } String legacyNamespace = legacyNamespaceNode.getString(); if (currentScript.isModule) { t.report(legacyNamespaceNode, INVALID_PROVIDE_CALL); } if (rewriteState.containsModule(legacyNamespace)) { t.report(call, DUPLICATE_NAMESPACE, legacyNamespace); } // Log legacy namespaces and prefixes. rewriteState.legacyScriptNamespaces.add(legacyNamespace); rewriteState.legacyNamespacesByScriptNode.put(NodeUtil.getEnclosingScript(call), legacyNamespace); LinkedList parts = Lists.newLinkedList(Splitter.on('.').split(legacyNamespace)); while (!parts.isEmpty()) { legacyScriptNamespacesAndPrefixes.add(Joiner.on('.').join(parts)); parts.removeLast(); } } private void recordGoogRequire(NodeTraversal t, Node call, boolean mustBeOrdered) { maybeSplitMultiVar(call); Node legacyNamespaceNode = call.getLastChild(); if (!legacyNamespaceNode.isString()) { t.report(legacyNamespaceNode, INVALID_REQUIRE_NAMESPACE); return; } String legacyNamespace = legacyNamespaceNode.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(legacyNamespace); boolean targetIsALegacyScript = rewriteState.legacyScriptNamespaces.contains(legacyNamespace); if (currentScript.isModule && !targetIsAModule && !targetIsALegacyScript) { unrecognizedRequires.add(new UnrecognizedRequire(call, legacyNamespace, mustBeOrdered)); } } private void recordGoogForwardDeclare(NodeTraversal t, Node call) { Node namespaceNode = call.getLastChild(); if (call.getChildCount() != 2 || !namespaceNode.isString()) { 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 recordGoogModuleGet(NodeTraversal t, Node call) { Node legacyNamespaceNode = call.getLastChild(); if (call.getChildCount() != 2 || !legacyNamespaceNode.isString()) { t.report(legacyNamespaceNode, INVALID_GET_NAMESPACE); return; } if (!currentScript.isModule && t.inGlobalScope()) { t.report(legacyNamespaceNode, INVALID_GET_CALL_SCOPE); return; } String legacyNamespace = legacyNamespaceNode.getString(); if (!rewriteState.containsModule(legacyNamespace)) { unrecognizedRequires.add( new UnrecognizedRequire(call, legacyNamespace, false /** mustBeOrderd */)); } String aliasName = null; boolean isFillingAnAlias = call.getParent().isAssign() && call.getParent().getFirstChild().isName(); if (isFillingAnAlias && currentScript.isModule) { 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) { t.report(call, INVALID_GET_ALIAS); 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")) { t.report(call, INVALID_GET_ALIAS); return; } if (!legacyNamespace.equals(aliasVarNodeRhs.getLastChild().getString())) { t.report(call, INVALID_GET_ALIAS); return; } } } 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) { for (Node lhs : NodeUtil.getLhsNodesOfDeclaration(varNode)) { String name = lhs.getString(); currentScript.topLevelNames.add(name); } } private void rewriteShortObjectKey(Node n) { Preconditions.checkArgument(n.isStringKey()); if (!n.hasChildren()) { Node nameNode = IR.name(n.getString()).srcref(n); n.addChildToBack(nameNode); } } private void maybeRecordExportDeclaration(NodeTraversal t, Node n) { if (!currentScript.isModule || !n.getString().equals("exports") || !isAssignTarget(n)) { return; } Preconditions.checkState(currentScript.defaultExportRhs == null); Node exportRhs = n.getNext(); if (isNamedExportsLiteral(exportRhs)) { boolean areAllExportsInlinable = true; List inlinableExports = new ArrayList<>(); for (Node key = exportRhs.getFirstChild(); key != null; key = key.getNext()) { String exportName = key.getString(); Node rhs = key.hasChildren() ? key.getFirstChild() : key; ExportDefinition namedExport = ExportDefinition.newNamedExport(t, exportName, rhs); currentScript.namedExports.add(exportName); if (currentScript.declareLegacyNamespace || !namedExport.hasInlinableName()) { areAllExportsInlinable = false; } else { inlinableExports.add(namedExport); } } if (areAllExportsInlinable) { for (ExportDefinition export : inlinableExports) { recordExportToInline(export); } NodeUtil.removeChild(n.getParent().getParent(), n.getParent()); } else { currentScript.willCreateExportsObject = true; } return; } currentScript.defaultExportRhs = exportRhs; currentScript.willCreateExportsObject = true; ExportDefinition defaultExport = ExportDefinition.newDefaultExport(t, exportRhs); if (!currentScript.declareLegacyNamespace && defaultExport.hasInlinableName()) { String localName = defaultExport.getLocalName(); currentScript.defaultExportLocalName = localName; recordExportToInline(defaultExport); } return; } private static boolean isNamedExportsLiteral(Node objLit) { if (!objLit.isObjectLit() || !objLit.hasChildren()) { return false; } for (Node key = objLit.getFirstChild(); key != null; key = key.getNext()) { if (!key.isStringKey() || key.isQuotedString()) { return false; } if (key.hasChildren() && !key.getFirstChild().isName()) { return false; } } return true; } private void recordModuleReturn() { popScript(); } static void inlineModuleIntoGlobal(Node scriptNode) { Preconditions.checkArgument(NodeUtil.isGoogModuleFile(scriptNode)); Node moduleNode = scriptNode.getFirstChild(); scriptNode.removeChild(moduleNode); scriptNode.addChildrenToBack(moduleNode.removeChildren()); } private void updateGoogLoadModuleEarly(Node exprResultNode) { pushScript(currentScript.removeFirstChildScript()); Node moduleScopeRoot = exprResultNode.getFirstChild().getLastChild().getLastChild(); Preconditions.checkState(NodeUtil.isModuleScopeRoot(moduleScopeRoot), "goog.loadModule called with non-module contents: %s", moduleScopeRoot); currentScript.rootNode = moduleScopeRoot; } private void updateGoogLoadModuleLate(Node exprResultNode) { Node call = exprResultNode.removeFirstChild(); Node moduleBlockNode = call.getLastChild().getLastChild(); exprResultNode.setToken(Token.BLOCK); exprResultNode.addChildrenToBack(moduleBlockNode.removeChildren()); currentScript.rootNode = exprResultNode; NodeUtil.tryMergeBlock(exprResultNode); compiler.reportCodeChange(); } private void updateGoogModule(Node call) { Preconditions.checkState(currentScript.isModule); // If it's a goog.module() with a legacy namespace. if (currentScript.declareLegacyNamespace) { // Rewrite "goog.module('Foo');" as "goog.provide('Foo');". call.getFirstChild().getLastChild().setString("provide"); } // 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) { Preconditions.checkState(!currentScript.hasCreatedExportObject); exportTheEmptyBinaryNamespaceAt(NodeUtil.getEnclosingStatement(call), AddAt.AFTER); } if (!currentScript.declareLegacyNamespace) { // Otherwise it's a regular module and the goog.module() line can be removed. NodeUtil.getEnclosingStatement(call).detach(); } Node callee = call.getFirstChild(); Node arg = callee.getNext(); maybeAddToSymbolTable(callee); maybeAddToSymbolTable(createNamespaceNode(arg)); compiler.reportCodeChange(); } private void updateGoogDeclareLegacyNamespace(Node call) { NodeUtil.getEnclosingStatement(call).detach(); } private void updateGoogRequire(NodeTraversal t, Node call) { Node legacyNamespaceNode = call.getLastChild(); Node statementNode = NodeUtil.getEnclosingStatement(call); String legacyNamespace = legacyNamespaceNode.getString(); boolean targetIsNonLegacyGoogModule = rewriteState.containsModule(legacyNamespace) && !rewriteState.isLegacyModule(legacyNamespace); 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(legacyNamespace); 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); } else if (lhs.isDestructuringLhs() && lhs.getFirstChild().isObjectPattern()) { // `const {Foo}` case maybeWarnForInvalidDestructuring(t, lhs.getParent(), legacyNamespace); for (Node importSpec : lhs.getFirstChild().children()) { String importedProperty = importSpec.getString(); String aliasName = importSpec.hasChildren() ? importSpec.getFirstChild().getString() : importedProperty; String fullName = exportedNamespace + "." + importedProperty; recordNameToInline(aliasName, fullName); } } else { throw new RuntimeException("Illegal goog.module import: " + lhs); } } if (currentScript.isModule || targetIsNonLegacyGoogModule) { if (isDestructuring) { // Delete the goog.require() because we're going to inline its alias later. statementNode.detach(); } else if (targetIsNonLegacyGoogModule) { if (!isTopLevel(t, statementNode, ScopeType.EXEC_CONTEXT)) { // Rewrite // "function() {var Foo = goog.require("bar.Foo");}" to // "function() {var Foo = module$exports$bar$Foo;}" Node binaryNamespaceName = IR.name(rewriteState.getBinaryNamespace(legacyNamespace)); binaryNamespaceName.putProp(Node.ORIGINALNAME_PROP, legacyNamespace); call.replaceWith(binaryNamespaceName); } else if (importHasAlias || !rewriteState.isLegacyModule(legacyNamespace)) { // Delete the goog.require() because we're going to inline its alias later. statementNode.detach(); } } else { // 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)); } if (targetIsNonLegacyGoogModule) { // Add goog.require() and namespace name to preprocessor table because they're removed // by current pass. If target is not a module then goog.require() is retained for // ProcessClosurePrimitives pass and symbols will be added there instead. Node callee = call.getFirstChild(); Node arg = callee.getNext(); maybeAddToSymbolTable(callee); maybeAddToSymbolTable(createNamespaceNode(arg)); } compiler.reportCodeChange(); } } // 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) { Preconditions.checkArgument(importNode.getFirstChild().isDestructuringLhs()); ScriptDescription importedModule = rewriteState.scriptDescriptionsByGoogModuleNamespace.get(importedNamespace); if (importedModule == null) { // Don't know enough to give a good warning here. return; } if (importedModule.defaultExportRhs != 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 // legacyNamespace import categorization, goog.forwardDeclare is much the same as goog.require. updateGoogRequire(t, call); } private void updateGoogModuleGetCall(Node call) { Node legacyNamespaceNode = call.getSecondChild(); String legacyNamespace = legacyNamespaceNode.getString(); if (currentScript.isModule) { // In a module each goog.module.get() will have a paired goog.forwardDeclare() and the module // alias importing will be handled there so that the alias is available in the entire module // scope. NodeUtil.getEnclosingStatement(call).detach(); } else { // In a non-module script goog.module.get() is used inside of a goog.scope() and it's // imported alias need only be made available within that scope. // Replace "goog.module.get('pkg.Foo');" with "module$exports$pkg$Foo;". String exportedNamespace = rewriteState.getExportedNamespaceOrScript(legacyNamespace); Node exportedNamespaceName = NodeUtil.newQName(compiler, exportedNamespace).srcrefTree(call); exportedNamespaceName.putProp(Node.ORIGINALNAME_PROP, legacyNamespace); call.replaceWith(exportedNamespaceName); } compiler.reportCodeChange(); } private void recordExportsPropertyAssignment(NodeTraversal t, Node getpropNode) { if (!currentScript.isModule) { return; } Node parent = getpropNode.getParent(); Preconditions.checkState(parent.isAssign() || parent.isExprResult(), parent); Node exportsNameNode = getpropNode.getFirstChild(); Preconditions.checkState(exportsNameNode.getString().equals("exports")); // Would be just t.inModuleScope() if this ran before the inlineModuleIntoGlobal() call // that happens at the beginning of module rewriting. if (t.inModuleScope() || t.inGlobalScope()) { String exportName = getpropNode.getLastChild().getString(); currentScript.namedExports.add(exportName); Node exportRhs = getpropNode.getNext(); ExportDefinition namedExport = ExportDefinition.newNamedExport(t, exportName, exportRhs); if (!currentScript.declareLegacyNamespace && currentScript.defaultExportRhs == null && namedExport.hasInlinableName()) { recordExportToInline(namedExport); parent.getParent().detach(); } } } private void updateExportsPropertyAssignment(Node getpropNode) { if (!currentScript.isModule) { return; } Node parent = getpropNode.getParent(); Preconditions.checkState(parent.isAssign() || parent.isExprResult(), parent); // Update "exports.foo = Foo" to "module$exports$pkg$Foo.foo = Foo"; Node exportsNameNode = getpropNode.getFirstChild(); Preconditions.checkState(exportsNameNode.getString().equals("exports")); String exportedNamespace = currentScript.getExportedNamespace(); safeSetMaybeQualifiedString(exportsNameNode, exportedNamespace); Node jsdocNode = parent.isAssign() ? parent : getpropNode; markConstAndCopyJsDoc(jsdocNode, jsdocNode, parent.getLastChild()); // 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); } } /** * 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")) { return; } } // If the name is an alias for an imported namespace rewrite from // "new Foo;" to "new module$exports$Foo;" boolean nameIsAnAlias = currentScript.namesToInlineByAlias.containsKey(name); if (nameIsAnAlias && var.getNode() != nameNode) { String namespaceToInline = currentScript.namesToInlineByAlias.get(name); if (namespaceToInline.equals(currentScript.getBinaryNamespace())) { currentScript.hasCreatedExportObject = true; } safeSetMaybeQualifiedString(nameNode, namespaceToInline); // 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.isLocal()) { 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). */ 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()) { if (!c.hasChildren()) { c.addChildToBack(IR.name(c.getString()).useSourceInfoFrom(c)); } 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) { Scope varScope = v.getScope(); if (varScope.getDepth() == currentScope.getDepth()) { JSDocInfo info = v.getJSDocInfo(); if (info != null && info.hasTypedefType()) { JSDocInfoBuilder builder = JSDocInfoBuilder.copyFrom(info); target.setJSDocInfo(builder.build()); return; } } } } markConstAndCopyJsDoc(target, target, value); } /** * In module "foo.Bar", rewrite "exports = Bar" to "var module$exports$foo$Bar = Bar". */ private void maybeUpdateExportDeclaration(NodeTraversal t, Node n) { if (!currentScript.isModule || !n.getString().equals("exports") || !isAssignTarget(n)) { return; } Node assignNode = n.getParent(); if (!currentScript.declareLegacyNamespace && currentScript.defaultExportLocalName != null) { assignNode.getParent().detach(); return; } // Rewrite "exports = ..." as "var module$exports$foo$Bar = ..." Node rhs = assignNode.getLastChild(); Node jsdocNode; if (currentScript.declareLegacyNamespace) { Node legacyQname = NodeUtil.newQName(compiler, currentScript.legacyNamespace).srcrefTree(n); assignNode.replaceChild(n, legacyQname); jsdocNode = assignNode; } else { rhs.detach(); Node exprResultNode = assignNode.getParent(); Node binaryNamespaceName = IR.name(currentScript.getBinaryNamespace()); binaryNamespaceName.putProp(Node.ORIGINALNAME_PROP, currentScript.legacyNamespace); Node exportsObjectCreationNode = IR.var(binaryNamespaceName, rhs); exportsObjectCreationNode.useSourceInfoIfMissingFromForTree(exprResultNode); exportsObjectCreationNode.putBooleanProp(Node.IS_NAMESPACE, true); exprResultNode.replaceWith(exportsObjectCreationNode); jsdocNode = exportsObjectCreationNode; currentScript.hasCreatedExportObject = true; } markConstAndCopyJsDoc(assignNode, jsdocNode, rhs); compiler.reportCodeChange(); maybeUpdateExportObjectLiteral(t, rhs); return; } private void maybeUpdateExportNameRef(Node n) { if (!currentScript.isModule || !"exports".equals(n.getString()) || n.getParent() == null) { return; } if (n.getParent().isParamList()) { return; } if (currentScript.declareLegacyNamespace) { Node legacyQname = NodeUtil.newQName(compiler, currentScript.legacyNamespace).srcrefTree(n); n.replaceWith(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. Preconditions.checkState( currentScript.willCreateExportsObject || currentScript.hasCreatedExportObject); } private void updateModuleReturn(Node returnNode) { if (!currentScript.isModule) { return; } Node returnStatementNode = NodeUtil.getEnclosingStatement(returnNode); if (!currentScript.hasCreatedExportObject) { exportTheEmptyBinaryNamespaceAt(returnStatementNode, AddAt.BEFORE); } returnStatementNode.detach(); updateEndModule(); popScript(); } private void updateEndScript() { if (!currentScript.isModule) { return; } updateEndModule(); } private void updateEndModule() { for (ExportDefinition export : currentScript.exportsToInline.values()) { Node nameNode = export.nameDecl.getNameNode(); safeSetMaybeQualifiedString( nameNode, currentScript.getBinaryNamespace() + export.getExportPostfix()); } Preconditions.checkState(currentScript.isModule); Preconditions.checkState( currentScript.declareLegacyNamespace || currentScript.hasCreatedExportObject); } /** * 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, modules, and nested goog.loadModule() 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) { if (currentScript.declareLegacyNamespace) { return; } Node binaryNamespaceName = IR.name(currentScript.getBinaryNamespace()); binaryNamespaceName.putProp(Node.ORIGINALNAME_PROP, currentScript.legacyNamespace); Node binaryNamespaceExportNode = IR.var(binaryNamespaceName, IR.objectlit()); if (addAt == AddAt.BEFORE) { atNode.getParent().addChildBefore(binaryNamespaceExportNode, atNode); } else if (addAt == AddAt.AFTER) { atNode.getParent().addChildAfter(binaryNamespaceExportNode, atNode); } binaryNamespaceExportNode.putBooleanProp(Node.IS_NAMESPACE, true); binaryNamespaceExportNode.srcrefTree(atNode); markConst(binaryNamespaceExportNode); compiler.reportCodeChange(); currentScript.hasCreatedExportObject = true; } static void checkAndSetStrictModeDirective(NodeTraversal t, Node n) { Preconditions.checkState(n.isScript(), n); Set directives = n.getDirectives(); if (directives != null && directives.contains("use strict")) { t.report(n, USELESS_USE_STRICT_DIRECTIVE); } else { if (directives == null) { n.setDirectives(USE_STRICT_ONLY); } else { ImmutableSet.Builder builder = new ImmutableSet.Builder().add("use strict"); builder.addAll(directives); n.setDirectives(builder.build()); } } } private void markConst(Node n) { JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo()); builder.recordConstancy(); n.setJSDocInfo(builder.build()); } private void maybeSplitMultiVar(Node rhsNode) { Node statementNode = rhsNode.getParent().getParent(); if (!statementNode.isVar() || !statementNode.hasMoreThanOneChild()) { return; } Node nameNode = rhsNode.getParent(); nameNode.detach(); rhsNode.detach(); statementNode.getParent().addChildBefore(IR.var(nameNode, rhsNode), statementNode); } private void markConstAndCopyJsDoc(Node from, Node target, Node value) { JSDocInfo info = from.getJSDocInfo(); JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(info); builder.recordConstancy(); target.setJSDocInfo(builder.build()); } private void recordExportToInline(ExportDefinition exportDefinition) { currentScript.exportsToInline.put(exportDefinition.nameDecl, exportDefinition); String localName = exportDefinition.getLocalName(); String fullExportedName = currentScript.getBinaryNamespace() + exportDefinition.getExportPostfix(); recordNameToInline(localName, fullExportedName); } private void recordNameToInline(String aliasName, String legacyNamespace) { Preconditions.checkNotNull(aliasName); Preconditions.checkNotNull(legacyNamespace); Preconditions.checkState( null == currentScript.namesToInlineByAlias.put(aliasName, legacyNamespace), "Already found a mapping for inlining short name: %s", aliasName); } /** * 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 legacyNamespace = unrecognizedRequire.legacyNamespace; Node requireNode = unrecognizedRequire.requireNode; boolean targetGoogModuleExists = rewriteState.containsModule(legacyNamespace); boolean targetLegacyScriptExists = rewriteState.legacyScriptNamespaces.contains(legacyNamespace); if (!targetGoogModuleExists && !targetLegacyScriptExists) { // The required thing was free to be either a goog.module() or a legacy script but neither // flavor of file provided the required namespace, so report a vague error. compiler.report( JSError.make( requireNode, MISSING_MODULE_OR_PROVIDE.level, MISSING_MODULE_OR_PROVIDE, legacyNamespace)); // Remove the require node so this problem isn't reported all over again in // ProcessClosurePrimitives. NodeUtil.getEnclosingStatement(requireNode).detach(); continue; } // The required thing actually was available somewhere in the program but just wasn't // available as early as the require statement would have liked. if (unrecognizedRequire.mustBeOrdered) { compiler.report( JSError.make( requireNode, LATE_PROVIDE_ERROR.level, LATE_PROVIDE_ERROR, legacyNamespace)); } } // Clear the queue so that repeated reportUnrecognizedRequires() invocations in hotswap compiles // only report new problems. unrecognizedRequires.clear(); } private void safeSetString(Node n, String newString) { String originalName = n.getString(); n.setString(newString); if (n.getOriginalName() == null) { n.setOriginalName(originalName); } compiler.reportCodeChange(); } private void safeSetMaybeQualifiedString(Node nameNode, String newString) { if (!newString.contains(".")) { safeSetString(nameNode, newString); return; } // When replacing with a dotted fully qualified name it's already better than an original // name. Node nameParent = nameNode.getParent(); JSDocInfo jsdoc = nameParent.getJSDocInfo(); switch (nameParent.getToken()) { case FUNCTION: case CLASS: if (NodeUtil.isStatement(nameParent) && nameParent.getFirstChild() == nameNode) { Node statementParent = nameParent.getParent(); Node placeholder = IR.empty(); statementParent.replaceChild(nameParent, placeholder); Node newStatement = NodeUtil.newQNameDeclaration(compiler, newString, nameParent, jsdoc); nameParent.setJSDocInfo(null); newStatement.useSourceInfoIfMissingFromForTree(nameParent); statementParent.replaceChild(placeholder, newStatement); NodeUtil.removeName(nameParent); return; } break; case VAR: case LET: case CONST: { Node rhs = nameNode.hasChildren() ? nameNode.getLastChild().detach() : null; Node newStatement = NodeUtil.newQNameDeclaration(compiler, newString, rhs, jsdoc); newStatement.useSourceInfoIfMissingFromForTree(nameParent); NodeUtil.replaceDeclarationChild(nameNode, newStatement); return; } case OBJECT_PATTERN: case ARRAY_PATTERN: case PARAM_LIST: throw new RuntimeException("Not supported"); default: break; } Node newQualifiedNameNode = NodeUtil.newQName(compiler, newString); newQualifiedNameNode.srcrefTree(nameNode); nameParent.replaceChild(nameNode, newQualifiedNameNode); compiler.reportCodeChange(); } private boolean isTopLevel(NodeTraversal t, Node n, ScopeType scopeType) { if (scopeType == ScopeType.EXEC_CONTEXT) { return t.getClosestHoistScope().getRootNode() == currentScript.rootNode; } else { // Must be ScopeType.BLOCK; return n.getParent() == currentScript.rootNode; } } private static String toModuleContentsPrefix(String legacyNamespace) { return MODULE_CONTENTS_PREFIX + legacyNamespace.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); } /** * @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(Node n) { Node target = n.getFirstChild(); return (isAssignTarget(n) || isTypedefTarget(n)) && target.isName() && target.getString().equals("exports"); } 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 the given qualified name node to the symbol table. */ private void maybeAddToSymbolTable(Node n) { if (preprocessorSymbolTable != null) { preprocessorSymbolTable.addReference(n); } } /** * @param n String node containing goog.module namespace. * @return A NAMESPACE node with the same name and source info as provided node. */ private static Node createNamespaceNode(Node n) { Node node = Node.newString(n.getString()).useSourceInfoFrom(n); node.putBooleanProp(Node.IS_MODULE_NAME, true); return node; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy