All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.ModuleImportResolver Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy