com.google.javascript.jscomp.ModuleImportResolver Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2019 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
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.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.QualifiedName;
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 org.jspecify.nullness.Nullable;
/**
* Resolves module requires into {@link TypedVar}s.
*
* Currently this only supports goog.modules, but can be extended for ES modules.
*/
final class ModuleImportResolver {
private final ModuleMap moduleMap;
private final Function nodeToScopeMapper;
private final JSTypeRegistry registry;
private static final String GOOG = "goog";
private static final ImmutableSet GOOG_DEPENDENCY_CALLS =
ImmutableSet.of("require", "requireType", "forwardDeclare", "requireDynamic");
private static final QualifiedName GOOG_MODULE_GET = QualifiedName.of("goog.module.get");
ModuleImportResolver(
ModuleMap moduleMap, Function nodeToScopeMapper, JSTypeRegistry registry) {
this.moduleMap = moduleMap;
this.nodeToScopeMapper = nodeToScopeMapper;
this.registry = registry;
}
/**
* Returns whether this is a CALL node for goog.require, goog.requireType, goog.requireDynamic,
* goog.forwardDeclare, or goog.module.get.
*
* This method does not verify that the call is actually in a valid location. For example, this
* method does not verify that goog.require calls are at the top-level. That is left to the
* caller.
*/
static boolean isGoogModuleDependencyCall(Node value) {
if (value == null
|| !value.isCall()
|| !value.hasTwoChildren()
|| !value.getSecondChild().isStringLit()) {
return false;
}
Node callee = value.getFirstChild();
if (!callee.isGetProp()) {
return false;
}
Node owner = callee.getFirstChild();
return (owner.isName()
&& owner.getString().equals(GOOG)
&& GOOG_DEPENDENCY_CALLS.contains(callee.getString()))
|| GOOG_MODULE_GET.matches(callee);
}
/**
* Attempts to look up the type of a Closure namespace from a require call
*
*
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 goog.modules, goog.provides, and ES module with goog.declareModuleId are supported.
*
* @param googRequire a CALL node representing some kind of Closure require.
*/
@Nullable
ScopedName getClosureNamespaceTypeFromCall(Node googRequire) {
if (moduleMap == null) {
// TODO(b/124919359): make sure all tests have generated a ModuleMap
return null;
}
String moduleId = googRequire.getSecondChild().getString();
Module module = moduleMap.getClosureModule(moduleId);
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;
}
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:
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:
throw new IllegalStateException("Cannot import a name from a SCRIPT");
}
throw new AssertionError();
}
/** Returns the corresponding scope root Node from a goog.module. */
private @Nullable Node getGoogModuleScopeRoot(@Nullable Module module) {
checkArgument(module.metadata().isGoogModule(), module.metadata());
Node scriptNode = module.metadata().rootNode();
if (scriptNode.isScript()
&& scriptNode.hasOneChild()
&& scriptNode.getOnlyChild().isModuleBody()) {
// The module root node should be a SCRIPT, whose first child is a MODULE_BODY.
// The map is keyed off a MODULE_BODY node for a goog.module,
// which is the only child of our SCRIPT node.
return scriptNode.getOnlyChild();
} else if (scriptNode.isCall()) {
// This is a goog.loadModule call, and the scope is keyed off the FUNCTION node's BLOCK in:
// goog.loadModule(function(exports)
Node functionLiteral = scriptNode.getSecondChild();
return NodeUtil.getFunctionBody(functionLiteral);
}
// TODO(b/124919359): this case should not happen, but is triggering on goog.require calls in
// rewritten modules with preserveClosurePrimitives enabled.
return null;
}
/**
* 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.
*/
ImmutableMap 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();
if (!binding.isCreatedByEsImport()) {
continue;
}
// 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 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) {
checkState(binding.sourceNode().getString().equals(localName), binding.sourceNode());
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.buildOrThrow();
}
/**
* 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);
updateAstForExport(
bindingSourceNode,
registry.getNativeType(JSTypeNative.UNKNOWN_TYPE),
/* typedefType= */ null);
continue;
}
TypedVar originalName = originalScope.getSlot(export.getName());
JSType exportType = originalName.getType();
if (exportType == null) {
exportType = registry.getNativeType(JSTypeNative.NO_TYPE);
}
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);
}
updateAstForExport(
bindingSourceNode, exportType, originalName.getNameNode().getTypedefTypeProp());
}
}
private void updateAstForExport(
Node bindingSourceNode, JSType exportType, @Nullable JSType typedefType) {
bindingSourceNode.setJSType(exportType);
bindingSourceNode.setTypedefTypeProp(typedefType);
if (bindingSourceNode.getParent().isExportSpec()) {
// given `export {x as y} from './a/b'` update the y NAME node
bindingSourceNode.getNext().setJSType(exportType);
}
}
/** 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. */
static @Nullable Module getModuleFromScopeRoot(
ModuleMap moduleMap, CompilerInputProvider inputProvider, Node moduleBody) {
if (isGoogModuleBody(moduleBody)) {
Node googModuleCall = moduleBody.getFirstChild();
String namespace = googModuleCall.getFirstChild().getSecondChild().getString();
return moduleMap.getClosureModule(namespace);
} else if (moduleBody.isModuleBody()) {
Node scriptNode = moduleBody.getParent();
CompilerInput input = checkNotNull(inputProvider.getInput(scriptNode.getInputId()));
Module module = moduleMap.getModule(input.getPath());
// TODO(b/131418081): Also cover CommonJS modules.
checkState(
module.metadata().isEs6Module(),
"Typechecking of non-goog- and non-es-modules not supported");
return module;
}
return null;
}
private static boolean isGoogModuleBody(Node moduleBody) {
if (moduleBody.isModuleBody()) {
return moduleBody.getParent().getBooleanProp(Node.GOOG_MODULE);
} else if (moduleBody.isBlock()) {
return moduleBody.getParent().isFunction()
&& NodeUtil.isBundledGoogModuleCall(moduleBody.getGrandparent());
}
return false;
}
}