Skip to content

Commit

Permalink
Support 'import *', circular imports and goog.require(ES module) in t…
Browse files Browse the repository at this point in the history
…ypechecking

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
  • Loading branch information
lauraharker authored and brad4d committed May 7, 2019
1 parent 72e75ab commit 1ccb534
Show file tree
Hide file tree
Showing 7 changed files with 507 additions and 106 deletions.
149 changes: 119 additions & 30 deletions src/com/google/javascript/jscomp/ModuleImportResolver.java
Expand Up @@ -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;
Expand Down Expand Up @@ -75,7 +78,7 @@ static boolean isGoogModuleDependencyCall(Node value) {
*
* <p>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.
*/
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<Node, ScopedName> declareEsModuleImports(
Module module, TypedScope scope, CompilerInput moduleInput) {
checkArgument(module.metadata().isEs6Module(), module);
checkArgument(scope.isModuleScope(), scope);
ImmutableMap.Builder<Node, ScopedName> missingNames = ImmutableMap.builder();
for (Map.Entry<String, Binding> boundName : module.boundNames().entrySet()) {
Binding binding = boundName.getValue();
String localName = boundName.getKey();
Expand All @@ -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
*
* <p>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.
*
* <p>The additional properties will be inferred (instead of declared) if and only if {@link
* TypedVar#isTypeInferred()} is true for the original exported name.
*
* <p>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<String, Binding> 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. */
Expand Down
37 changes: 35 additions & 2 deletions src/com/google/javascript/jscomp/TypeInference.java
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
18 changes: 3 additions & 15 deletions src/com/google/javascript/jscomp/TypeInferencePass.java
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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());
}

/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
Expand Down

1 comment on commit 1ccb534

@supersteves
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Please sign in to comment.