From d5ccde0e398a63496ce9a7a130934ee88307ef78 Mon Sep 17 00:00:00 2001 From: lharker Date: Tue, 23 Apr 2019 20:49:52 -0700 Subject: [PATCH] Handle interop between goog.provides and goog.modules and fix typedef bug - declare legacy namespace exports in the global scope - goog.modules can require a goog.provide'd name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=244976803 --- .../javascript/jscomp/AstValidator.java | 15 +- .../jscomp/ModuleImportResolver.java | 38 ++-- .../ProcessClosureProvidesAndRequires.java | 34 +++- .../javascript/jscomp/TypedScopeCreator.java | 141 ++++++++++---- ...ProcessClosureProvidesAndRequiresTest.java | 36 ++++ .../jscomp/TypedScopeCreatorTest.java | 182 +++++++++++++++++- 6 files changed, 377 insertions(+), 69 deletions(-) diff --git a/src/com/google/javascript/jscomp/AstValidator.java b/src/com/google/javascript/jscomp/AstValidator.java index f0ec2526f4c..a4772189d80 100644 --- a/src/com/google/javascript/jscomp/AstValidator.java +++ b/src/com/google/javascript/jscomp/AstValidator.java @@ -466,16 +466,17 @@ private void validateNameType(Node nameNode) { private void validateCallType(Node callNode) { // TODO(b/74537281): Shouldn't CALL nodes always have a type, even if it is unknown? Node callee = callNode.getFirstChild(); - JSType calleeTypeI = + JSType calleeType = checkNotNull(callee.getJSType(), "Callee of\n\n%s\nhas no type.", callNode.toStringTree()); - if (calleeTypeI.isFunctionType()) { - FunctionType calleeFunctionTypeI = calleeTypeI.toMaybeFunctionType(); - JSType returnTypeI = calleeFunctionTypeI.getReturnType(); + if (calleeType.isFunctionType()) { + FunctionType calleeFunctionType = calleeType.toMaybeFunctionType(); + JSType returnType = calleeFunctionType.getReturnType(); // Skip this check if the call node was originally in a cast, because the cast type may be - // narrower than the return type. - if (callNode.getJSTypeBeforeCast() == null) { - expectMatchingTypeInformation(callNode, returnTypeI); + // narrower than the return type. Also skip the check if the function's return type is the + // any (formerly unknown) type, since we may have inferred a better type. + if (callNode.getJSTypeBeforeCast() == null && !returnType.isUnknownType()) { + expectMatchingTypeInformation(callNode, returnType); } } // TODO(b/74537281): What other cases should be covered? } diff --git a/src/com/google/javascript/jscomp/ModuleImportResolver.java b/src/com/google/javascript/jscomp/ModuleImportResolver.java index 472c692abea..dcecfcdf63e 100644 --- a/src/com/google/javascript/jscomp/ModuleImportResolver.java +++ b/src/com/google/javascript/jscomp/ModuleImportResolver.java @@ -15,6 +15,7 @@ */ package com.google.javascript.jscomp; +import static com.google.common.base.Preconditions.checkArgument; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.modules.Module; @@ -74,23 +75,36 @@ ScopedName getClosureNamespaceTypeFromCall(Node googRequire) { if (module == null) { return null; } + switch (module.metadata().moduleType()) { + case GOOG_PROVIDE: + // Expect this to be a global variable + Node provide = module.metadata().rootNode(); + if (provide != null && provide.isScript()) { + return ScopedName.of(moduleId, provide.getGrandparent()); + } else { + // Unknown module requires default to 'goog provides', but we don't want to type them. + return null; + } - Node scopeRoot = getModuleScopeRoot(module); - if (scopeRoot != null) { - return ScopedName.of("exports", scopeRoot); + case GOOG_MODULE: + case LEGACY_GOOG_MODULE: + // TODO(b/124919359): Fix getGoogModuleScopeRoot to never return null. + Node scopeRoot = getGoogModuleScopeRoot(module); + return scopeRoot != null ? ScopedName.of("exports", scopeRoot) : null; + case ES6_MODULE: + throw new IllegalStateException("Type checking ES modules not yet supported"); + case COMMON_JS: + throw new IllegalStateException("Type checking CommonJs modules not yet supported"); + case SCRIPT: + throw new IllegalStateException("Cannot import a name from a SCRIPT"); } - // TODO(b/124919359): assert that this is non-nullable once getModuleScopeRoot handles modules - // other than goog.modules. - return null; + throw new AssertionError(); } - /** Converts a {@link Module} reference to a {@link Node} scope root. */ + /** Returns the corresponding scope root Node from a goog.module. */ @Nullable - private Node getModuleScopeRoot(@Nullable Module module) { - if (!module.metadata().isGoogModule()) { - // TODO(b/124919359): also handle ES modules and goog.provides - return null; - } + private Node getGoogModuleScopeRoot(@Nullable Module module) { + checkArgument(module.metadata().isGoogModule(), module.metadata()); Node scriptNode = module.metadata().rootNode(); if (scriptNode.isScript() diff --git a/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java b/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java index 9537ae940e1..7f975ef4edb 100644 --- a/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java +++ b/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java @@ -54,6 +54,9 @@ *

This also annotates all provided namespace definitions `a.b = {};` with {@link * Node#IS_NAMESPACE} so that later passes know they refer to a goog.provide'd namespace. * + *

If modules have not been rewritten, this pass also includes legacy Closure module namespaces + * in the list of {@link ProvidedName}s. + * * @author chrisn@google.com (Chris Nokleberg) */ class ProcessClosureProvidesAndRequires { @@ -151,6 +154,20 @@ private void checkForLateOrMissingProvide(UnrecognizedRequire r) { private class CollectDefinitions implements NodeTraversal.Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { + // Don't recurse into modules, which cannot have goog.provides. We do need to handle legacy + // goog.modules, but we do that quickly here rather than descending all the way into them. + // We completely ignore ES modules and CommonJS modules. + if ((n.isModuleBody() && n.getParent().getBooleanProp(Node.GOOG_MODULE)) + || NodeUtil.isBundledGoogModuleScopeRoot(n)) { + Node googModuleCall = n.getFirstChild(); + String closureNamespace = googModuleCall.getFirstChild().getSecondChild().getString(); + Node maybeLegacyNamespaceCall = googModuleCall.getNext(); + if (maybeLegacyNamespaceCall != null + && NodeUtil.isGoogModuleDeclareLegacyNamespaceCall(maybeLegacyNamespaceCall)) { + processLegacyModuleCall(closureNamespace, googModuleCall, t.getModule()); + } + return false; + } return !n.isModuleBody(); } @@ -322,6 +339,19 @@ private void processRequireCall(NodeTraversal t, Node n, Node parent) { } } + /** Handles a goog.module that is a legacy namespace. */ + private void processLegacyModuleCall(String namespace, Node googModuleCall, JSModule module) { + registerAnyProvidedPrefixes(namespace, googModuleCall, module); + providedNames.put( + namespace, + new ProvidedName( + namespace, + googModuleCall, + module, + /* explicit= */ true, + /* fromPreviousProvide= */ false)); + } + /** Handles a goog.provide call. */ private void processProvideCall(NodeTraversal t, Node n, Node parent) { checkState(n.isCall()); @@ -700,8 +730,8 @@ private boolean hasCandidateDefinitionNotFromPreviousPass() { } /** - * Returns the `goog.provide` call that created this name, if any, or otherwise the first - * 'previous provide' assignmetn that created this name. + * Returns the `goog.provide` or legacy namespace `goog.module` call that created this name, if + * any, or otherwise the first 'previous provide' assignment that created this name. */ Node getFirstProvideCall() { return firstNode; diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 490ac4d00a7..ca2794e9449 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -243,6 +243,13 @@ void resolve() { requiredVar != null ? requiredVar.getType() : unknownType, compiler.getInput(moduleLocalNode.getInputId()), requiredVar == null || requiredVar.isTypeInferred()); + if (requiredVar != null && requiredVar.getNameNode().getTypedefTypeProp() != null) { + // Propagate the 'typedef type' from the module export to this variable. Otherwise + // NamedTypes pointing to the imported name fail to resolve. + JSType typedefType = requiredVar.getNameNode().getTypedefTypeProp(); + moduleLocalNode.setTypedefTypeProp(typedefType); + typeRegistry.declareType(localModuleScope, moduleLocalNode.getString(), typedefType); + } } } @@ -401,15 +408,16 @@ private TypedScope createScopeInternal(Node root, TypedScope typedParent) { newScope = new TypedScope(typedParent, root); } - if (root.isModuleBody() || isGoogLoadModuleBlock(root)) { - initializeModuleScope(root, newScope); + Module module = getModuleFromScopeRoot(root); + if (module != null) { + initializeModuleScope(root, module, newScope); } if (root.isFunction()) { scopeBuilder = new FunctionScopeBuilder(newScope); } else if (root.isClass()) { scopeBuilder = new ClassScopeBuilder(newScope); } else { - scopeBuilder = new NormalScopeBuilder(newScope); + scopeBuilder = new NormalScopeBuilder(newScope, module); } scopeBuilder.build(); @@ -426,6 +434,27 @@ private TypedScope createScopeInternal(Node root, TypedScope typedParent) { return newScope; } + /** Returns the {@link Module} corresponding to this scope root, or null if not a module root. */ + @Nullable + private Module getModuleFromScopeRoot(Node moduleBody) { + if (moduleBody.isModuleBody()) { + // TODO(b/128633181): handle ES modules here + Node scriptNode = moduleBody.getParent(); + checkState( + scriptNode.getBooleanProp(Node.GOOG_MODULE), + "Typechecking of non-goog-modules not supported"); + Node googModuleCall = moduleBody.getFirstChild(); + String namespace = googModuleCall.getFirstChild().getSecondChild().getString(); + return moduleMap.getClosureModule(namespace); + + } else if (isGoogLoadModuleBlock(moduleBody)) { + Node googModuleCall = moduleBody.getFirstChild(); + String namespace = googModuleCall.getFirstChild().getSecondChild().getString(); + return moduleMap.getClosureModule(namespace); + } + return null; + } + private static boolean isGoogLoadModuleBlock(Node scopeRoot) { return scopeRoot.isBlock() && scopeRoot.getParent().isFunction() @@ -433,28 +462,8 @@ private static boolean isGoogLoadModuleBlock(Node scopeRoot) { } /** Builds the beginning of a module-scope. This can be an ES module or a goog.module. */ - private void initializeModuleScope(Node moduleBody, TypedScope moduleScope) { - - Node googModuleCall; - if (moduleBody.isModuleBody()) { - Node scriptNode = moduleBody.getParent(); - if (scriptNode.getBooleanProp(Node.GOOG_MODULE)) { - googModuleCall = moduleBody.getFirstChild(); - } else { - googModuleCall = null; - } - } else { - checkArgument(moduleBody.isBlock()); - googModuleCall = moduleBody.getFirstChild(); - } - - if (googModuleCall != null) { - - Node namespace = googModuleCall.getFirstChild().getSecondChild(); - - String closureModuleNamespace = namespace.getString(); - Module module = moduleMap.getClosureModule(closureModuleNamespace); - + private void initializeModuleScope(Node moduleBody, Module module, TypedScope moduleScope) { + if (module.metadata().isGoogModule()) { declareExportsInModuleScope(module, moduleBody, moduleScope); markGoogModuleExportsAsConst(module); } @@ -589,7 +598,7 @@ void patchGlobalScope(TypedScope globalScope, Node scriptRoot) { } // Now re-traverse the given script. - NormalScopeBuilder scopeBuilder = new NormalScopeBuilder(globalScope); + NormalScopeBuilder scopeBuilder = new NormalScopeBuilder(globalScope, null); NodeTraversal.traverse(compiler, scriptRoot, scopeBuilder); } @@ -761,6 +770,9 @@ private abstract class AbstractScopeBuilder implements NodeTraversal.Callback { /** The InputId of the current node. */ private InputId inputId; + /** The Module object for this scope, if any. */ + private Module module; + /** * Some actions need to be deferred, such as analyzing object literals with * lends annotations, or resolving type-less stubs. These actions are added @@ -768,9 +780,10 @@ private abstract class AbstractScopeBuilder implements NodeTraversal.Callback { */ final Multimap deferredActions = HashMultimap.create(); - AbstractScopeBuilder(TypedScope scope) { + AbstractScopeBuilder(TypedScope scope, Module module) { this.currentScope = scope; this.currentHoistScope = scope.getClosestHoistScope(); + this.module = module; } /** Returns the current compiler input. */ @@ -782,6 +795,24 @@ CompilerInput getCompilerInput() { void build() { new NodeTraversal(compiler, this, ScopeCreator.ASSERT_NO_SCOPES_CREATED) .traverseAtScope(currentScope); + if (this.module != null && this.module.metadata().isLegacyGoogModule()) { + TypedVar exportsVar = currentScope.getSlot("exports"); // We declared exports already. + currentScope + .getGlobalScope() + .declare( + module.closureNamespace(), + exportsVar.getNameNode(), + exportsVar.getType(), + exportsVar.getInput(), + exportsVar.isTypeInferred()); + declareAliasTypeIfRvalueIsAliasable( + module.closureNamespace(), + exportsVar.getNameNode(), // Pretend that 'exports = '... is the lvalue node. + QualifiedName.of("exports"), + exportsVar.getType(), + currentScope, + currentScope.getGlobalScope()); + } } @Override @@ -1304,7 +1335,7 @@ private void defineModuleImport( TypedVar exportsVar = exportScope.getSlot(exportedName.getName()); final JSType type; if (exportsVar != null) { - maybeDeclareAliasType( + declareAliasTypeIfRvalueIsAliasable( localNameNode, QualifiedName.of(exportedName.getName()), exportsVar.getType(), @@ -2144,14 +2175,16 @@ && shouldUseFunctionLiteralType( || isGoogModuleExports(lValue)) { if (rValue != null) { JSType rValueType = getDeclaredRValueType(lValue, rValue); - maybeDeclareAliasType(lValue, rValue.getQualifiedNameObject(), rValueType, currentScope); + declareAliasTypeIfRvalueIsAliasable( + lValue, rValue.getQualifiedNameObject(), rValueType, currentScope); if (rValueType != null) { return rValueType; } } else if (declaredRValueTypeSupplier != null) { RValueInfo rvalueInfo = declaredRValueTypeSupplier.get(); if (rvalueInfo != null) { - maybeDeclareAliasType(lValue, rvalueInfo.qualifiedName, rvalueInfo.type, currentScope); + declareAliasTypeIfRvalueIsAliasable( + lValue, rvalueInfo.qualifiedName, rvalueInfo.type, currentScope); if (rvalueInfo.type != null) { return rvalueInfo.type; } @@ -2182,19 +2215,44 @@ && shouldUseFunctionLiteralType( * typeRegistry. For @typedefs and global @enums, this method also marks the qualified name * referring to the type as non-nullable by default. */ - private void maybeDeclareAliasType( + private void declareAliasTypeIfRvalueIsAliasable( Node lValue, @Nullable QualifiedName rValue, @Nullable JSType rValueType, TypedScope rValueLookupScope) { + if (!lValue.isQualifiedName()) { + return; + } + declareAliasTypeIfRvalueIsAliasable( + lValue.getQualifiedName(), lValue, rValue, rValueType, rValueLookupScope, currentScope); + } + + /** + * For a const alias, like `const alias = other.name`, this may declare `alias` as a type name, + * depending on what other.name is defined to be. + * + *

NOTE: in most cases, call the version with fewer arguments. This version only exists to + * handle goog.declareLegacyNamespace, which is strange compared to normal type aliasing because + * 1) there's no GETPROP node representing the lvalue and 2) the type is declared in the global + * scope, not the current module-local scope. + * + * @param aliasDeclarationScope The scope in which to declare the alias name. In most cases, + * this should just be the {@link #currentScope}. + */ + private void declareAliasTypeIfRvalueIsAliasable( + String lValueName, + @Nullable Node actualLvalueNode, + @Nullable QualifiedName rValue, + @Nullable JSType rValueType, + TypedScope rValueLookupScope, + TypedScope aliasDeclarationScope) { // NOTE: this allows some strange patterns such allowing instance properties // to be aliases of constructors, and then creating a local alias of that to be // used as a type name. Consider restricting this. - if (!lValue.isQualifiedName() || (rValue == null)) { + if (rValue == null) { return; } - String lValueName = lValue.getQualifiedName(); // Look for a @typedef annotation on the definition node Node definitionNode = getDefinitionNode(rValue, rValueLookupScope); @@ -2202,9 +2260,9 @@ private void maybeDeclareAliasType( JSType typedefType = definitionNode.getTypedefTypeProp(); if (typedefType != null) { // Propagate typedef type to typedef aliases. - lValue.setTypedefTypeProp(typedefType); + actualLvalueNode.setTypedefTypeProp(typedefType); typeRegistry.identifyNonNullableName(lValueName); - typeRegistry.declareType(currentScope, lValueName, typedefType); + typeRegistry.declareType(aliasDeclarationScope, lValueName, typedefType); return; } } @@ -2218,8 +2276,7 @@ private void maybeDeclareAliasType( && rValueType.toMaybeFunctionType().hasInstanceType()) { // Look for @constructor/@interface by checking if the RHS has an instance type FunctionType functionType = rValueType.toMaybeFunctionType(); - typeRegistry.declareType( - currentScope, lValue.getQualifiedName(), functionType.getInstanceType()); + typeRegistry.declareType(aliasDeclarationScope, lValueName, functionType.getInstanceType()); return; } @@ -2227,7 +2284,7 @@ private void maybeDeclareAliasType( // Look for cases where the rValue is an Enum namespace typeRegistry.declareType( currentScope, lValueName, rValueType.toMaybeEnumType().getElementsType()); - if (isLValueRootedInGlobalScope(lValue)) { + if (isLValueRootedInGlobalScope(actualLvalueNode)) { // TODO(b/123710194): Also make local aliases non-nullable typeRegistry.identifyNonNullableName(lValueName); } @@ -2815,8 +2872,8 @@ void declarePropertyIfNamespaceType( /** A shallow traversal of the global scope to build up all classes, functions, and methods. */ private final class NormalScopeBuilder extends AbstractScopeBuilder { - NormalScopeBuilder(TypedScope scope) { - super(scope); + NormalScopeBuilder(TypedScope scope, Module module) { + super(scope, module); } @Override @@ -2921,7 +2978,7 @@ void visitPostorder(NodeTraversal t, Node n, Node parent) { private final class FunctionScopeBuilder extends AbstractScopeBuilder { FunctionScopeBuilder(TypedScope scope) { - super(scope); + super(scope, null); } @Override @@ -3168,7 +3225,7 @@ private final class ClassScopeBuilder extends AbstractScopeBuilder { private Table getterSetterTypes = null; ClassScopeBuilder(TypedScope scope) { - super(scope); + super(scope, null); } @Override diff --git a/test/com/google/javascript/jscomp/ProcessClosureProvidesAndRequiresTest.java b/test/com/google/javascript/jscomp/ProcessClosureProvidesAndRequiresTest.java index 8062ddf505a..249f0c955dd 100644 --- a/test/com/google/javascript/jscomp/ProcessClosureProvidesAndRequiresTest.java +++ b/test/com/google/javascript/jscomp/ProcessClosureProvidesAndRequiresTest.java @@ -1234,6 +1234,42 @@ public void testSimpleProvidedNameCollection() { assertThat(providedNameMap.keySet()).containsExactly("goog", "a", "a.b", "a.b.c", "a.b.d"); } + @Test + public void testLegacyGoogModule() { + Map providedNameMap = + getProvidedNameCollection( + lines( + "goog.module('a.b.c');", // + "goog.module.declareLegacyNamespace();", + "", + "exports = class {};")); + + assertThat(providedNameMap.keySet()).containsExactly("goog", "a", "a.b", "a.b.c"); + } + + @Test + public void testLegacyGoogModule_inLoadModuleCall() { + Map providedNameMap = + getProvidedNameCollection( + lines( + "goog.loadModule(function(exports) {", + " goog.module('a.b.c');", // + " goog.module.declareLegacyNamespace();", + " exports = class {};", + " return exports;", + "});")); + + assertThat(providedNameMap.keySet()).containsExactly("goog", "a", "a.b", "a.b.c"); + } + + @Test + public void testEsModule_ignored() { + Map providedNameMap = + getProvidedNameCollection("goog.declareModuleId('a.b.c'); export const x = 0;"); + + assertThat(providedNameMap.keySet()).containsExactly("goog"); + } + private Map getProvidedNameCollection(String js) { Compiler compiler = createCompiler(); ProcessClosureProvidesAndRequires processor = diff --git a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java index 0584957ba71..4d2633b45d3 100644 --- a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java +++ b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java @@ -4628,7 +4628,7 @@ public void testGoogModuleRequireAndRequireType_namedExportOfClasses() { @Test public void testGoogModuleRequireAndRequireType_typedef() { - testWarning( + testSame( srcs( lines( "goog.module('a.Foo');", @@ -4639,12 +4639,13 @@ public void testGoogModuleRequireAndRequireType_typedef() { "goog.module('b.Bar');", "/** @typedef {number} */", "let numType;", - "exports = numType;")), - // TODO(b/124919359): We should recognize 'Bar'. - warning(RhinoErrorReporter.UNRECOGNIZED_TYPE_ERROR)); + "exports = numType;"))); Node fNode = getLabeledStatement("X").statementNode.getOnlyChild(); - assertNode(fNode).hasJSTypeThat().isUnknown(); + assertNode(fNode).hasJSTypeThat().isNumber(); + + Node barDeclaration = getLabeledStatement("X").enclosingScope.getVar("Bar").getNameNode(); + assertType(barDeclaration.getTypedefTypeProp()).isNumber(); } @Test @@ -5081,7 +5082,175 @@ public void testGoogProvide_overwritingExternsFunction() { warning(TypeValidator.DUP_VAR_DECLARATION_TYPE_MISMATCH)); } - // TODO(b/124919359): Verify that you can access legacy namespaces in code. + @Test + public void testLegacyGoogLoadModule_accessibleWithGoogRequire_exportingConstructor() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.loadModule(function (exports) {", + " goog.module('mod.A');", + " goog.module.declareLegacyNamespace();", + "", + " exports = class A {};", + "});"), + lines( + "goog.require('mod.A');", // + "const /** !mod.A */ a = new mod.A();", + "A: a;"))); + + assertType(getLabeledStatement("A").statementNode.getOnlyChild().getJSType()) + .toStringIsEqualTo("exports"); + } + + @Test + public void testLegacyGoogModule_accessibleWithGoogRequire_exportingConstructor() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod.A');", + "goog.module.declareLegacyNamespace();", + "", + "exports = class A {};"), + lines( + "goog.require('mod.A');", // + "const /** !mod.A */ a = new mod.A();", + "A: a;"))); + + assertType(getLabeledStatement("A").statementNode.getOnlyChild().getJSType()) + .toStringIsEqualTo("exports"); + } + + @Test + public void testLegacyGoogModule_accessibleWithGoogRequire_exportingLocalTypedef() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod.A');", + "goog.module.declareLegacyNamespace();", + "", + "/** @typedef {number} */", + "let numType;", + "", + "exports = numType;"), + lines( + "goog.require('mod.A');", // + "var /** !mod.A */ x;", + "X: x"))); + + assertType(getLabeledStatement("X").statementNode.getOnlyChild().getJSType()).isNumber(); + } + + @Test + public void testLegacyGoogModule_accessibleWithGoogRequire_exportingNamespace() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod');", + "goog.module.declareLegacyNamespace();", + "", + "exports.A = class A {};"), + lines( + "goog.require('mod');", // + "const /** !mod.A */ a = new mod.A();"))); + } + + @Test + public void testLegacyGoogModule_withNamedExport_extendedByGoogProvide() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod');", + "goog.module.declareLegacyNamespace();", + "", + "exports.A = class A {};"), + lines( + "goog.provide('mod.B');", // This is bad style, but probably people do it. + "mod.B = class B {};"))); + + JSType modType = globalScope.getVar("mod").getType(); + assertType(modType).withTypeOfProp("A").toStringIsEqualTo("function(new:exports.A): undefined"); + assertType(modType).withTypeOfProp("B").toStringIsEqualTo("function(new:mod.B): undefined"); + } + + @Test + public void testLegacyGoogModule_withDefaultExport_extendedByGoogProvide() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod');", + "goog.module.declareLegacyNamespace();", + "", + "/** @return {number} */", + "exports = function() { return 0; };"), + lines( + "goog.provide('mod.B');", // This is bad style, but probably people do it. + "mod.B = class B {};"))); + + JSType modType = globalScope.getVar("mod").getType(); + assertType(modType).isFunctionTypeThat().hasReturnTypeThat().isNumber(); + assertType(modType).withTypeOfProp("B").toStringIsEqualTo("function(new:mod.B): undefined"); + } + + @Test + public void testLegacyGoogModule_accessibleWithGoogRequire_exportingTypedef() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.module('mod');", + "goog.module.declareLegacyNamespace();", + "", + "/** @typedef {number} */", + "exports.numType;"), + lines( + "goog.require('mod');", // + "var /** !mod.numType */ a;"))); + + JSType modType = globalScope.getVar("a").getType(); + assertType(modType).isNumber(); + } + + @Test + public void testGoogModuleRequiringGoogProvide_class() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.provide('a.b.Foo')", // + "a.b.Foo = class {};"), + lines( + "goog.module('c');", + "const Foo = goog.require('a.b.Foo');", + "var /** !Foo */ x;", + "X: x;"))); + + assertType(getLabeledStatement("X").statementNode.getOnlyChild().getJSType()) + .toStringIsEqualTo("a.b.Foo"); + } + + @Test + public void testGoogModuleRequiringGoogProvide_classWithDestructuring() { + testSame( + srcs( + CLOSURE_GLOBALS, + lines( + "goog.provide('a.b')", // + "a.b.Foo = class {};"), + lines( + "goog.module('c');", + "const {Foo} = goog.require('a.b');", + "var /** !Foo */ x;", + "X: x;"))); + + assertType(getLabeledStatement("X").statementNode.getOnlyChild().getJSType()) + .toStringIsEqualTo("a.b.Foo"); + } @Test public void testMemoization() { @@ -5159,6 +5328,7 @@ private ObjectType getNativeObjectType(JSTypeNative type) { lines( "var goog = {};", "goog.module = function(name) {};", + "/** @return {?} */", "goog.require = function(id) {};", "goog.provide = function(id) {};"); }