diff --git a/src/com/google/javascript/jscomp/CheckMissingAndExtraRequires.java b/src/com/google/javascript/jscomp/CheckMissingAndExtraRequires.java index 85efdff7b25..12f5e5ad9d9 100644 --- a/src/com/google/javascript/jscomp/CheckMissingAndExtraRequires.java +++ b/src/com/google/javascript/jscomp/CheckMissingAndExtraRequires.java @@ -314,10 +314,11 @@ private void visitScriptNode(NodeTraversal t) { private boolean isMissingRequire(String namespace, Node node) { if (namespace.startsWith("goog.global.") // Most functions in base.js are goog.someName, but - // goog.module.{get,declareLegacyNamespace} are the exceptions, so just check for them - // explicitly. + // goog.module.{get,declareLegacyNamespace,declareNamespace} are the exceptions, so just + // check for them explicitly. || namespace.equals("goog.module.get") - || namespace.equals("goog.module.declareLegacyNamespace")) { + || namespace.equals("goog.module.declareLegacyNamespace") + || namespace.equals("goog.module.declareNamespace")) { return false; } diff --git a/src/com/google/javascript/jscomp/ClosureCheckModule.java b/src/com/google/javascript/jscomp/ClosureCheckModule.java index 2147ee35243..84ef9f54555 100644 --- a/src/com/google/javascript/jscomp/ClosureCheckModule.java +++ b/src/com/google/javascript/jscomp/ClosureCheckModule.java @@ -67,10 +67,11 @@ public final class ClosureCheckModule extends AbstractModuleCallback "JSC_GOOG_MODULE_USES_THROW", "The body of a goog.module cannot use 'throw'."); - static final DiagnosticType GOOG_MODULE_USES_GOOG_MODULE_GET = DiagnosticType.error( - "JSC_GOOG_MODULE_USES_GOOG_MODULE_GET", - "It's illegal to use a 'goog.module.get' at the module top-level." - + " Did you mean to use goog.require instead?"); + static final DiagnosticType MODULE_USES_GOOG_MODULE_GET = + DiagnosticType.error( + "JSC_MODULE_USES_GOOG_MODULE_GET", + "It's illegal to use a 'goog.module.get' at the module top-level." + + " Did you mean to use goog.require instead?"); static final DiagnosticType DUPLICATE_NAME_SHORT_REQUIRE = DiagnosticType.error( @@ -159,6 +160,11 @@ public final class ClosureCheckModule extends AbstractModuleCallback "JSC_REQUIRE_NOT_AT_TOP_LEVEL", "goog.require() must be called at file scope."); + static final DiagnosticType DECLARE_LEGACY_NAMESPACE_OUTSIDE_GOOG_MODULE = + DiagnosticType.error( + "JSC_DECLARE_LEGACY_NAMESPACE_OUTSIDE_GOOG_MODULE", + "goog.module.declareLegacyNamespace can only be called in goog.modules."); + private final AbstractCompiler compiler; private static class ModuleInfo { @@ -242,7 +248,7 @@ public void visit(NodeTraversal t, Node n, Node parent) { || callee.matchesQualifiedName("goog.forwardDeclare")) { checkRequireCall(t, n, parent); } else if (callee.matchesQualifiedName("goog.module.get") && t.inModuleHoistScope()) { - t.report(n, GOOG_MODULE_USES_GOOG_MODULE_GET); + t.report(n, MODULE_USES_GOOG_MODULE_GET); } break; case ASSIGN: { diff --git a/src/com/google/javascript/jscomp/ClosureRewriteModule.java b/src/com/google/javascript/jscomp/ClosureRewriteModule.java index d384792688e..590e350bc57 100644 --- a/src/com/google/javascript/jscomp/ClosureRewriteModule.java +++ b/src/com/google/javascript/jscomp/ClosureRewriteModule.java @@ -331,7 +331,7 @@ public ScriptDescription removeFirstChildScript() { if (!this.isModule || this.declareLegacyNamespace) { return null; } - return MODULE_EXPORTS_PREFIX + this.legacyNamespace.replace('.', '$'); + return getBinaryModuleNamespace(legacyNamespace); } @Nullable @@ -343,6 +343,10 @@ String getExportedNamespace() { } } + static String getBinaryModuleNamespace(String legacyNamespace) { + return MODULE_EXPORTS_PREFIX + legacyNamespace.replace('.', '$'); + } + private class ScriptPreprocessor extends NodeTraversal.AbstractPreOrderCallback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { diff --git a/src/com/google/javascript/jscomp/CompilerInput.java b/src/com/google/javascript/jscomp/CompilerInput.java index 65276f7e404..62609562b38 100644 --- a/src/com/google/javascript/jscomp/CompilerInput.java +++ b/src/com/google/javascript/jscomp/CompilerInput.java @@ -394,6 +394,14 @@ void visitSubtree(Node n, Node parent) { default: return; } + } else if (parent.isGetProp() + && parent.matchesQualifiedName("goog.module.declareNamespace") + && parent.getParent().isCall()) { + Node argument = parent.getParent().getSecondChild(); + if (!argument.isString()) { + return; + } + provides.add(argument.getString()); } break; diff --git a/src/com/google/javascript/jscomp/Es6RewriteModules.java b/src/com/google/javascript/jscomp/Es6RewriteModules.java index e1e2a3f0844..fbe1ba75a5e 100644 --- a/src/com/google/javascript/jscomp/Es6RewriteModules.java +++ b/src/com/google/javascript/jscomp/Es6RewriteModules.java @@ -16,16 +16,27 @@ package com.google.javascript.jscomp; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.javascript.jscomp.ClosureCheckModule.MODULE_USES_GOOG_MODULE_GET; +import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_GET_CALL_SCOPE; +import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_GET_NAMESPACE; +import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_REQUIRE_NAMESPACE; +import static com.google.javascript.jscomp.ClosureRewriteModule.MISSING_MODULE_OR_PROVIDE; +import static com.google.javascript.jscomp.ProcessClosurePrimitives.INVALID_CLOSURE_CALL_ERROR; import static com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature.MODULES; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; +import com.google.javascript.jscomp.ModuleMetadata.Module; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.deps.ModuleLoader; +import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSDocInfoBuilder; @@ -64,6 +75,34 @@ public final class Es6RewriteModules extends AbstractPostOrderCallback static final DiagnosticType DUPLICATE_EXPORT = DiagnosticType.error("JSC_DUPLICATE_EXPORT", "Duplicate export ''{0}''."); + static final DiagnosticType PATH_REQUIRE_FOR_NON_ES6_MODULE = + DiagnosticType.error( + "JSC_PATH_REQUIRE_FOR_NON_ES6_MODULE", + "Cannot goog.require ''{0}'' by path, it is not an ES6 module.{0}"); + + static final DiagnosticType PATH_REQUIRE_IN_NON_GOOG_MODULE = + DiagnosticType.error( + "JSC_PATH_REQUIRE_IN_NON_GOOG_MODULE", + "Cannot goog.require by path outside of goog.modules."); + + static final DiagnosticType PATH_REQUIRE_IN_GOOG_PROVIDE_FILE = + DiagnosticType.error( + "JSC_PATH_REQUIRE_IN_GOOG_PROVIDE_FILE", + "goog.provide'd files can only goog.require namespaces. goog.require by path is only " + + "valid in goog.modules."); + + static final DiagnosticType PATH_REQUIRE_IN_ES6_MODULE = + DiagnosticType.error( + "JSC_PATH_REQUIRE_IN_ES6_MODULE", + "Import other ES6 modules and goog.require namespaces in ES6 modules. Do not " + + "goog.require paths in ES6 modules."); + + static final DiagnosticType PATH_REQUIRE_IN_GOOG_MODULE_WITH_NO_PATH = + DiagnosticType.error( + "JSC_PATH_REQUIRE_IN_GOOG_MODULE_WITH_NO_PATH", + "The goog.module {0} cannot require by path. It has been loaded without path " + + "information (is the path argument to goog.loadModule provided?)."); + private final AbstractCompiler compiler; @Nullable private final PreprocessorSymbolTable preprocessorSymbolTable; @@ -84,17 +123,26 @@ public final class Es6RewriteModules extends AbstractPostOrderCallback */ private Map importMap; + private final Set importedGoogNames; + private Set classes; private Set typedefs; + private final ModuleMetadata moduleMetadata; + /** * Creates a new Es6RewriteModules instance which can be used to rewrite ES6 modules to a * concatenable form. */ public Es6RewriteModules( - AbstractCompiler compiler, @Nullable PreprocessorSymbolTable preprocessorSymbolTable) { + AbstractCompiler compiler, + @Nullable PreprocessorSymbolTable preprocessorSymbolTable, + boolean processCommonJsModules, + ResolutionMode moduleResolutionMode) { this.compiler = compiler; this.preprocessorSymbolTable = preprocessorSymbolTable; + moduleMetadata = new ModuleMetadata(compiler, processCommonJsModules, moduleResolutionMode); + this.importedGoogNames = new HashSet<>(); } /** @@ -112,6 +160,7 @@ public static boolean isEs6ModuleRoot(Node scriptNode) { public void process(Node externs, Node root) { checkArgument(externs.isRoot(), externs); checkArgument(root.isRoot(), root); + moduleMetadata.process(externs, root); for (Node file : Iterables.concat(externs.children(), root.children())) { checkState(file.isScript(), file); hotSwapScript(file, null); @@ -121,8 +170,11 @@ public void process(Node externs, Node root) { @Override public void hotSwapScript(Node scriptNode, Node originalRoot) { + moduleMetadata.hotSwapScript(scriptNode); if (isEs6ModuleRoot(scriptNode)) { processFile(scriptNode); + } else { + NodeTraversal.traverseEs6(compiler, scriptNode, new RewriteRequiresForEs6Modules()); } } @@ -139,45 +191,133 @@ public void clearState() { this.scriptNodeCount = 0; this.exportsByLocalName = LinkedHashMultimap.create(); this.importMap = new HashMap<>(); + this.importedGoogNames.clear(); this.classes = new HashSet<>(); this.typedefs = new HashSet<>(); } /** - * Avoid processing if we find the appearance of goog.provide or goog.module. - * - *

TODO(moz): Let ES6, CommonJS and goog.provide live happily together. + * Checks for goog.require + goog.module.get calls in non-ES6 modules that are meant to import ES6 + * modules and rewrites them. */ - static class FindGoogProvideOrGoogModule extends NodeTraversal.AbstractPreOrderCallback { + private class RewriteRequiresForEs6Modules extends AbstractPostOrderCallback { + @Override + public void visit(NodeTraversal t, Node n, Node parent) { + if (n.isCall()) { + boolean isRequire = n.getFirstChild().matchesQualifiedName("goog.require"); + boolean isGet = n.getFirstChild().matchesQualifiedName("goog.module.get"); + if (isRequire || isGet) { + if (!n.hasTwoChildren() || !n.getLastChild().isString()) { + t.report(n, isRequire ? INVALID_REQUIRE_NAMESPACE : INVALID_GET_NAMESPACE); + return; + } - private boolean found; + String name = n.getLastChild().getString(); + Module data = moduleMetadata.getModulesByGoogNamespace().get(name); - boolean isFound() { - return found; + boolean isPathRequire = false; + + // Can only goog.module.get by namespace. + if (data == null && isRequire && ModuleLoader.isRelativeIdentifier(name)) { + data = resolvePathRequire(t, n); + isPathRequire = true; + } + + if (data == null || !data.isEs6Module()) { + return; + } + + if (isGet && t.inGlobalHoistScope()) { + t.report(n, INVALID_GET_CALL_SCOPE); + return; + } + + Node statementNode = NodeUtil.getEnclosingStatement(n); + boolean importHasAlias = NodeUtil.isNameDeclaration(statementNode); + if (importHasAlias) { + // const module = goog.require('./es6.js'); + // const module = module$es6; + // + // const module = goog.require('an.es6.namespace'); + // const module = module$es6; + n.replaceWith( + IR.name(isPathRequire ? data.getGlobalName() : data.getGlobalName(name)) + .useSourceInfoFromForTree(n)); + t.reportCodeChange(); + } else { + if (parent.isExprResult()) { + parent.detach(); + } + n.detach(); + t.reportCodeChange(); + } + } + } } - @Override - public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { - if (found) { - return false; + @Nullable + private Module resolvePathRequire(NodeTraversal t, Node requireCall) { + String name = requireCall.getLastChild().getString(); + Module thisModule = moduleMetadata.getContainingModule(requireCall); + checkNotNull(thisModule); + + if (!thisModule.isGoogModule()) { + t.report( + requireCall, + thisModule.isGoogProvide() + ? PATH_REQUIRE_IN_GOOG_PROVIDE_FILE + : PATH_REQUIRE_IN_NON_GOOG_MODULE); + return null; } - // Shallow traversal, since we don't need to inspect within functions or expressions. - if (parent == null - || NodeUtil.isControlStructure(parent) - || NodeUtil.isStatementBlock(parent)) { - if (n.isExprResult()) { - Node maybeGetProp = n.getFirstFirstChild(); - if (maybeGetProp != null - && (maybeGetProp.matchesQualifiedName("goog.provide") - || maybeGetProp.matchesQualifiedName("goog.module"))) { - found = true; - return false; - } + + if (thisModule.getPath() == null) { + t.report( + requireCall, + PATH_REQUIRE_IN_GOOG_MODULE_WITH_NO_PATH, + Iterables.getOnlyElement(thisModule.getGoogNamespaces())); + return null; + } + + ModulePath path = + thisModule + .getPath() + .resolveJsModule( + name, t.getSourceName(), requireCall.getLineno(), requireCall.getCharno()); + + // If path is null then resolveJsModule has logged an error. + if (path != null) { + Module requiredModule = moduleMetadata.getModulesByPath().get(path.toString()); + checkNotNull(requiredModule); + if (!requiredModule.isEs6Module()) { + reportPathRequireForNonEs6Module(requiredModule, t, requireCall); } - return true; + return requiredModule; } - return false; + return null; + } + } + + private void reportPathRequireForNonEs6Module( + Module requiredModule, NodeTraversal t, Node requireCall) { + String suggestion = ""; + + if (requiredModule.getGoogNamespaces().size() == 1) { + suggestion = + " Did you mean to goog.require " + + Iterables.getOnlyElement(requiredModule.getGoogNamespaces()) + + "?"; + } else if (requiredModule.getGoogNamespaces().size() > 1) { + suggestion = + " Did you mean to goog.require one of (" + + Joiner.on(", ").join(requiredModule.getGoogNamespaces()) + + ")?"; } + + t.report( + requireCall, + PATH_REQUIRE_FOR_NON_ES6_MODULE, + t.getInput().getPath().toString(), + suggestion); } @Override @@ -191,6 +331,10 @@ public void visit(NodeTraversal t, Node n, Node parent) { } else if (n.isScript()) { scriptNodeCount++; visitScript(t, n); + } else if (n.isCall()) { + if (n.getFirstChild().matchesQualifiedName("goog.module.declareNamespace")) { + n.getParent().detach(); + } } } @@ -209,8 +353,16 @@ private void visitImport(NodeTraversal t, Node importDecl, Node parent) { if (isNamespaceImport) { // Allow importing Closure namespace objects (e.g. from goog.provide or goog.module) as // import ... from 'goog:my.ns.Object'. - // These are rewritten to plain namespace object accesses. - moduleName = importName.substring("goog:".length()); + String namespace = importName.substring("goog:".length()); + moduleName = namespace; + Module m = moduleMetadata.getModulesByGoogNamespace().get(namespace); + + if (m == null) { + t.report(importDecl, MISSING_MODULE_OR_PROVIDE, namespace); + } else { + moduleName = m.getGlobalName(namespace); + checkState(m.isEs6Module() || m.isGoogModule() || m.isGoogProvide()); + } } else { ModuleLoader.ModulePath modulePath = t.getInput() @@ -228,6 +380,8 @@ private void visitImport(NodeTraversal t, Node importDecl, Node parent) { moduleName = modulePath.toModuleName(); maybeAddImportedFileReferenceToSymbolTable(importDecl.getLastChild(), modulePath.toString()); + // TODO(johnplaisted): Use ModuleMetadata to ensure the path required is CommonJs or ES6 and + // if not give a better error. } for (Node child : importDecl.children()) { @@ -418,9 +572,8 @@ private void visitScript(NodeTraversal t, Node script) { scriptNodeCount == 1, "Es6RewriteModules supports only one invocation per " + "CompilerInput / script node"); - // rewriteRequires is here (rather than being part of the main visit() - // method, because we only want to rewrite the requires if this is an - // ES6 module. + // rewriteRequires is here (rather than being part of the main visit() method, because we only + // want to rewrite the requires if this is an ES6 module. rewriteRequires(script); String moduleName = t.getInput().getPath().toModuleName(); @@ -527,35 +680,73 @@ private void rewriteRequires(Node script) { NodeTraversal.traverseEs6( compiler, script, - new NodeTraversal.AbstractShallowCallback() { + new AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { - if (n.isCall() - && n.getFirstChild().matchesQualifiedName("goog.require") - && NodeUtil.isNameDeclaration(parent.getParent())) { - visitRequire(n, parent); + if (n.isCall()) { + if (n.getFirstChild().matchesQualifiedName("goog.require")) { + visitRequire(t, n, parent, true /* checkScope */); + } else if (n.getFirstChild().matchesQualifiedName("goog.module.get")) { + visitGoogModuleGet(t, n, parent); + } + } + } + + private void visitGoogModuleGet(NodeTraversal t, Node getCall, Node parent) { + if (!getCall.hasTwoChildren() || !getCall.getLastChild().isString()) { + t.report(getCall, INVALID_GET_NAMESPACE); + return; + } + + // Module has already been turned into a script at this point. + if (t.inGlobalHoistScope()) { + t.report(getCall, MODULE_USES_GOOG_MODULE_GET); + return; } + + visitRequire(t, getCall, parent, false /* checkScope */); } - /** - * Rewrites - * const foo = goog.require('bar.foo'); - * to - * goog.require('bar.foo'); - * const foo = bar.foo; - */ - private void visitRequire(Node requireCall, Node parent) { + private void visitRequire( + NodeTraversal t, Node requireCall, Node parent, boolean checkScope) { + if (!requireCall.hasTwoChildren() || !requireCall.getLastChild().isString()) { + t.report(requireCall, INVALID_REQUIRE_NAMESPACE); + return; + } + + // Module has already been turned into a script at this point. + if (checkScope && !t.getScope().isGlobal()) { + t.report(requireCall, INVALID_CLOSURE_CALL_ERROR); + return; + } + String namespace = requireCall.getLastChild().getString(); - if (!parent.getParent().isConst()) { + + boolean isStoredInDeclaration = NodeUtil.isDeclaration(parent.getParent()); + + if (isStoredInDeclaration && !parent.getParent().isConst()) { compiler.report(JSError.make(parent.getParent(), LHS_OF_GOOG_REQUIRE_MUST_BE_CONST)); } - Node replacement = NodeUtil.newQName(compiler, namespace).srcrefTree(requireCall); - parent.replaceChild(requireCall, replacement); - Node varNode = parent.getParent(); - varNode.getParent().addChildBefore( - IR.exprResult(requireCall).srcrefTree(requireCall), - varNode); + Module m = moduleMetadata.getModulesByGoogNamespace().get(namespace); + + if (m == null) { + if (ModuleLoader.isRelativeIdentifier(namespace)) { + t.report(requireCall, PATH_REQUIRE_IN_ES6_MODULE, namespace); + } else { + t.report(requireCall, MISSING_MODULE_OR_PROVIDE, namespace); + } + return; + } + + if (isStoredInDeclaration) { + Node replacement = + NodeUtil.newQName(compiler, m.getGlobalName(namespace)).srcrefTree(requireCall); + parent.replaceChild(requireCall, replacement); + } else { + checkState(requireCall.getParent().isExprResult()); + requireCall.getParent().detach(); + } } }); } @@ -620,13 +811,14 @@ public void visit(NodeTraversal t, Node n, Node parent) { } Var var = t.getScope().getVar(name); - if (var != null && var.isGlobal()) { + if (var != null && var.isGlobal() && !importedGoogNames.contains(var.getNameNode())) { // Avoid polluting the global namespace. String newName = name + "$$" + suffix; n.setString(newName); n.setOriginalName(name); t.reportCodeChange(n); - } else if (var == null && importMap.containsKey(name)) { + } else if ((var == null || importedGoogNames.contains(var.getNameNode())) + && importMap.containsKey(name)) { // Change to property access on the imported module object. if (parent.isCall() && parent.getFirstChild() == n) { parent.putBooleanProp(Node.FREE_CALL, false); @@ -694,9 +886,10 @@ private void fixTypeNode(NodeTraversal t, Node typeNode) { rest = "." + splitted.get(1); } Var var = t.getScope().getVar(baseName); - if (var != null && var.isGlobal()) { + if (var != null && var.isGlobal() && !importedGoogNames.contains(var.getNameNode())) { maybeSetNewName(t, typeNode, name, baseName + "$$" + suffix + rest); - } else if (var == null && importMap.containsKey(baseName)) { + } else if ((var == null || importedGoogNames.contains(var.getNameNode())) + && importMap.containsKey(baseName)) { ModuleOriginalNamePair pair = importMap.get(baseName); maybeAddAliasToSymbolTable(typeNode, t.getSourceName()); if (pair.originalName.isEmpty()) { diff --git a/src/com/google/javascript/jscomp/FindModuleDependencies.java b/src/com/google/javascript/jscomp/FindModuleDependencies.java index 37a1b32ecf5..92b9c911221 100644 --- a/src/com/google/javascript/jscomp/FindModuleDependencies.java +++ b/src/com/google/javascript/jscomp/FindModuleDependencies.java @@ -20,7 +20,6 @@ import com.google.common.collect.ImmutableMap; import com.google.javascript.jscomp.CompilerInput.ModuleType; -import com.google.javascript.jscomp.Es6RewriteModules.FindGoogProvideOrGoogModule; import com.google.javascript.jscomp.deps.DependencyInfo.Require; import com.google.javascript.jscomp.deps.ModuleLoader; import com.google.javascript.rhino.Node; @@ -62,6 +61,38 @@ public class FindModuleDependencies implements NodeTraversal.ScopedCallback { this.inputPathByWebpackId = inputPathByWebpackId; } + private static class FindGoogProvideOrGoogModule extends NodeTraversal.AbstractPreOrderCallback { + + private boolean found; + + boolean isFound() { + return found; + } + + @Override + public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { + if (found) { + return false; + } + // Shallow traversal, since we don't need to inspect within functions or expressions. + if (parent == null + || NodeUtil.isControlStructure(parent) + || NodeUtil.isStatementBlock(parent)) { + if (n.isExprResult()) { + Node maybeGetProp = n.getFirstFirstChild(); + if (maybeGetProp != null + && (maybeGetProp.matchesQualifiedName("goog.provide") + || maybeGetProp.matchesQualifiedName("goog.module"))) { + found = true; + return false; + } + } + return true; + } + return false; + } + } + public void process(Node root) { checkArgument(root.isScript()); if (Es6RewriteModules.isEs6ModuleRoot(root)) { diff --git a/src/com/google/javascript/jscomp/ModuleMetadata.java b/src/com/google/javascript/jscomp/ModuleMetadata.java new file mode 100644 index 00000000000..478ad58bb28 --- /dev/null +++ b/src/com/google/javascript/jscomp/ModuleMetadata.java @@ -0,0 +1,506 @@ +/* + * Copyright 2018 The Closure Compiler Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.javascript.jscomp; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.javascript.jscomp.ClosureCheckModule.DECLARE_LEGACY_NAMESPACE_OUTSIDE_GOOG_MODULE; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.javascript.jscomp.NodeTraversal.Callback; +import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.Node; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Gathers metadata around modules that is useful for checking imports / requires. + * + *

TODO(johnplaisted): There's an opportunity for reuse here in ClosureRewriteModules, which + * would involve putting this in some common location. Currently this is only used as a helper class + * for Es6RewriteModules. CompilerInput already has some (not all) of this information but it is not + * always populated. Additionally we'd ideally unwrap the goog.loadModule calls so each becomes its + * own CompilerInput, otherwise goog.require(path) from loadModules won't work correctly. But not + * having a 1:1 mapping of actual inputs to compiler inputs may cause issues. It may also be ideal + * to include CommonJS here too as ES6 modules can import them. That would allow decoupling of how + * these modules are written; right now Es6RewriteModule only checks this for goog.requires and + * goog: imports, not for ES6 path imports. + */ +final class ModuleMetadata { + /** Various types of Javascript "modules" that can be found in the JS Compiler. */ + public enum ModuleType { + ES6_MODULE("an ES6 module"), + GOOG_PROVIDE("a goog.provide'd file"), + /** A goog.module that does not declare a legacy namespace. */ + GOOG_MODULE("a goog.module"), + /** A goog.module that declares a legacy namespace with goog.module.declareLegacyNamespace. */ + LEGACY_GOOG_MODULE("a goog.module"), + COMMON_JS("a CommonJS module"), + SCRIPT("a script"); + + private final String description; + + ModuleType(String description) { + this.description = description; + } + } + + static final DiagnosticType MIXED_MODULE_TYPE = + DiagnosticType.error("JSC_MIXED_MODULE_TYPE", "A file cannot be both {0} and {1}."); + + static final DiagnosticType INVALID_DECLARE_NAMESPACE_CALL = + DiagnosticType.error( + "JSC_INVALID_DECLARE_NAMESPACE_CALL", + "goog.module.declareNamespace parameter must be a string literal."); + + static final DiagnosticType DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE = + DiagnosticType.error( + "JSC_DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE", + "goog.module.declareNamespace can only be called within ES6 modules."); + + static final DiagnosticType MULTIPLE_DECLARE_MODULE_NAMESPACE = + DiagnosticType.error( + "JSC_MULTIPLE_DECLARE_MODULE_NAMESPACE", + "goog.module.declareNamespace can only be called once per ES6 module."); + + private static final Node GOOG_PROVIDE = IR.getprop(IR.name("goog"), IR.string("provide")); + private static final Node GOOG_MODULE = IR.getprop(IR.name("goog"), IR.string("module")); + private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE = + IR.getprop(GOOG_MODULE.cloneTree(), IR.string("declareLegacyNamespace")); + private static final Node GOOG_MODULE_DECLARNAMESPACE = + IR.getprop(GOOG_MODULE.cloneTree(), IR.string("declareNamespace")); + + /** + * Map from module path to module. These modules represent files and thus will contain all goog + * namespaces that are in the file. These are not the same modules in modulesByGoogNamespace. + */ + private final Map modulesByPath = new HashMap<>(); + + /** + * Map from Closure namespace to module. These modules represent just the single namespace and + * thus each module has only one goog namespace in its {@link Module#getGoogNamespaces()}. These + * are not the same modules in modulesByPath. + */ + private final Map modulesByGoogNamespace = new HashMap<>(); + + /** Modules by AST node. */ + private final Map modulesByNode = new HashMap<>(); + + /** The current module being traversed. */ + private ModuleBuilder currentModule; + + /** + * The module currentModule is nested under, if any. Modules are expected to be at most two deep + * (a script and then a goog.loadModule call). + */ + private ModuleBuilder parentModule; + + /** The call to goog.loadModule we are traversing. */ + private Node loadModuleCall; + + private final AbstractCompiler compiler; + private final boolean processCommonJsModules; + private final ResolutionMode moduleResolutionMode; + private Finder finder; + + ModuleMetadata( + AbstractCompiler compiler, + boolean processCommonJsModules, + ResolutionMode moduleResolutionMode) { + this.compiler = compiler; + this.processCommonJsModules = processCommonJsModules; + this.moduleResolutionMode = moduleResolutionMode; + this.finder = new Finder(); + } + + /** Struct containing basic information about a module including its type and goog namespaces. */ + static final class Module { + private final ModuleType moduleType; + private final ModulePath path; + private final ImmutableList nestedModules; + + /** + * Closure namespaces that this file is associated with. Created by goog.provide, goog.module, + * and goog.module.declareNamespace. + */ + private final ImmutableSet googNamespaces; + + private Module( + @Nullable ModulePath path, + ModuleType moduleType, + Set googNamespaces, + List nestedModules) { + this.path = path; + this.moduleType = moduleType; + this.googNamespaces = ImmutableSet.copyOf(googNamespaces); + this.nestedModules = ImmutableList.copyOf(nestedModules); + } + + public ModuleType getModuleType() { + return moduleType; + } + + public boolean isEs6Module() { + return moduleType == ModuleType.ES6_MODULE; + } + + public boolean isGoogModule() { + return isNonLegacyGoogModule() || isLegacyGoogModule(); + } + + public boolean isNonLegacyGoogModule() { + return moduleType == ModuleType.GOOG_MODULE; + } + + public boolean isLegacyGoogModule() { + return moduleType == ModuleType.LEGACY_GOOG_MODULE; + } + + public boolean isGoogProvide() { + return moduleType == ModuleType.GOOG_PROVIDE; + } + + public boolean isCommonJs() { + return moduleType == ModuleType.COMMON_JS; + } + + public boolean isScript() { + return moduleType == ModuleType.SCRIPT; + } + + public ImmutableSet getGoogNamespaces() { + return googNamespaces; + } + + @Nullable + public ModulePath getPath() { + return path; + } + + /** @return the global, qualified name to rewrite any references to this module to */ + public String getGlobalName() { + return getGlobalName(null); + } + + /** @return the global, qualified name to rewrite any references to this module to */ + public String getGlobalName(@Nullable String googNamespace) { + checkState(googNamespace == null || googNamespaces.contains(googNamespace)); + switch (moduleType) { + case GOOG_MODULE: + return ClosureRewriteModule.getBinaryModuleNamespace(googNamespace); + case GOOG_PROVIDE: + case LEGACY_GOOG_MODULE: + return googNamespace; + case ES6_MODULE: + case COMMON_JS: + return path.toModuleName(); + case SCRIPT: + // fall through, throw an error + } + throw new IllegalStateException("Unexpected module type: " + moduleType); + } + } + + private final class ModuleBuilder { + final Node rootNode; + @Nullable final ModulePath path; + final Set googNamespaces; + final List nestedModules; + ModuleType moduleType; + Node declaresNamespace; + Node declaresLegacyNamespace; + boolean ambiguous; + + ModuleBuilder(Node rootNode, @Nullable ModulePath path) { + this.rootNode = rootNode; + this.path = path; + googNamespaces = new HashSet<>(); + nestedModules = new ArrayList<>(); + moduleType = ModuleType.SCRIPT; + ambiguous = false; + } + + Module build() { + if (!ambiguous) { + if (declaresNamespace != null && moduleType != ModuleType.ES6_MODULE) { + compiler.report( + JSError.make(declaresNamespace, DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE)); + } + + if (declaresLegacyNamespace != null) { + if (moduleType == ModuleType.GOOG_MODULE) { + moduleType = ModuleType.LEGACY_GOOG_MODULE; + } else { + compiler.report( + JSError.make(declaresNamespace, DECLARE_LEGACY_NAMESPACE_OUTSIDE_GOOG_MODULE)); + } + } + } + + return new Module(path, moduleType, googNamespaces, nestedModules); + } + + void setModuleType(ModuleType type, NodeTraversal t, Node n) { + checkNotNull(type); + + if (moduleType == type) { + return; + } + + if (moduleType == ModuleType.SCRIPT) { + moduleType = type; + return; + } + + ambiguous = true; + t.report(n, MIXED_MODULE_TYPE, moduleType.description, type.description); + } + + void addGoogNamespace(String namespace) { + googNamespaces.add(namespace); + } + + void recordDeclareNamespace(Node declaresNamespace) { + this.declaresNamespace = declaresNamespace; + } + + void recordDeclareLegacyNamespace(Node declaresLegacyNamespace) { + this.declaresLegacyNamespace = declaresLegacyNamespace; + } + + public boolean isScript() { + return moduleType == ModuleType.SCRIPT; + } + } + + /** Traverses the AST and build a sets of {@link Module}s. */ + private final class Finder implements Callback { + @Override + public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { + switch (n.getToken()) { + case SCRIPT: + enterModule(n, t.getInput().getPath()); + break; + case IMPORT: + case EXPORT: + checkNotNull(currentModule); + currentModule.setModuleType(ModuleType.ES6_MODULE, t, n); + break; + case CALL: + if (n.isCall() && n.getFirstChild().matchesQualifiedName("goog.loadModule")) { + loadModuleCall = n; + if (n.getChildCount() > 2 && n.getChildAtIndex(2).isString()) { + enterModule(n, compiler.getModuleLoader().resolve(n.getChildAtIndex(2).getString())); + } else { + enterModule(n, null); + } + } + break; + default: + break; + } + + return true; + } + + private void enterModule(Node n, @Nullable ModulePath path) { + ModuleBuilder newModule = new ModuleBuilder(n, path); + if (currentModule != null) { + checkState(parentModule == null, "Expected modules to be nested at most 2 deep."); + parentModule = currentModule; + } + currentModule = newModule; + } + + private void leaveModule() { + checkNotNull(currentModule); + Module module = currentModule.build(); + modulesByNode.put(currentModule.rootNode, module); + if (module.path != null) { + modulesByPath.put(module.path.toString(), module); + } + for (String namespace : module.getGoogNamespaces()) { + modulesByGoogNamespace.put(namespace, module); + } + if (parentModule != null) { + parentModule.nestedModules.add(module); + } + currentModule = parentModule; + parentModule = null; + } + + @Override + public void visit(NodeTraversal t, Node n, Node parent) { + if (processCommonJsModules && currentModule != null && currentModule.isScript()) { + if (ProcessCommonJSModules.isCommonJsExport(t, n, moduleResolutionMode) + || ProcessCommonJSModules.isCommonJsImport(n, moduleResolutionMode)) { + currentModule.setModuleType(ModuleType.COMMON_JS, t, n); + return; + } + } + + switch (n.getToken()) { + case SCRIPT: + leaveModule(); + break; + case CALL: + if (loadModuleCall == n) { + leaveModule(); + loadModuleCall = null; + } else { + visitGoogCall(t, n); + } + break; + default: + break; + } + } + + private void visitGoogCall(NodeTraversal t, Node n) { + if (!n.hasChildren() + || !n.getFirstChild().isGetProp() + || !n.getFirstChild().isQualifiedName()) { + return; + } + + Node getprop = n.getFirstChild(); + + if (getprop.matchesQualifiedName(GOOG_PROVIDE)) { + currentModule.setModuleType(ModuleType.GOOG_PROVIDE, t, n); + if (n.hasTwoChildren() && n.getLastChild().isString()) { + String namespace = n.getLastChild().getString(); + currentModule.addGoogNamespace(namespace); + checkDuplicates(namespace, t, n); + } else { + t.report(n, ClosureRewriteModule.INVALID_PROVIDE_NAMESPACE); + } + } else if (getprop.matchesQualifiedName(GOOG_MODULE)) { + currentModule.setModuleType(ModuleType.GOOG_MODULE, t, n); + if (n.hasTwoChildren() && n.getLastChild().isString()) { + String namespace = n.getLastChild().getString(); + currentModule.addGoogNamespace(namespace); + checkDuplicates(namespace, t, n); + } else { + t.report(n, ClosureRewriteModule.INVALID_MODULE_NAMESPACE); + } + } else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) { + currentModule.recordDeclareLegacyNamespace(n); + } else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARNAMESPACE)) { + if (currentModule.declaresNamespace != null) { + t.report(n, MULTIPLE_DECLARE_MODULE_NAMESPACE); + } + if (n.hasTwoChildren() && n.getLastChild().isString()) { + currentModule.recordDeclareNamespace(n); + String namespace = n.getLastChild().getString(); + currentModule.addGoogNamespace(namespace); + checkDuplicates(namespace, t, n); + } else { + t.report(n, INVALID_DECLARE_NAMESPACE_CALL); + } + } + } + + /** Checks if the given Closure namespace is a duplicate or not. */ + private void checkDuplicates(String namespace, NodeTraversal t, Node n) { + Module existing = modulesByGoogNamespace.get(namespace); + if (existing != null) { + switch (existing.moduleType) { + case ES6_MODULE: + case GOOG_MODULE: + t.report(n, ClosureRewriteModule.DUPLICATE_MODULE); + break; + case GOOG_PROVIDE: + t.report(n, ClosureRewriteModule.DUPLICATE_NAMESPACE); + break; + default: + throw new IllegalStateException("Unexpected module type: " + existing.moduleType); + } + } + } + } + + public void process(Node externs, Node root) { + finder = new Finder(); + NodeTraversal.traverseEs6(compiler, externs, finder); + NodeTraversal.traverseEs6(compiler, root, finder); + } + + private void remove(Module module) { + if (module != null) { + for (String symbol : module.getGoogNamespaces()) { + modulesByGoogNamespace.remove(symbol); + } + if (module.path != null) { + modulesByPath.remove(module.path.toString()); + } + for (Module nested : module.nestedModules) { + remove(nested); + } + } + } + + public void hotSwapScript(Node scriptRoot) { + Module existing = + modulesByPath.get(compiler.getInput(scriptRoot.getInputId()).getPath().toString()); + remove(existing); + NodeTraversal.traverseEs6(compiler, scriptRoot, finder); + } + + /** + * @return map from module path to module. These modules represent files and thus {@link + * Module#getGoogNamespaces()} contains all Closure namespaces in the file. These are not the + * same modules from {@link ModuleMetadata#getModulesByGoogNamespace()}. It is not valid to + * call {@link Module#getGlobalName()} on {@link ModuleType#GOOG_PROVIDE} modules from this + * map that have more than one Closure namespace as it is ambiguous. + */ + Map getModulesByPath() { + return Collections.unmodifiableMap(modulesByPath); + } + + /** + * @return map from Closure namespace to module. These modules represent the Closure namespace and + * thus {@link Module#getGoogNamespaces()} will have size 1. As a result, it is valid to call + * {@link Module#getGlobalName()} on these modules. These are not the same modules from {@link + * ModuleMetadata#getModulesByPath()}. + */ + Map getModulesByGoogNamespace() { + return Collections.unmodifiableMap(modulesByGoogNamespace); + } + + /** @return the {@link Module} that contains the given AST node */ + @Nullable + Module getContainingModule(Node n) { + if (finder == null) { + return null; + } + Module m = null; + while (m == null && n != null) { + m = modulesByNode.get(n); + n = n.getParent(); + } + return m; + } +} diff --git a/src/com/google/javascript/jscomp/TranspilationPasses.java b/src/com/google/javascript/jscomp/TranspilationPasses.java index 91c02f9c3b3..2705aa9fbaf 100644 --- a/src/com/google/javascript/jscomp/TranspilationPasses.java +++ b/src/com/google/javascript/jscomp/TranspilationPasses.java @@ -41,7 +41,11 @@ public static void addEs6ModulePass( @Override protected HotSwapCompilerPass create(AbstractCompiler compiler) { preprocessorTableFactory.maybeInitialize(compiler); - return new Es6RewriteModules(compiler, preprocessorTableFactory.getInstanceOrNull()); + return new Es6RewriteModules( + compiler, + preprocessorTableFactory.getInstanceOrNull(), + compiler.getOptions().processCommonJSModules, + compiler.getOptions().moduleResolutionMode); } @Override diff --git a/src/com/google/javascript/jscomp/deps/JsFileParser.java b/src/com/google/javascript/jscomp/deps/JsFileParser.java index 17a40e47944..6dc4f5a6ce2 100644 --- a/src/com/google/javascript/jscomp/deps/JsFileParser.java +++ b/src/com/google/javascript/jscomp/deps/JsFileParser.java @@ -52,7 +52,8 @@ public final class JsFileParser extends JsFileLineParser { // but fails to match without "use strict"; since we look for semicolon, not open brace. Pattern.compile( "(?:^|;)(?:[a-zA-Z0-9$_,:{}\\s]+=)?\\s*" - + "goog\\.(provide|module|require|requireType|addDependency)\\s*\\((.*?)\\)"); + + "goog\\.(?provide|module|require|requireType|addDependency)" + + "(?\\.declareNamespace)?\\s*\\((?.*?)\\)"); /** * Pattern for matching import ... from './path/to/file'. @@ -253,11 +254,12 @@ protected boolean parseLine(String line) throws ParseException { } // See if it's a require or provide. - String methodName = googMatcher.group(1); + String methodName = googMatcher.group("func"); char firstChar = methodName.charAt(0); - boolean isModule = firstChar == 'm'; + boolean isDeclareModuleNamespace = firstChar == 'm' && googMatcher.group("subfunc") != null; + boolean isModule = !isDeclareModuleNamespace && firstChar == 'm'; boolean isProvide = firstChar == 'p'; - boolean providesNamespace = isProvide || isModule; + boolean providesNamespace = isProvide || isModule || isDeclareModuleNamespace; boolean isRequire = firstChar == 'r'; if (isModule && this.moduleType != ModuleType.WRAPPED_GOOG_MODULE) { @@ -270,7 +272,7 @@ protected boolean parseLine(String line) throws ParseException { if (providesNamespace || isRequire) { // Parse the param. - String arg = parseJsString(googMatcher.group(2)); + String arg = parseJsString(googMatcher.group("args")); // Add the dependency. if (isRequire) { if ("requireType".equals(methodName)) { diff --git a/src/com/google/javascript/jscomp/gwt/client/JsfileParser.java b/src/com/google/javascript/jscomp/gwt/client/JsfileParser.java index 950c515f625..240e452820b 100644 --- a/src/com/google/javascript/jscomp/gwt/client/JsfileParser.java +++ b/src/com/google/javascript/jscomp/gwt/client/JsfileParser.java @@ -365,6 +365,13 @@ public void visit(NodeTraversal traversal, Node node, Node parent) { default: // Do nothing } + } else if (parent.isGetProp() + && parent.matchesQualifiedName("goog.module.declareNamespace") + && parent.getParent().isCall()) { + Node arg = parent.getParent().getSecondChild(); + if (arg.isString()) { + info.provides.add(arg.getString()); + } // TODO(johnplaisted): else warning? } } } diff --git a/test/com/google/javascript/jscomp/ClosureCheckModuleTest.java b/test/com/google/javascript/jscomp/ClosureCheckModuleTest.java index 0ad136e5e2e..93f0aada779 100644 --- a/test/com/google/javascript/jscomp/ClosureCheckModuleTest.java +++ b/test/com/google/javascript/jscomp/ClosureCheckModuleTest.java @@ -20,7 +20,6 @@ import static com.google.javascript.jscomp.ClosureCheckModule.EXPORT_NOT_A_MODULE_LEVEL_STATEMENT; import static com.google.javascript.jscomp.ClosureCheckModule.EXPORT_REPEATED_ERROR; import static com.google.javascript.jscomp.ClosureCheckModule.GOOG_MODULE_REFERENCES_THIS; -import static com.google.javascript.jscomp.ClosureCheckModule.GOOG_MODULE_USES_GOOG_MODULE_GET; import static com.google.javascript.jscomp.ClosureCheckModule.GOOG_MODULE_USES_THROW; import static com.google.javascript.jscomp.ClosureCheckModule.INCORRECT_SHORTNAME_CAPITALIZATION; import static com.google.javascript.jscomp.ClosureCheckModule.INVALID_DESTRUCTURING_FORWARD_DECLARE; @@ -28,6 +27,7 @@ import static com.google.javascript.jscomp.ClosureCheckModule.JSDOC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME; import static com.google.javascript.jscomp.ClosureCheckModule.LET_GOOG_REQUIRE; import static com.google.javascript.jscomp.ClosureCheckModule.MODULE_AND_PROVIDES; +import static com.google.javascript.jscomp.ClosureCheckModule.MODULE_USES_GOOG_MODULE_GET; import static com.google.javascript.jscomp.ClosureCheckModule.MULTIPLE_MODULES_IN_FILE; import static com.google.javascript.jscomp.ClosureCheckModule.ONE_REQUIRE_PER_DECLARATION; import static com.google.javascript.jscomp.ClosureCheckModule.REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME; @@ -99,7 +99,7 @@ public void testGoogModuleUsesThrow() { } public void testGoogModuleGetAtTopLevel() { - testError("goog.module('xyz');\ngoog.module.get('abc');", GOOG_MODULE_USES_GOOG_MODULE_GET); + testError("goog.module('xyz');\ngoog.module.get('abc');", MODULE_USES_GOOG_MODULE_GET); testError( lines( @@ -110,7 +110,7 @@ public void testGoogModuleGetAtTopLevel() { "if (x) {", " var y = goog.module.get('abc');", "}"), - GOOG_MODULE_USES_GOOG_MODULE_GET); + MODULE_USES_GOOG_MODULE_GET); testSame( lines( diff --git a/test/com/google/javascript/jscomp/Es6RewriteModulesTest.java b/test/com/google/javascript/jscomp/Es6RewriteModulesTest.java index 2645ba3b4b3..a41a3b23760 100644 --- a/test/com/google/javascript/jscomp/Es6RewriteModulesTest.java +++ b/test/com/google/javascript/jscomp/Es6RewriteModulesTest.java @@ -16,11 +16,10 @@ package com.google.javascript.jscomp; -import static com.google.javascript.jscomp.Es6RewriteModules.LHS_OF_GOOG_REQUIRE_MUST_BE_CONST; - import com.google.common.collect.ImmutableList; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.deps.ModuleLoader; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; /** * Unit tests for {@link Es6RewriteModules} @@ -53,7 +52,11 @@ protected CompilerOptions getOptions() { @Override protected CompilerPass getProcessor(Compiler compiler) { - return new Es6RewriteModules(compiler, /* preprocessorSymbolTable= */ null); + return new Es6RewriteModules( + compiler, + /* preprocessorSymbolTable= */ null, + /* processCommonJsModules= */ false, + ResolutionMode.BROWSER); } @Override @@ -737,133 +740,6 @@ public void testRenameImportedReference() { "/** @const */ var module$testcode = {};")); } - public void testGoogRequires_noChange() { - testSame("goog.require('foo.bar');"); - testSame("var bar = goog.require('foo.bar');"); - - testModules( - "goog.require('foo.bar');\nexport var x;", - lines( - "goog.require('foo.bar');", - "var x$$module$testcode;", - "/** @const */ var module$testcode = {};", - "module$testcode.x = x$$module$testcode;")); - - testModules( - "export var x;\n goog.require('foo.bar');", - lines( - "var x$$module$testcode;", - "goog.require('foo.bar');", - "/** @const */ var module$testcode = {};", - "module$testcode.x = x$$module$testcode;")); - - testModules( - "import * as s from './other.js';\ngoog.require('foo.bar');", - "goog.require('foo.bar'); /** @const */ var module$testcode = {};"); - - testModules( - "goog.require('foo.bar');\nimport * as s from './other.js';", - "goog.require('foo.bar'); /** @const */ var module$testcode = {};"); - } - - public void testGoogRequires_rewrite() { - testModules( - "const bar = goog.require('foo.bar')\nexport var x;", - lines( - "goog.require('foo.bar');", - "const bar$$module$testcode = foo.bar;", - "var x$$module$testcode;", - "/** @const */ var module$testcode = {};", - "module$testcode.x = x$$module$testcode;")); - - testModules( - "export var x\nconst bar = goog.require('foo.bar');", - lines( - "var x$$module$testcode;", - "goog.require('foo.bar');", - "const bar$$module$testcode = foo.bar;", - "/** @const */ var module$testcode = {};", - "module$testcode.x = x$$module$testcode;")); - - testModules( - "import * as s from './other.js';\nconst bar = goog.require('foo.bar');", - lines( - "goog.require('foo.bar');", - "const bar$$module$testcode = foo.bar;", - "/** @const */ var module$testcode = {};")); - - testModules( - "const bar = goog.require('foo.bar');\nimport * as s from './other.js';", - lines( - "goog.require('foo.bar');", - "const bar$$module$testcode = foo.bar;", - "/** @const */ var module$testcode = {};")); - } - - public void testGoogRequires_nonConst() { - ModulesTestUtils.testModulesError(this, "var bar = goog.require('foo.bar');\nexport var x;", - LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - - ModulesTestUtils.testModulesError(this, "export var x;\nvar bar = goog.require('foo.bar');", - LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - - ModulesTestUtils.testModulesError(this, - "import * as s from './other.js';\nvar bar = goog.require('foo.bar');", - LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - - ModulesTestUtils.testModulesError(this, - "var bar = goog.require('foo.bar');\nimport * as s from './other.js';", - LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - } - - public void testGoogRequiresDestructuring_rewrite() { - testModules( - lines( - "import * as s from './other.js';", - "const {foo, bar} = goog.require('some.name.space');", - "use(foo, bar);"), - lines( - "goog.require('some.name.space');", - "const {", - " foo: foo$$module$testcode,", - " bar: bar$$module$testcode,", - "} = some.name.space;", - "use(foo$$module$testcode, bar$$module$testcode);", - "/** @const */ var module$testcode = {};")); - - ModulesTestUtils.testModulesError(this, lines( - "import * as s from './other.js';", - "var {foo, bar} = goog.require('some.name.space');", - "use(foo, bar);"), LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - - ModulesTestUtils.testModulesError(this, lines( - "import * as s from './other.js';", - "let {foo, bar} = goog.require('some.name.space');", - "use(foo, bar);"), LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); - } - - public void testNamespaceImports() { - testModules( - lines("import Foo from 'goog:other.Foo';", "use(Foo);"), - "use(other.Foo); /** @const */ var module$testcode = {};"); - - testModules( - lines("import {x, y} from 'goog:other.Foo';", "use(x);", "use(y);"), - "use(other.Foo.x);\n use(other.Foo.y);/** @const */ var module$testcode = {};"); - - testModules( - lines( - "import Foo from 'goog:other.Foo';", - "/** @type {Foo} */ var foo = new Foo();"), - lines( - "/** @type {other.Foo} */", - "var foo$$module$testcode = new other.Foo();", - "/** @const */ var module$testcode = {};")); - - ModulesTestUtils.testModulesError(this, "import * as Foo from 'goog:other.Foo';", - Es6RewriteModules.NAMESPACE_IMPORT_CANNOT_USE_STAR); - } - public void testObjectDestructuringAndObjLitShorthand() { testModules( lines( diff --git a/test/com/google/javascript/jscomp/Es6RewriteModulesWithGoogInteropTest.java b/test/com/google/javascript/jscomp/Es6RewriteModulesWithGoogInteropTest.java new file mode 100644 index 00000000000..3caf958a214 --- /dev/null +++ b/test/com/google/javascript/jscomp/Es6RewriteModulesWithGoogInteropTest.java @@ -0,0 +1,401 @@ +/* + * 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.javascript.jscomp.ClosureCheckModule.MODULE_USES_GOOG_MODULE_GET; +import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_GET_NAMESPACE; +import static com.google.javascript.jscomp.ClosureRewriteModule.MISSING_MODULE_OR_PROVIDE; +import static com.google.javascript.jscomp.Es6RewriteModules.LHS_OF_GOOG_REQUIRE_MUST_BE_CONST; +import static com.google.javascript.jscomp.Es6RewriteModules.NAMESPACE_IMPORT_CANNOT_USE_STAR; +import static com.google.javascript.jscomp.Es6RewriteModules.PATH_REQUIRE_FOR_NON_ES6_MODULE; +import static com.google.javascript.jscomp.Es6RewriteModules.PATH_REQUIRE_IN_ES6_MODULE; +import static com.google.javascript.jscomp.Es6RewriteModules.PATH_REQUIRE_IN_GOOG_MODULE_WITH_NO_PATH; +import static com.google.javascript.jscomp.Es6RewriteModules.PATH_REQUIRE_IN_GOOG_PROVIDE_FILE; +import static com.google.javascript.jscomp.Es6RewriteModules.PATH_REQUIRE_IN_NON_GOOG_MODULE; +import static com.google.javascript.jscomp.ProcessClosurePrimitives.INVALID_CLOSURE_CALL_ERROR; + +import com.google.common.collect.ImmutableList; +import com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; + +/** Unit tests for {@link Es6RewriteModules} that test interop with closure files. */ + +public final class Es6RewriteModulesWithGoogInteropTest extends CompilerTestCase { + private static final SourceFile CLOSURE_PROVIDE = + SourceFile.fromCode("closure_provide.js", "goog.provide('closure.provide');"); + + private static final SourceFile CLOSURE_MODULE = + SourceFile.fromCode("closure_module.js", "goog.module('closure.module');"); + + private static final SourceFile CLOSURE_LEGACY_MODULE = + SourceFile.fromCode( + "closure_legacy_module.js", + "goog.module('closure.legacy.module'); goog.module.declareLegacyNamespace();"); + + @Override + protected void setUp() throws Exception { + super.setUp(); + // ECMASCRIPT5 to trigger module processing after parsing. + setLanguage(LanguageMode.ECMASCRIPT_2015, LanguageMode.ECMASCRIPT5); + enableRunTypeCheckAfterProcessing(); + } + + @Override + protected CompilerOptions getOptions() { + CompilerOptions options = super.getOptions(); + // ECMASCRIPT5 to Trigger module processing after parsing. + options.setLanguageOut(LanguageMode.ECMASCRIPT5); + options.setWarningLevel(DiagnosticGroups.LINT_CHECKS, CheckLevel.ERROR); + + return options; + } + + @Override + protected CompilerPass getProcessor(Compiler compiler) { + return new Es6RewriteModules( + compiler, + /* preprocessorSymbolTable= */ null, + /* processCommonJsModules= */ false, + ResolutionMode.BROWSER); + } + + @Override + protected int getNumRepetitions() { + return 1; + } + + void testModules(String input, String expected) { + test( + srcs( + CLOSURE_PROVIDE, + CLOSURE_MODULE, + CLOSURE_LEGACY_MODULE, + SourceFile.fromCode("testcode", input)), + super.expected( + CLOSURE_PROVIDE, + CLOSURE_MODULE, + CLOSURE_LEGACY_MODULE, + SourceFile.fromCode("testcode", expected))); + } + + void testModulesError(String input, DiagnosticType error) { + testError( + ImmutableList.of( + CLOSURE_PROVIDE, + CLOSURE_MODULE, + CLOSURE_LEGACY_MODULE, + SourceFile.fromCode("testcode", input)), + error); + } + + public void testClosureFilesUnchanged() { + testSame(srcs(CLOSURE_PROVIDE, CLOSURE_MODULE, CLOSURE_LEGACY_MODULE)); + } + + public void testGoogRequiresInNonEs6ModuleUnchanged() { + testSame("goog.require('foo.bar');"); + testSame("var bar = goog.require('foo.bar');"); + } + + public void testGoogRequireInEs6ModuleDoesNotExistIsError() { + testError("export var x; goog.require('foo.bar');", MISSING_MODULE_OR_PROVIDE); + } + + public void testGoogModuleGetNonStringIsError() { + testError("const y = goog.module.get();\nexport {};", INVALID_GET_NAMESPACE); + testError("const y = goog.module.get(0);\nexport {};", INVALID_GET_NAMESPACE); + } + + public void testGoogRequireForProvide() { + testModules( + "const y = goog.require('closure.provide');\nexport {};", + lines( + "const y$$module$testcode = closure.provide;", + "/** @const */ var module$testcode = {};")); + } + + public void testGoogRequireForGoogModule() { + testModules( + "const y = goog.require('closure.module');\nexport {};", + lines( + "const y$$module$testcode = module$exports$closure$module;", + "/** @const */ var module$testcode = {};")); + } + + public void testGoogModuleGetForGoogModule() { + testModules( + "function foo() { const y = goog.module.get('closure.module'); }\nexport {};", + lines( + "function foo$$module$testcode() { const y = module$exports$closure$module; }", + "/** @const */ var module$testcode = {};")); + } + + public void testGoogRequireForLegacyGoogModule() { + testModules( + "const y = goog.require('closure.legacy.module');\nexport {};", + lines( + "const y$$module$testcode = closure.legacy.module;", + "/** @const */ var module$testcode = {};")); + } + + public void testGoogModuleGetForLegacyGoogModule() { + testModules( + "function foo(){ const y = goog.module.get('closure.legacy.module'); }\nexport {};", + lines( + "function foo$$module$testcode(){ const y = closure.legacy.module; }", + "/** @const */ var module$testcode = {};")); + } + + public void testGoogModuleGetForEs6Module() { + test( + srcs( + SourceFile.fromCode("es6.js", "export{}; goog.module.declareNamespace('es6');"), + SourceFile.fromCode( + "closure.js", + "goog.module('my.module'); function f() { const y = goog.module.get('es6'); }")), + expected( + SourceFile.fromCode("es6.js", "/** @const */ var module$es6 = {};"), + SourceFile.fromCode( + "closure.js", "goog.module('my.module'); function f() { const y = module$es6; }"))); + } + + public void testRewriteGoogRequiresForEs6ModulePath() { + String srcEs6 = "export var x;"; + String expectedEs6 = + "var x$$module$es6;/** @const */ var module$es6={};module$es6.x=x$$module$es6;"; + + test( + srcs( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode( + "goog.js", "goog.module('m'); const es6 = goog.require('./es6.js'); use(es6.x);")), + expected( + SourceFile.fromCode("es6.js", expectedEs6), + SourceFile.fromCode( + "goog.js", "goog.module('m'); const es6 = module$es6; use(es6.x);"))); + + test( + srcs( + SourceFile.fromCode("nested0/es6.js", srcEs6), + SourceFile.fromCode( + "nested1/goog.js", + "goog.module('m'); const {x} = goog.require('../nested0/es6.js');")), + expected( + SourceFile.fromCode( + "nested0/es6.js", + lines( + "var x$$module$nested0$es6;", + "/** @const */ var module$nested0$es6={};", + "module$nested0$es6.x=x$$module$nested0$es6;")), + SourceFile.fromCode( + "nested1/goog.js", "goog.module('m'); const {x} = module$nested0$es6"))); + + test( + srcs( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode("goog.js", "goog.module('m'); goog.require('./es6.js');")), + expected( + SourceFile.fromCode("es6.js", expectedEs6), + SourceFile.fromCode("goog.js", "goog.module('m');"))); + + testError( + ImmutableList.of( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode("goog.js", "goog.provide('m'); goog.require('./es6.js');")), + PATH_REQUIRE_IN_GOOG_PROVIDE_FILE); + + testError( + ImmutableList.of( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode("goog.js", "export {}; goog.require('./es6.js');")), + PATH_REQUIRE_IN_ES6_MODULE); + + testError( + ImmutableList.of( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode("goog.js", "goog.require('./es6.js');")), + PATH_REQUIRE_IN_NON_GOOG_MODULE); + + testError( + ImmutableList.of( + SourceFile.fromCode("not_es6.js", "goog.module('a');"), + SourceFile.fromCode("goog.js", "goog.module('m'); goog.require('./not_es6.js');")), + PATH_REQUIRE_FOR_NON_ES6_MODULE); + } + + public void testRewriteGoogRequiresForEs6ModulePathInLoadModule() { + String srcEs6 = "export var x;"; + String expectedEs6 = + "var x$$module$es6;/** @const */ var module$es6={};module$es6.x=x$$module$es6;"; + + testError( + ImmutableList.of( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode( + "bundle.js", + lines( + "goog.loadModule(function() {", + " goog.module('m');", + " const es6 = goog.require('./es6.js');", + "});"))), + PATH_REQUIRE_IN_GOOG_MODULE_WITH_NO_PATH); + + test( + srcs( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode( + "bundle.js", + lines( + "goog.loadModule(function() {", + " goog.module('m');", + " const es6 = goog.require('../es6.js');", + "}, 'nested/goog.js');"))), + expected( + SourceFile.fromCode("es6.js", expectedEs6), + SourceFile.fromCode( + "bundle.js", + lines( + "goog.loadModule(function() {", + " goog.module('m');", + " const es6 = module$es6;", + "}, 'nested/goog.js');")))); + + test( + srcs( + SourceFile.fromCode("es6.js", srcEs6), + SourceFile.fromCode( + "bundle.js", + lines( + "goog.loadModule(function() {", + " goog.module('m');", + " const es6 = goog.require('./es6.js');", + "}, 'goog.js');", + "goog.loadModule(function() {", + " goog.module('m2');", + " const es6 = goog.require('../es6.js');", + "}, 'nested/goog.js');"))), + expected( + SourceFile.fromCode("es6.js", expectedEs6), + SourceFile.fromCode( + "bundle.js", + lines( + "goog.loadModule(function() {", + " goog.module('m');", + " const es6 = module$es6;", + "}, 'goog.js');", + "goog.loadModule(function() {", + " goog.module('m2');", + " const es6 = module$es6;", + "}, 'nested/goog.js');")))); + } + + public void testDeclareNamespace() { + SourceFile srcEs6 = + SourceFile.fromCode("es6.js", "export var x; goog.module.declareNamespace('my.es6');"); + SourceFile expectedEs6 = + SourceFile.fromCode( + "es6.js", + "var x$$module$es6;/** @const */ var module$es6={};module$es6.x=x$$module$es6;"); + + test( + srcs( + srcEs6, + SourceFile.fromCode("goog.js", "const es6 = goog.require('my.es6'); use(es6, es6.x);")), + expected( + expectedEs6, + SourceFile.fromCode("goog.js", "const es6 = module$es6; use(es6, es6.x);"))); + } + + public void testGoogRequireLhsNonConstIsError() { + testModulesError( + "var bar = goog.require('closure.provide');\nexport var x;", + LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); + + testModulesError( + "export var x;\nvar bar = goog.require('closure.provide');", + LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); + } + + public void testGoogRequiresDestructuring_rewrite() { + testModules( + lines( + "export {};", "const {foo, bar} = goog.require('closure.provide');", "use(foo, bar);"), + lines( + "const {", + " foo: foo$$module$testcode,", + " bar: bar$$module$testcode,", + "} = closure.provide;", + "use(foo$$module$testcode, bar$$module$testcode);", + "/** @const */ var module$testcode = {};")); + + testModulesError( + lines("export {};", "var {foo, bar} = goog.require('closure.provide');", "use(foo, bar);"), + LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); + + testModulesError( + lines("export {};", "let {foo, bar} = goog.require('closure.provide');", "use(foo, bar);"), + LHS_OF_GOOG_REQUIRE_MUST_BE_CONST); + } + + public void testNamespaceImports() { + testModules( + lines("import Foo from 'goog:closure.provide';", "use(Foo);"), + "use(closure.provide); /** @const */ var module$testcode = {};"); + + testModules( + lines("import {x, y} from 'goog:closure.provide';", "use(x);", "use(y);"), + "use(closure.provide.x);\n use(closure.provide.y);/** @const */ var module$testcode = {};"); + + testModules( + lines("import Foo from 'goog:closure.provide';", "/** @type {Foo} */ var foo = new Foo();"), + lines( + "/** @type {closure.provide} */", + "var foo$$module$testcode = new closure.provide();", + "/** @const */ var module$testcode = {};")); + + testModules( + lines( + "import Foo from 'goog:closure.module';", + "/** @type {Foo.Bar} */ var foo = new Foo.Bar();"), + lines( + "/** @type {module$exports$closure$module.Bar} */", + "var foo$$module$testcode = new module$exports$closure$module.Bar();", + "/** @const */ var module$testcode = {};")); + + testModules( + lines( + "import Foo from 'goog:closure.legacy.module';", + "/** @type {Foo.Bar} */ var foo = new Foo.Bar();"), + lines( + "/** @type {closure.legacy.module.Bar} */", + "var foo$$module$testcode = new closure.legacy.module.Bar();", + "/** @const */ var module$testcode = {};")); + + testModulesError( + "import * as Foo from 'goog:closure.provide';", NAMESPACE_IMPORT_CANNOT_USE_STAR); + } + + public void testGoogRequireMustBeModuleScope() { + testModulesError("{ goog.require('closure.provide'); } export {}", INVALID_CLOSURE_CALL_ERROR); + } + + public void testGoogModuleGetCannotBeModuleHoistScope() { + testModulesError("goog.module.get('closure.module'); export {}", MODULE_USES_GOOG_MODULE_GET); + testModulesError( + "{ goog.module.get('closure.module'); } export {}", MODULE_USES_GOOG_MODULE_GET); + } +} diff --git a/test/com/google/javascript/jscomp/ModuleMetadataTest.java b/test/com/google/javascript/jscomp/ModuleMetadataTest.java new file mode 100644 index 00000000000..97c7b627ad2 --- /dev/null +++ b/test/com/google/javascript/jscomp/ModuleMetadataTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018 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.truth.Truth.assertThat; + +import com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.jscomp.ModuleMetadata.Module; +import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; +import com.google.javascript.rhino.Node; + + +public final class ModuleMetadataTest extends CompilerTestCase { + ModuleMetadata metadata; + + @Override + protected void setUp() throws Exception { + super.setUp(); + // ECMASCRIPT5 to trigger module processing after parsing. + setLanguage(LanguageMode.ECMASCRIPT_2015, LanguageMode.ECMASCRIPT5); + enableRunTypeCheckAfterProcessing(); + } + + @Override + protected CompilerPass getProcessor(Compiler compiler) { + metadata = + new ModuleMetadata(compiler, /* processCommonJsModules */ true, ResolutionMode.BROWSER); + return (Node externs, Node root) -> metadata.process(externs, root); + } + + public void testGoogProvide() { + testSame("goog.provide('my.provide');"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.provide"); + assertThat(metadata.getModulesByPath().keySet()).contains("testcode"); + Module m = metadata.getModulesByGoogNamespace().get("my.provide"); + assertThat(m.getGoogNamespaces()).containsExactly("my.provide"); + assertThat(m.isGoogProvide()).isTrue(); + } + + public void testMultipleGoogProvide() { + testSame("goog.provide('my.first.provide'); goog.provide('my.second.provide');"); + assertThat(metadata.getModulesByGoogNamespace().keySet()) + .containsExactly("my.first.provide", "my.second.provide"); + assertThat(metadata.getModulesByPath().keySet()).contains("testcode"); + Module m = metadata.getModulesByGoogNamespace().get("my.first.provide"); + assertThat(m.getGoogNamespaces()).containsExactly("my.first.provide", "my.second.provide"); + assertThat(m.isGoogProvide()).isTrue(); + + m = metadata.getModulesByGoogNamespace().get("my.second.provide"); + assertThat(m.getGoogNamespaces()).containsExactly("my.first.provide", "my.second.provide"); + assertThat(m.isGoogProvide()).isTrue(); + + m = metadata.getModulesByPath().get("testcode"); + assertThat(m.getGoogNamespaces()).containsExactly("my.first.provide", "my.second.provide"); + assertThat(m.isGoogProvide()).isTrue(); + } + + public void testGoogModule() { + testSame("goog.module('my.module');"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isGoogProvide()).isFalse(); + assertThat(m.isGoogModule()).isTrue(); + assertThat(m.isNonLegacyGoogModule()).isTrue(); + assertThat(m.isLegacyGoogModule()).isFalse(); + } + + public void testGoogModuleWithDefaultExport() { + // exports = 0; on its own is CommonJS! + testSame("goog.module('my.module'); exports = 0;"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isGoogProvide()).isFalse(); + assertThat(m.isGoogModule()).isTrue(); + assertThat(m.isNonLegacyGoogModule()).isTrue(); + assertThat(m.isLegacyGoogModule()).isFalse(); + } + + public void testLegacyGoogModule() { + testSame("goog.module('my.module'); goog.module.declareLegacyNamespace();"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isGoogProvide()).isFalse(); + assertThat(m.isGoogModule()).isTrue(); + assertThat(m.isNonLegacyGoogModule()).isFalse(); + assertThat(m.isLegacyGoogModule()).isTrue(); + } + + public void testLoadModule() { + testSame("goog.loadModule(function() { goog.module('my.module'); });"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isNonLegacyGoogModule()).isTrue(); + assertThat(m.getPath()).isNull(); + + m = metadata.getModulesByPath().get("testcode"); + assertThat(m.getGoogNamespaces()).isEmpty(); + assertThat(m.isScript()).isTrue(); + } + + public void testLoadModuleWithPath() { + testSame("goog.loadModule(function() { goog.module('my.module'); }, '/my/file.js');"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isNonLegacyGoogModule()).isTrue(); + + m = metadata.getModulesByPath().get("testcode"); + assertThat(m.getGoogNamespaces()).isEmpty(); + assertThat(m.isScript()).isTrue(); + + m = metadata.getModulesByPath().get("/my/file.js"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isNonLegacyGoogModule()).isTrue(); + } + + public void testEs6Module() { + testSame("export var x;"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).isEmpty(); + assertThat(metadata.getModulesByPath().keySet()).contains("testcode"); + Module m = metadata.getModulesByPath().get("testcode"); + assertThat(m.getGoogNamespaces()).isEmpty(); + assertThat(m.isEs6Module()).isTrue(); + } + + public void testEs6ModuleDeclareNamespace() { + testSame("export var x; goog.module.declareNamespace('my.module');"); + assertThat(metadata.getModulesByGoogNamespace().keySet()).containsExactly("my.module"); + Module m = metadata.getModulesByGoogNamespace().get("my.module"); + assertThat(m.getGoogNamespaces()).containsExactly("my.module"); + assertThat(m.isEs6Module()).isTrue(); + assertThat(m.isGoogModule()).isFalse(); + } + + public void testCommonJsModule() { + testSame("exports = 0;"); + assertThat(metadata.getModulesByGoogNamespace()).isEmpty(); + Module m = metadata.getModulesByPath().get("testcode"); + assertThat(m.isCommonJs()).isTrue(); + } +} diff --git a/test/com/google/javascript/jscomp/deps/JsFileParserTest.java b/test/com/google/javascript/jscomp/deps/JsFileParserTest.java index 5e732828329..13f09f9c771 100644 --- a/test/com/google/javascript/jscomp/deps/JsFileParserTest.java +++ b/test/com/google/javascript/jscomp/deps/JsFileParserTest.java @@ -342,6 +342,28 @@ public void testParseEs6ModuleWithGoogProvide() { assertThat(errorManager.getWarnings()[0].getType()).isEqualTo(ModuleLoader.MODULE_CONFLICT); } + public void testEs6ModuleWithDeclareNamespace() { + ModuleLoader loader = + new ModuleLoader( + null, + ImmutableList.of("/foo"), + ImmutableList.of(), + ModuleLoader.ResolutionMode.BROWSER); + + String contents = "goog.module.declareNamespace('my.namespace');\nexport {};"; + + DependencyInfo expected = + SimpleDependencyInfo.builder("../bar/baz.js", "/foo/js/bar/baz.js") + .setProvides(ImmutableList.of("my.namespace", "module$js$bar$baz")) + .setLoadFlags(ImmutableMap.of("module", "es6")) + .build(); + + DependencyInfo result = + parser.setModuleLoader(loader).parseFile("/foo/js/bar/baz.js", "../bar/baz.js", contents); + + assertDeps(expected, result); + } + /** * Tests: * -Shortcut mode doesn't stop at setTestOnly() or declareLegacyNamespace().