From 1ccb534b35ffd4a3bc2f11a8f6ddfef6314a7ea2 Mon Sep 17 00:00:00 2001 From: lharker Date: Mon, 6 May 2019 11:46:35 -0700 Subject: [PATCH] Support 'import *', circular imports and goog.require(ES module) in typechecking We declare a dummy variable *exports* in every ES module to cache the return type of 'import *' and goog.require. This change also stops relying on 'FunctionType::getReferenceName' being a globally unique identifier when building a constructor/interface. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=246866865 --- .../jscomp/ModuleImportResolver.java | 149 ++++++++-- .../javascript/jscomp/TypeInference.java | 37 ++- .../javascript/jscomp/TypeInferencePass.java | 18 +- .../javascript/jscomp/TypedScopeCreator.java | 129 +++++---- .../javascript/rhino/testing/TypeSubject.java | 8 + .../javascript/jscomp/SymbolTableTest.java | 12 + .../jscomp/TypedScopeCreatorTest.java | 260 +++++++++++++++++- 7 files changed, 507 insertions(+), 106 deletions(-) diff --git a/src/com/google/javascript/jscomp/ModuleImportResolver.java b/src/com/google/javascript/jscomp/ModuleImportResolver.java index 51d3cf2fb4c..653cc85ff1c 100644 --- a/src/com/google/javascript/jscomp/ModuleImportResolver.java +++ b/src/com/google/javascript/jscomp/ModuleImportResolver.java @@ -18,16 +18,19 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.javascript.jscomp.deps.ModuleNames; import com.google.javascript.jscomp.modules.Binding; import com.google.javascript.jscomp.modules.Export; import com.google.javascript.jscomp.modules.Module; import com.google.javascript.jscomp.modules.ModuleMap; +import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.JSTypeNative; import com.google.javascript.rhino.jstype.JSTypeRegistry; +import com.google.javascript.rhino.jstype.ObjectType; import java.util.Map; import java.util.function.Function; import javax.annotation.Nullable; @@ -75,7 +78,7 @@ static boolean isGoogModuleDependencyCall(Node value) { * *

This returns null if the given {@link ModuleMap} is null, if the required module does not * exist, or if support is missing for the type of required {@link Module}. Currently only - * requires of other goog.modules are supported. + * requires of goog.modules, goog.provides, and ES module with goog.declareModuleId are supported. * * @param googRequire a CALL node representing some kind of Closure require. */ @@ -106,7 +109,8 @@ ScopedName getClosureNamespaceTypeFromCall(Node googRequire) { 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"); + Node moduleBody = module.metadata().rootNode().getFirstChild(); // SCRIPT -> MODULE_BODY + return ScopedName.of(Export.NAMESPACE, moduleBody); case COMMON_JS: throw new IllegalStateException("Type checking CommonJs modules not yet supported"); case SCRIPT: @@ -139,10 +143,19 @@ private Node getGoogModuleScopeRoot(@Nullable Module module) { return null; } - /** Declares/updates the type of all bindings imported into the ES module scope */ - void declareEsModuleImports(Module module, TypedScope scope, CompilerInput moduleInput) { + /** + * Declares/updates the type of all bindings imported into the ES module scope + * + * @return A map from local nodes to ScopedNames for which {@link #nodeToScopeMapper} couldn't + * find a scope, despite the original module existing. This is expected to happen for circular + * references if not all module scopes are created and the caller should handle declaring + * these names later, e.g. in TypedScopeCreator. + */ + Map declareEsModuleImports( + Module module, TypedScope scope, CompilerInput moduleInput) { checkArgument(module.metadata().isEs6Module(), module); checkArgument(scope.isModuleScope(), scope); + ImmutableMap.Builder missingNames = ImmutableMap.builder(); for (Map.Entry boundName : module.boundNames().entrySet()) { Binding binding = boundName.getValue(); String localName = boundName.getKey(); @@ -152,35 +165,111 @@ void declareEsModuleImports(Module module, TypedScope scope, CompilerInput modul // ES imports fall into two categories: // - namespace imports. These correspond to an object type containing all named exports. // - named imports. These always correspond, eventually, to a name local to a module. - // Note that we include default imports in this case. - if (binding.isModuleNamespace()) { - // TODO(b/128633181): Support import *. - scope.declare( - localName, - binding.sourceNode(), - registry.getNativeType(JSTypeNative.UNKNOWN_TYPE), - moduleInput); - } else { - Export originatingExport = binding.originatingExport(); - Node exportModuleRoot = originatingExport.moduleMetadata().rootNode().getFirstChild(); - TypedScope modScope = nodeToScopeMapper.apply(exportModuleRoot); - // NB: If the original export was an `export default` then the local name is *default*. - // We've already declared a dummy variable named `*default*` in the scope. - TypedVar originalVar = modScope.getSlot(originatingExport.localName()); - JSType importType = originalVar.getType(); - scope.declare( - localName, - binding.sourceNode(), - importType, - moduleInput, - /* inferred= */ originalVar.isTypeInferred()); - if (originalVar.getNameNode().getTypedefTypeProp() != null - && binding.sourceNode().getTypedefTypeProp() == null) { - binding.sourceNode().setTypedefTypeProp(originalVar.getNameNode().getTypedefTypeProp()); - registry.declareType(scope, localName, originalVar.getNameNode().getTypedefTypeProp()); + // Note that we include imports of an `export default` in this case and map them to a + // pseudo-variable named *default*. + ScopedName export = getScopedNameFromEsBinding(binding); + TypedScope modScope = nodeToScopeMapper.apply(export.getScopeRoot()); + if (modScope == null) { + missingNames.put(binding.sourceNode(), export); + continue; + } + + TypedVar originalVar = modScope.getVar(export.getName()); + JSType importType = originalVar.getType(); + scope.declare( + localName, + binding.sourceNode(), + importType, + moduleInput, + /* inferred= */ originalVar.isTypeInferred()); + + // Non-namespace imports may be typedefs; if so, propagate the typedef prop onto the + // export and import bindings, if not already there. + if (!binding.isModuleNamespace() && binding.sourceNode().getTypedefTypeProp() == null) { + JSType typedefType = originalVar.getNameNode().getTypedefTypeProp(); + if (typedefType != null) { + binding.sourceNode().setTypedefTypeProp(typedefType); + registry.declareType(scope, localName, typedefType); } } } + return missingNames.build(); + } + + /** + * Declares or updates the type of properties representing exported names from ES module + * + *

When the given object type does not have existing properties corresponding to exported + * names, this method adds new properties to the object type. If the object type already has + * properties, this method will ignore declared properties and update the type of inferred + * properties. + * + *

The additional properties will be inferred (instead of declared) if and only if {@link + * TypedVar#isTypeInferred()} is true for the original exported name. + * + *

We create this type to support 'import *' and goog.requires of this module. Note: we could + * lazily initialize this type if always creating it hurts performance. + * + * @param namespace An object type which may already have properties representing exported names. + * @param scope The scope rooted at the given module. + */ + void updateEsModuleNamespaceType(ObjectType namespace, Module module, TypedScope scope) { + checkArgument(module.metadata().isEs6Module(), module); + checkArgument(scope.isModuleScope(), scope); + + for (Map.Entry boundName : module.namespace().entrySet()) { + String exportKey = boundName.getKey(); + if (namespace.isPropertyTypeDeclared(exportKey)) { + // Cannot change the type of a declared property after it is added to the ObjectType. + continue; + } + + Binding binding = boundName.getValue(); + Node bindingSourceNode = binding.sourceNode(); // e.g. 'x' in `export let x;` or `export {x};` + ScopedName export = getScopedNameFromEsBinding(binding); + TypedScope originalScope = + export.getScopeRoot() == scope.getRootNode() + ? scope + : nodeToScopeMapper.apply(export.getScopeRoot()); + if (originalScope == null) { + // Exporting an import from an invalid module load or early reference. + namespace.defineInferredProperty( + exportKey, registry.getNativeType(JSTypeNative.UNKNOWN_TYPE), bindingSourceNode); + continue; + } + + TypedVar originalName = originalScope.getSlot(export.getName()); + JSType exportType = originalName.getType(); + if (originalName.isTypeInferred()) { + // NB: this method may be either adding a new inferred property or updating the type of an + // existing inferred property. + namespace.defineInferredProperty(exportKey, exportType, bindingSourceNode); + } else { + namespace.defineDeclaredProperty(exportKey, exportType, bindingSourceNode); + } + + bindingSourceNode.setTypedefTypeProp(originalName.getNameNode().getTypedefTypeProp()); + } + } + + /** Given a Binding from an ES module, return the name and scope of the bound name. */ + private static ScopedName getScopedNameFromEsBinding(Binding binding) { + // NB: If the original export was an `export default` then the local name is *default*. + // We've already declared a dummy variable named `*default*` in the scope. + String name = binding.isModuleNamespace() ? Export.NAMESPACE : binding.boundName(); + ModuleMetadata originalMetadata = + binding.isModuleNamespace() + ? binding.metadata() + : binding.originatingExport().moduleMetadata(); + if (!originalMetadata.isEs6Module()) { + // Importing SCRIPTs should not allow you to look up names in scope. + return ScopedName.of(name, null); + } + Node scriptNode = originalMetadata.rootNode(); + // Imports of nonexistent modules have a null 'root node'. Imports of names from scripts are + // meaningless. + checkState(scriptNode == null || scriptNode.isScript(), scriptNode); + return ScopedName.of(name, scriptNode != null ? scriptNode.getOnlyChild() : null); } /** Returns the {@link Module} corresponding to this scope root, or null if not a module root. */ diff --git a/src/com/google/javascript/jscomp/TypeInference.java b/src/com/google/javascript/jscomp/TypeInference.java index 27b9a9a5f82..b3dc6531c0d 100644 --- a/src/com/google/javascript/jscomp/TypeInference.java +++ b/src/com/google/javascript/jscomp/TypeInference.java @@ -40,6 +40,8 @@ import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec; import com.google.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; +import com.google.javascript.jscomp.modules.Export; +import com.google.javascript.jscomp.modules.Module; import com.google.javascript.jscomp.type.FlowScope; import com.google.javascript.jscomp.type.ReverseAbstractInterpreter; import com.google.javascript.rhino.JSDocInfo; @@ -349,10 +351,34 @@ FlowScope flowThrough(Node n, FlowScope input) { return input; } + // This method also does some logic for ES modules right before and after entering/exiting the + // scope rooted at the module. The reasoning for separating out this logic is that we can just + // ignore the actual AST nodes for IMPORT/EXPORT, in most cases, because we have already + // created an abstraction of imports and exports. Node root = NodeUtil.getEnclosingScopeRoot(n); - FlowScope output = input.withSyntacticScope(scopeCreator.createScope(root)); + // Inferred types of ES module imports/exports aren't knowable until after TypeInference runs. + // First update the type of all imports in the scope, then do flow-sensitive inference, then + // update the implicit '*exports*' object. + Module module = moduleImportResolver.getModuleFromScopeRoot(root); + TypedScope syntacticBlockScope = scopeCreator.createScope(root); + if (module != null && module.metadata().isEs6Module()) { + moduleImportResolver.declareEsModuleImports( + module, syntacticBlockScope, compiler.getInput(n.getInputId())); + } + + // This logic is not specific to ES modules. + FlowScope output = input.withSyntacticScope(syntacticBlockScope); output = inferDeclarativelyUnboundVarsWithoutTypes(output); output = traverse(n, output); + + if (module != null && module.metadata().isEs6Module()) { + // This call only affects exports with an inferred, not declared, type. Declared exports were + // already added to the namespace object type in TypedScopeCreator. + moduleImportResolver.updateEsModuleNamespaceType( + syntacticBlockScope.getVar(Export.NAMESPACE).getType().toObjectType(), + module, + syntacticBlockScope); + } return output; } @@ -715,8 +741,14 @@ private FlowScope traverse(Node n, FlowScope scope) { break; case EXPORT: + scope = traverseChildren(n, scope); if (n.getBooleanProp(Node.EXPORT_DEFAULT)) { - scope = traverseChildren(n, scope); + // TypedScopeCreator declared a dummy variable *default* to store this type. Update the + // variable with the inferred type. + TypedVar defaultExport = getDeclaredVar(scope, Export.DEFAULT_EXPORT_NAME); + if (defaultExport.isTypeInferred()) { + defaultExport.setType(getJSType(n.getOnlyChild())); + } } break; @@ -744,6 +776,7 @@ private FlowScope traverse(Node n, FlowScope scope) { case IMPORT: case IMPORT_SPEC: case IMPORT_SPECS: + case EXPORT_SPECS: // These don't need to be typed here, since they only affect control flow. break; diff --git a/src/com/google/javascript/jscomp/TypeInferencePass.java b/src/com/google/javascript/jscomp/TypeInferencePass.java index d3541d7265a..42cf6a36ecd 100644 --- a/src/com/google/javascript/jscomp/TypeInferencePass.java +++ b/src/com/google/javascript/jscomp/TypeInferencePass.java @@ -20,7 +20,6 @@ import com.google.javascript.jscomp.CodingConvention.AssertionFunctionLookup; import com.google.javascript.jscomp.NodeTraversal.AbstractScopedCallback; -import com.google.javascript.jscomp.modules.Module; import com.google.javascript.jscomp.type.ReverseAbstractInterpreter; import com.google.javascript.rhino.Node; @@ -39,7 +38,6 @@ class TypeInferencePass implements CompilerPass { private final TypedScope topScope; private final TypedScopeCreator scopeCreator; private final AssertionFunctionLookup assertionFunctionLookup; - private final ModuleImportResolver moduleImportResolver; TypeInferencePass( AbstractCompiler compiler, @@ -52,11 +50,6 @@ class TypeInferencePass implements CompilerPass { this.scopeCreator = scopeCreator; this.assertionFunctionLookup = AssertionFunctionLookup.of(compiler.getCodingConvention().getAssertionFunctions()); - this.moduleImportResolver = - new ModuleImportResolver( - compiler.getModuleMap(), - this.scopeCreator.getNodeToScopeMapper(), - compiler.getTypeRegistry()); } /** @@ -115,13 +108,6 @@ compiler, new SecondScopeBuildingCallback(), scopeCreator)) } private void inferScope(Node n, TypedScope scope) { - // Inferred types of ES module imports/exports aren't knowable until after TypeInference runs. - // First update the type of all imports in the scope, then do flow-sensitive inference. - Module module = moduleImportResolver.getModuleFromScopeRoot(scope.getRootNode()); - if (module != null && module.metadata().isEs6Module()) { - moduleImportResolver.declareEsModuleImports(module, scope, compiler.getInput(n.getInputId())); - } - TypeInference typeInference = new TypeInference( compiler, @@ -156,7 +142,9 @@ public void enterScope(NodeTraversal t) { // This ensures that incremental compilation only touches the root // that's been swapped out. TypedScope scope = t.getTypedScope(); - if (!scope.isBlockScope()) { // ignore scopes that don't have their own CFGs. + if (!scope.isBlockScope() && !scope.isModuleScope()) { + // ignore scopes that don't have their own CFGs and module scopes, which are visited + // as if they were a regular script. inferScope(t.getCurrentNode(), scope); } } diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 01dd31d05c8..da205587b4b 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -97,6 +97,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -431,6 +432,19 @@ private TypedScope createScopeInternal(Node root, TypedScope typedParent) { codingConvention.defineDelegateProxyPrototypeProperties( typeRegistry, delegateProxies, delegateCallingConventions); } + if (module != null && module.metadata().isEs6Module()) { + // Declare an implicit variable representing the namespace of this module, then add a property + // for each exported name to that variable's type. + ObjectType namespace = typeRegistry.createAnonymousObjectType(null); + newScope.declare( + Export.NAMESPACE, + root, // Use the given MODULE_BODY as the 'declaration node' for lack of a better option. + namespace, + compiler.getInput(root.getInputId()), + /* inferred= */ false); + + moduleImportResolver.updateEsModuleNamespaceType(namespace, module, newScope); + } return newScope; } @@ -443,8 +457,14 @@ private void initializeModuleScope(Node moduleBody, Module module, TypedScope mo } else { // For now, assume this is an ES module. In the future, it might be a CommonJS module. checkState(module.metadata().isEs6Module(), "CommonJS module typechecking not supported yet"); - moduleImportResolver.declareEsModuleImports( - module, moduleScope, compiler.getInput(moduleBody.getInputId())); + + Map unresolvedImports = + moduleImportResolver.declareEsModuleImports( + module, moduleScope, compiler.getInput(moduleBody.getInputId())); + weakImports.addAll( + unresolvedImports.entrySet().stream() + .map(entry -> new WeakModuleImport(entry.getKey(), entry.getValue(), moduleScope)) + .collect(Collectors.toList())); } } @@ -1311,17 +1331,10 @@ private void defineModuleImport( TypedScope exportScope = exportedName.getScopeRoot() != null ? memoized.get(exportedName.getScopeRoot()) : null; if (exportScope != null) { - TypedVar exportsVar = exportScope.getSlot(exportedName.getName()); - final JSType type; - if (exportsVar != null) { + JSType type = exportScope.lookupQualifiedName(QualifiedName.of(exportedName.getName())); + if (type != null) { declareAliasTypeIfRvalueIsAliasable( - localNameNode, - QualifiedName.of(exportedName.getName()), - exportsVar.getType(), - exportScope); - type = exportsVar.getType(); - } else { - type = null; + localNameNode, QualifiedName.of(exportedName.getName()), type, exportScope); } new SlotDefiner() @@ -1437,6 +1450,12 @@ private FunctionType createClassTypeFromNodes( ObjectType classPrototypeType = classType.getPrototypeProperty(); classPrototypeType.defineDeclaredProperty("constructor", qmarkCtor, constructor); } + if (classType.hasInstanceType()) { + Property classPrototype = classType.getSlot("prototype"); + // SymbolTable users expect the class prototype and actual class to have the same + // declaration node. + classPrototype.setNode(lvalueNode != null ? lvalueNode : classPrototype.getNode()); + } return classType; } @@ -1683,10 +1702,22 @@ private FunctionType createFunctionTypeFromNodes( fallbackReceiverType = currentScope.getTypeOfThis(); } - return builder - .inferThisType(info, fallbackReceiverType) - .inferParameterTypes(parametersNode, info) - .buildAndRegister(); + FunctionType fnType = + builder + .inferThisType(info, fallbackReceiverType) + .inferParameterTypes(parametersNode, info) + .buildAndRegister(); + + // Do some additional validation for constructors and interfaces. + if (fnType.hasInstanceType() && lvalueNode != null) { + Property prototypeSlot = fnType.getSlot("prototype"); + + // We want to make sure that the function and its prototype are declared at the same node. + // This consistency is helpful to users of SymbolTable, because everything gets declared at + // the same place. + prototypeSlot.setNode(lvalueNode); + } + return fnType; } /** @@ -1946,28 +1977,25 @@ void defineSlot() { // The input may be null if we are working with a AST snippet. So read // the extern info from the node. - TypedVar newVar = null; // declared in closest scope? CompilerInput input = compiler.getInput(inputId); if (!scopeToDeclareIn.canDeclare(variableName)) { TypedVar oldVar = scopeToDeclareIn.getVar(variableName); - newVar = - validator.expectUndeclaredVariable( - sourceName, input, declarationNode, parent, oldVar, variableName, type); + validator.expectUndeclaredVariable( + sourceName, input, declarationNode, parent, oldVar, variableName, type); } else { if (type != null) { setDeferredType(declarationNode, type); } - newVar = - declare( - scopeToDeclareIn, - variableName, - declarationNode, - type, - input, - allowLaterTypeInference); + declare( + scopeToDeclareIn, + variableName, + declarationNode, + type, + input, + allowLaterTypeInference); } // We need to do some additional work for constructors and interfaces. @@ -1983,7 +2011,7 @@ void defineSlot() { // the variable name is a sufficient check for this. if (fnType.isConstructor() || fnType.isInterface()) { finishConstructorDefinition( - declarationNode, variableName, fnType, scopeToDeclareIn, input, newVar); + declarationNode, variableName, fnType, scopeToDeclareIn, input); } } @@ -2034,8 +2062,11 @@ private TypedVar declare( } private void finishConstructorDefinition( - Node n, String variableName, FunctionType fnType, - TypedScope scopeToDeclareIn, CompilerInput input, TypedVar newVar) { + Node declarationNode, + String variableName, + FunctionType fnType, + TypedScope scopeToDeclareIn, + CompilerInput input) { // Declare var.prototype in the scope chain. FunctionType superClassCtor = fnType.getSuperClassConstructor(); Property prototypeSlot = fnType.getSlot("prototype"); @@ -2052,35 +2083,12 @@ private void finishConstructorDefinition( scopeToDeclareIn.declare( prototypeName, - n, + declarationNode, prototypeSlot.getType(), input, // declared iff there's an explicit supertype superClassCtor == null || superClassCtor.getInstanceType().isEquivalentTo(getNativeType(OBJECT_TYPE))); - - // Only do the following at the initial initialization of the constructor, not on an - // alias. - if (variableName.equals(fnType.getReferenceName())) { - // Make sure the variable is initialized to something if it constructs itself. - if (newVar.getInitialValue() == null && !n.isFromExterns()) { - report( - JSError.make( - n, fnType.isConstructor() ? CTOR_INITIALIZER : IFACE_INITIALIZER, variableName)); - } - - // When we declare the function prototype implicitly, we - // want to make sure that the function and its prototype - // are declared at the same node. We also want to make sure - // that the if a symbol has both a TypedVar and a JSType, they have - // the same node. - // - // This consistency is helpful to users of SymbolTable, - // because everything gets declared at the same place. - // This is skipped for aliases of constructors; the .prototype property should point to the - // original definition. - prototypeSlot.setNode(n); - } } /** Check if the given node is a property of a name in the global scope. */ @@ -2156,7 +2164,16 @@ && shouldUseFunctionLiteralType( return createEnumTypeFromNodes(rValue, lValue.getQualifiedName(), lValue, info); } } else if (info.isConstructorOrInterface()) { - return createFunctionTypeFromNodes(rValue, lValue.getQualifiedName(), info, lValue); + FunctionType fnType = + createFunctionTypeFromNodes(rValue, lValue.getQualifiedName(), info, lValue); + if (rValue == null && !lValue.isFromExterns()) { + report( + JSError.make( + lValue, + fnType.isConstructor() ? CTOR_INITIALIZER : IFACE_INITIALIZER, + lValue.getQualifiedName())); + } + return fnType; } } diff --git a/src/com/google/javascript/rhino/testing/TypeSubject.java b/src/com/google/javascript/rhino/testing/TypeSubject.java index 8bd6afa2abe..963b34b8e28 100644 --- a/src/com/google/javascript/rhino/testing/TypeSubject.java +++ b/src/com/google/javascript/rhino/testing/TypeSubject.java @@ -164,6 +164,14 @@ public void hasDeclaredProperty(String propName) { .isTrue(); } + public void hasInferredProperty(String propName) { + check("isObjectType()").that(actualNonNull().isObjectType()).isTrue(); + + check("toMaybeObjectType().isPropertyTypeInferred(%s)", propName) + .that(actualNonNull().toMaybeObjectType().isPropertyTypeInferred(propName)) + .isTrue(); + } + public void isObjectTypeWithoutProperty(String propName) { isLiteralObject(); withTypeOfProp(propName).isNull(); diff --git a/test/com/google/javascript/jscomp/SymbolTableTest.java b/test/com/google/javascript/jscomp/SymbolTableTest.java index d60e29c4648..8b45ad2cff5 100644 --- a/test/com/google/javascript/jscomp/SymbolTableTest.java +++ b/test/com/google/javascript/jscomp/SymbolTableTest.java @@ -630,6 +630,18 @@ public void testPrototypeReferences5() { .isEqualTo(refs.get(0).getNode()); } + @Test + public void testPrototypeReferences_es6Class() { + SymbolTable table = createSymbolTable(lines("class DomHelper { method() {} }")); + Symbol prototype = getGlobalVar(table, "DomHelper.prototype"); + assertThat(prototype).isNotNull(); + List refs = table.getReferenceList(prototype); + + // The class declaration creates an implicit .prototype reference. + assertWithMessage(refs.toString()).that(refs.size()).isEqualTo(1); + assertNode(refs.get(0).getNode().getParent()).hasToken(Token.CLASS); + } + @Test public void testReferencesInJSDocType() { SymbolTable table = diff --git a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java index ea2c397c2ba..30c614b7734 100644 --- a/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java +++ b/test/com/google/javascript/jscomp/TypedScopeCreatorTest.java @@ -39,7 +39,9 @@ import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.deps.JsFileLineParser; +import com.google.javascript.jscomp.deps.ModuleLoader; import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode; +import com.google.javascript.jscomp.modules.Export; import com.google.javascript.jscomp.modules.ModuleMapCreator; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.InputId; @@ -4888,8 +4890,10 @@ public void testRequire_requireLoadModuleInRegularModule_inferredDefaultExport() "});"), "goog.module('b'); const x = goog.require('a'); X: x;")); + // This is unknown because we visit the CFG for the module before visiting the CFG for the + // function block. Users can work around this by explicitly typing their exports. Node xNode = getLabeledStatement("X").statementNode.getOnlyChild(); - assertNode(xNode).hasJSTypeThat().isNumber(); + assertNode(xNode).hasJSTypeThat().isUnknown(); } @Test @@ -4905,8 +4909,10 @@ public void testRequire_requireLoadModuleInRegularModule_inferredNamedExport() { "});"), "goog.module('b'); const {x} = goog.require('a'); X: x;")); + // This is unknown because we visit the CFG for the module before visiting the CFG for the + // function block. Users can work around this by explicitly typing their exports. Node xNode = getLabeledStatement("X").statementNode.getOnlyChild(); - assertNode(xNode).hasJSTypeThat().isNumber(); + assertNode(xNode).hasJSTypeThat().isUnknown(); } @Test @@ -5260,6 +5266,44 @@ public void testGoogModuleRequiringGoogProvide_classWithDestructuring() { .toStringIsEqualTo("a.b.Foo"); } + @Test + public void testEsModule_importStarMissingModule() { + // Make sure this does not crash the typechecker. + testError("import * as x from './invalid_path;'; X: x; export {x};", ModuleLoader.LOAD_WARNING); + + assertScope(getLabeledStatement("X").enclosingScope).declares("x").withTypeThat().isUnknown(); + assertScope(getLabeledStatement("X").enclosingScope) + .declares(Export.NAMESPACE) + .withTypeThat() + .hasInferredProperty("x"); + } + + @Test + public void testEsModule_importSpecsMissingModule() { + // Make sure this does not crash the typechecker. + testError("import {x} from './invalid_path;'; X: x; export {x};", ModuleLoader.LOAD_WARNING); + + assertScope(getLabeledStatement("X").enclosingScope).declares("x").withTypeThat().isUnknown(); + assertScope(getLabeledStatement("X").enclosingScope) + .declares(Export.NAMESPACE) + .withTypeThat() + .hasInferredProperty("x"); + } + + @Test + public void testEsModule_importNameFromScript() { + // Make sure this does not crash the typechecker. Importing a script for the side effects is + // fine, but you can't import a name. + testSame( + srcs( + lines("const x = 'oops, you did not export me.';"), + lines( + "import {x} from './input0';", // + "X: x;"))); + + assertScope(getLabeledStatement("X").enclosingScope).declares("x").withTypeThat().isUnknown(); + } + @Test public void testEsModule_exportNameDeclaration() { testSame("/** @type {string|number} */ export let strOrNum = 0; MOD: 0;"); @@ -5286,7 +5330,22 @@ public void testEsModule_importSpecs_declaredExport() { } @Test - public void testEsModule_importSpecs_inferredExport() { + public void testEsModule_importSpecs_inferredExport_nameDeclaration() { + testSame( + srcs( + lines( + "/** @return {number} */ const f = () => 0;", // + "export const y = f();"), + lines( + "import {y as x} from './input0';", // + "X: x;"))); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + assertThat(getLabeledStatement("X").enclosingScope.getVar("x")).isInferred(); + } + + @Test + public void testEsModule_importSpecs_inferredExport_exportSpecs() { testSame( srcs( lines( @@ -5380,6 +5439,100 @@ public void testEsModule_exportStarFrom() { assertNode(getLabeledStatement("Y").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); } + @Test + public void testEsModule_importStar_declaredExport() { + testSame( + srcs( + "export let /** number */ x;", + lines( + "import * as ns from './input0';", // + "NS: ns;", + "X: ns.x;"))); + + JSType nsType = getLabeledStatement("NS").statementNode.getOnlyChild().getJSType(); + assertType(nsType).hasDeclaredProperty("x"); + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testEsModule_importStar_inferredExport() { + testSame( + srcs( + lines( + "/** @return {number} */ const f = () => 0;", // + "export const x = f();"), + lines( + "import * as ns from './input0';", // + "NS: ns;", + "X: ns.x;"))); + + JSType nsType = getLabeledStatement("NS").statementNode.getOnlyChild().getJSType(); + assertType(nsType).hasInferredProperty("x"); + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testEsModule_importStar_typedefInExportSpec() { + testSame( + srcs( + "/** @typedef {number} */ let numType; export {numType};", + "import * as ns from './input0'; var /** !ns.numType */ x; X: x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testEsModule_importStar_typedefExportDeclaration() { + testSame( + srcs( + "/** @typedef {number} */ export let numType;", + "import * as ns from './input0'; var /** !ns.numType */ x; X: x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testEsModule_importStar_typedefReexported() { + testSame( + srcs( + "/** @typedef {number} */ export let numType;", + "import * as mod from './input0'; export {mod};", + "import {mod} from './input1'; var /** !mod.numType */ x; X: x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testEsModule_importStar_typedefReexportedThenImportStarred() { + testSame( + srcs( + "/** @typedef {number} */ export let numType;", + "import * as mod0 from './input0'; export {mod0};", + "import * as mod1 from './input1'; var /** !mod1.mod0.numType */ x; X: x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testGoogRequire_esModuleId_namespaceRequire() { + testSame( + srcs( + "goog.declareModuleId('a'); export const x = 0;", + "goog.module('b'); const a = goog.require('a'); X: a.x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + + @Test + public void testGoogRequire_esModuleId_destructuringRequire() { + testSame( + srcs( + "goog.declareModuleId('a'); export const x = 0;", + "goog.module('b'); const {x} = goog.require('a'); X: x;")); + + assertNode(getLabeledStatement("X").statementNode.getOnlyChild()).hasJSTypeThat().isNumber(); + } + @Test public void testGoogRequire_insideEsModule_namedExport() { testSame( @@ -5408,6 +5561,107 @@ public void testGoogRequire_insideEsModule_class() { .toStringIsEqualTo("exports.X"); } + @Test + public void testEsModuleImportCycle_namedExports() { + // Note: we cannot reference circular imports directly in the module body but we can reference + // them by type and inside functions. + testSame( + srcs( + lines( + "import {Bar} from './input1';", // + "var /** !Bar */ b;", + "BAR: b;", + "export class Foo {}"), + lines( + "import {Foo} from './input0';", // + "var /** !Foo */ f;", + "FOO: f;", + "export class Bar {}"))); + + assertNode(getLabeledStatement("FOO").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Foo"); + assertNode(getLabeledStatement("BAR").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + } + + @Test + public void testEsModuleImportCycle_importStarPlusNamedExport() { + testSame( + srcs( + lines( + "import * as mod from './input1';", // + "var /** !mod.Bar */ b;", + "BAR: b;", + "export class Foo {}"), + lines( + "import {Foo} from './input0';", // + "var /** !Foo */ f;", + "FOO: f;", + "export class Bar {}"))); + + assertNode(getLabeledStatement("FOO").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Foo"); + assertNode(getLabeledStatement("BAR").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + } + + @Test + public void testEsModuleImportStar_reexportLateReference() { + testSame( + srcs( + lines( + "import * as mod from './input1';", // + "function f() {", + " BAR1: new mod.Bar();", + "}", + "export {mod};"), + lines( + "import {mod} from './input0';", // + "var /** !mod.Bar */ b;", + "BAR: b;", + "function f() {", + " BAR2: new mod.Bar();", + "}", + "export class Bar {}"))); + + assertNode(getLabeledStatement("BAR").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + assertNode(getLabeledStatement("BAR1").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + assertNode(getLabeledStatement("BAR2").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + } + + @Test + public void testEsModuleImportCycle_importStar() { + testSame( + srcs( + lines( + "import * as mod from './input1';", + "var /** !mod.Bar */ b;", + "BAR: b;", + "export class Foo {}"), + lines( + "import * as mod from './input0';", + "var /** !mod.Foo */ f;", + "FOO: f;", + "export class Bar {}"))); + + assertNode(getLabeledStatement("FOO").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Foo"); + assertNode(getLabeledStatement("BAR").statementNode.getOnlyChild()) + .hasJSTypeThat() + .toStringIsEqualTo("Bar"); + } + @Test public void testMemoization() { Node root1 = createEmptyRoot();