From 562a178c15c7cb4f6f616dc0fe2abda3ef084961 Mon Sep 17 00:00:00 2001 From: johnplaisted Date: Fri, 6 Apr 2018 10:23:47 -0700 Subject: [PATCH] Roll forward of Rework interop between closure files and ES6 modules. - Fix goog.require and goog: import in ES6 modules for non-legacy goog.modules. - Add support for a new path based goog.require to require ES6 modules from goog modules. - ES6 modules can call "goog.module.declareNamespace('namespace')" to declare a non-legacy goog.module-like namespace. This is for backwards compatibility so users can update their goog.modules to ES6 modules in place. Note this migration path doesn't work for default exports (there is no analogy for "exports = x" in ES6 modules) or for legacy modules (goog.module.declareNamespace does not declare a legacy namespace and we won't add support for it). Library and deps generation support for this will be added in a future change. NEW: Whitelist polymer for the new mixed module type error. Polymer is generating files with goog.provide and es6 imports because there is no good interop story yet. These files are 100% side effect based, thus nothing should ever reference them, and rewriting doesn't matter. Automated g4 rollback of changelist 191366556. *** Reason for rollback *** Roll forward, whitelisting polymer. *** Original change description *** Automated g4 rollback of changelist 191347644. *** Reason for rollback *** Tap failures (polymer emits mixed modules). *** Original change description *** Rework interop between closure files and ES6 modules. - Fix goog.require and goog: import in ES6 modules for non-legacy goog.modules. - Add support for a new path based goog.require to require ES6 modules from goog modules. - ES6 modules can call "goog.module.declareNamespace('namespace')" to declare a non-legacy goog.module-like namespac... *** Public: Rework interop between closure files and ES6 modules. - Fix goog.require and goog: import in ES6 modules for non-legacy goog.modules. - Add support for a new path based goog.require to require ES6 modules from goog modules. - ES6 modules can call "goog.module.declareNamespace('namespace')" to declare a non-legacy goog.module-like namespace. This is for backwards compatibility so users can update their goog.modules to ES6 modules in place. Note this migration path doesn't work for default exports (there is no analogy for "exports = x" in ES6 modules) or for legacy modules (goog.module.declareNamespace does not declare a legacy namespace and we won't add support for it). Library and deps generation support for this will be added in a future change. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=191905296 --- .../jscomp/CheckMissingAndExtraRequires.java | 7 +- .../javascript/jscomp/ClosureCheckModule.java | 16 +- .../jscomp/ClosureRewriteModule.java | 6 +- .../javascript/jscomp/CompilerInput.java | 8 + .../javascript/jscomp/Es6RewriteModules.java | 303 +++++++++-- .../jscomp/FindModuleDependencies.java | 33 +- .../javascript/jscomp/ModuleMetadata.java | 506 ++++++++++++++++++ .../jscomp/TranspilationPasses.java | 6 +- .../javascript/jscomp/deps/JsFileParser.java | 12 +- .../jscomp/gwt/client/JsfileParser.java | 7 + .../jscomp/ClosureCheckModuleTest.java | 6 +- .../jscomp/Es6RewriteModulesTest.java | 136 +---- .../Es6RewriteModulesWithGoogInteropTest.java | 401 ++++++++++++++ .../javascript/jscomp/ModuleMetadataTest.java | 161 ++++++ .../jscomp/deps/JsFileParserTest.java | 22 + 15 files changed, 1426 insertions(+), 204 deletions(-) create mode 100644 src/com/google/javascript/jscomp/ModuleMetadata.java create mode 100644 test/com/google/javascript/jscomp/Es6RewriteModulesWithGoogInteropTest.java create mode 100644 test/com/google/javascript/jscomp/ModuleMetadataTest.java 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().