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

com.google.javascript.jscomp.ModuleMetadata 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 2018 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ClosureCheckModule.DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Gathers metadata around modules that is useful for checking imports / requires.
 *
 * 

TODO(johnplaisted): There's an opportunity for reuse here in ClosureRewriteModules, which * would involve putting this in some common location. Currently this is only used as a helper class * for Es6RewriteModules. CompilerInput already has some (not all) of this information but it is not * always populated. Additionally we'd ideally unwrap the goog.loadModule calls so each becomes its * own CompilerInput, otherwise goog.require(path) from loadModules won't work correctly. But not * having a 1:1 mapping of actual inputs to compiler inputs may cause issues. It may also be ideal * to include CommonJS here too as ES6 modules can import them. That would allow decoupling of how * these modules are written; right now Es6RewriteModule only checks this for goog.requires and * goog: imports, not for ES6 path imports. */ final class ModuleMetadata { /** Various types of Javascript "modules" that can be found in the JS Compiler. */ public enum ModuleType { ES6_MODULE("an ES6 module"), GOOG_PROVIDE("a goog.provide'd file"), /** A goog.module that does not declare a legacy namespace. */ GOOG_MODULE("a goog.module"), /** A goog.module that declares a legacy namespace with goog.module.declareLegacyNamespace. */ LEGACY_GOOG_MODULE("a goog.module"), COMMON_JS("a CommonJS module"), SCRIPT("a script"); private final String description; ModuleType(String description) { this.description = description; } } static final DiagnosticType MIXED_MODULE_TYPE = DiagnosticType.error("JSC_MIXED_MODULE_TYPE", "A file cannot be both {0} and {1}."); static final DiagnosticType INVALID_DECLARE_NAMESPACE_CALL = DiagnosticType.error( "JSC_INVALID_DECLARE_NAMESPACE_CALL", "goog.module.declareNamespace parameter must be a string literal."); static final DiagnosticType DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE = DiagnosticType.error( "JSC_DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE", "goog.module.declareNamespace can only be called within ES6 modules."); static final DiagnosticType MULTIPLE_DECLARE_MODULE_NAMESPACE = DiagnosticType.error( "JSC_MULTIPLE_DECLARE_MODULE_NAMESPACE", "goog.module.declareNamespace can only be called once per ES6 module."); private static final Node GOOG_PROVIDE = IR.getprop(IR.name("goog"), IR.string("provide")); private static final Node GOOG_MODULE = IR.getprop(IR.name("goog"), IR.string("module")); private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE = IR.getprop(GOOG_MODULE.cloneTree(), IR.string("declareLegacyNamespace")); private static final Node GOOG_MODULE_DECLARNAMESPACE = IR.getprop(GOOG_MODULE.cloneTree(), IR.string("declareNamespace")); /** * Map from module path to module. These modules represent files and thus will contain all goog * namespaces that are in the file. These are not the same modules in modulesByGoogNamespace. */ private final Map modulesByPath = new HashMap<>(); /** * Map from Closure namespace to module. These modules represent just the single namespace and * thus each module has only one goog namespace in its {@link Module#getGoogNamespaces()}. These * are not the same modules in modulesByPath. */ private final Map modulesByGoogNamespace = new HashMap<>(); /** Modules by AST node. */ private final Map modulesByNode = new HashMap<>(); /** The current module being traversed. */ private ModuleBuilder currentModule; /** * The module currentModule is nested under, if any. Modules are expected to be at most two deep * (a script and then a goog.loadModule call). */ private ModuleBuilder parentModule; /** The call to goog.loadModule we are traversing. */ private Node loadModuleCall; private final AbstractCompiler compiler; private final boolean processCommonJsModules; private final ResolutionMode moduleResolutionMode; private Finder finder; ModuleMetadata( AbstractCompiler compiler, boolean processCommonJsModules, ResolutionMode moduleResolutionMode) { this.compiler = compiler; this.processCommonJsModules = processCommonJsModules; this.moduleResolutionMode = moduleResolutionMode; this.finder = new Finder(); } /** Struct containing basic information about a module including its type and goog namespaces. */ static final class Module { private final ModuleType moduleType; private final ModulePath path; private final ImmutableList nestedModules; /** * Closure namespaces that this file is associated with. Created by goog.provide, goog.module, * and goog.module.declareNamespace. */ private final ImmutableSet googNamespaces; private Module( @Nullable ModulePath path, ModuleType moduleType, Set googNamespaces, List nestedModules) { this.path = path; this.moduleType = moduleType; this.googNamespaces = ImmutableSet.copyOf(googNamespaces); this.nestedModules = ImmutableList.copyOf(nestedModules); } public ModuleType getModuleType() { return moduleType; } public boolean isEs6Module() { return moduleType == ModuleType.ES6_MODULE; } public boolean isGoogModule() { return isNonLegacyGoogModule() || isLegacyGoogModule(); } public boolean isNonLegacyGoogModule() { return moduleType == ModuleType.GOOG_MODULE; } public boolean isLegacyGoogModule() { return moduleType == ModuleType.LEGACY_GOOG_MODULE; } public boolean isGoogProvide() { return moduleType == ModuleType.GOOG_PROVIDE; } public boolean isCommonJs() { return moduleType == ModuleType.COMMON_JS; } public boolean isScript() { return moduleType == ModuleType.SCRIPT; } public ImmutableSet getGoogNamespaces() { return googNamespaces; } @Nullable public ModulePath getPath() { return path; } /** @return the global, qualified name to rewrite any references to this module to */ public String getGlobalName() { return getGlobalName(null); } /** @return the global, qualified name to rewrite any references to this module to */ public String getGlobalName(@Nullable String googNamespace) { checkState(googNamespace == null || googNamespaces.contains(googNamespace)); switch (moduleType) { case GOOG_MODULE: return ClosureRewriteModule.getBinaryModuleNamespace(googNamespace); case GOOG_PROVIDE: case LEGACY_GOOG_MODULE: return googNamespace; case ES6_MODULE: case COMMON_JS: return path.toModuleName(); case SCRIPT: // fall through, throw an error } throw new IllegalStateException("Unexpected module type: " + moduleType); } } private final class ModuleBuilder { final Node rootNode; @Nullable final ModulePath path; final Set googNamespaces; final List nestedModules; ModuleType moduleType; Node declaresNamespace; Node declaresLegacyNamespace; boolean ambiguous; ModuleBuilder(Node rootNode, @Nullable ModulePath path) { this.rootNode = rootNode; this.path = path; googNamespaces = new HashSet<>(); nestedModules = new ArrayList<>(); moduleType = ModuleType.SCRIPT; ambiguous = false; } Module build() { if (!ambiguous) { if (declaresNamespace != null && moduleType != ModuleType.ES6_MODULE) { compiler.report( JSError.make(declaresNamespace, DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE)); } if (declaresLegacyNamespace != null) { if (moduleType == ModuleType.GOOG_MODULE) { moduleType = ModuleType.LEGACY_GOOG_MODULE; } else { compiler.report( JSError.make( declaresLegacyNamespace, DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE)); } } } return new Module(path, moduleType, googNamespaces, nestedModules); } void setModuleType(ModuleType type, NodeTraversal t, Node n) { checkNotNull(type); if (moduleType == type) { return; } if (moduleType == ModuleType.SCRIPT) { moduleType = type; return; } ambiguous = true; t.report(n, MIXED_MODULE_TYPE, moduleType.description, type.description); } void addGoogNamespace(String namespace) { googNamespaces.add(namespace); } void recordDeclareNamespace(Node declaresNamespace) { this.declaresNamespace = declaresNamespace; } void recordDeclareLegacyNamespace(Node declaresLegacyNamespace) { this.declaresLegacyNamespace = declaresLegacyNamespace; } public boolean isScript() { return moduleType == ModuleType.SCRIPT; } } /** Traverses the AST and build a sets of {@link Module}s. */ private final class Finder implements Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case SCRIPT: enterModule(n, t.getInput().getPath()); break; case IMPORT: case EXPORT: checkNotNull(currentModule); currentModule.setModuleType(ModuleType.ES6_MODULE, t, n); break; case CALL: if (n.isCall() && n.getFirstChild().matchesQualifiedName("goog.loadModule")) { loadModuleCall = n; if (n.getChildCount() > 2 && n.getChildAtIndex(2).isString()) { enterModule(n, compiler.getModuleLoader().resolve(n.getChildAtIndex(2).getString())); } else { enterModule(n, null); } } break; default: break; } return true; } private void enterModule(Node n, @Nullable ModulePath path) { ModuleBuilder newModule = new ModuleBuilder(n, path); if (currentModule != null) { checkState(parentModule == null, "Expected modules to be nested at most 2 deep."); parentModule = currentModule; } currentModule = newModule; } private void leaveModule() { checkNotNull(currentModule); Module module = currentModule.build(); modulesByNode.put(currentModule.rootNode, module); if (module.path != null) { modulesByPath.put(module.path.toString(), module); } for (String namespace : module.getGoogNamespaces()) { modulesByGoogNamespace.put(namespace, module); } if (parentModule != null) { parentModule.nestedModules.add(module); } currentModule = parentModule; parentModule = null; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (processCommonJsModules && currentModule != null && currentModule.isScript()) { if (ProcessCommonJSModules.isCommonJsExport(t, n, moduleResolutionMode) || ProcessCommonJSModules.isCommonJsImport(n, moduleResolutionMode)) { currentModule.setModuleType(ModuleType.COMMON_JS, t, n); return; } } switch (n.getToken()) { case SCRIPT: leaveModule(); break; case CALL: if (loadModuleCall == n) { leaveModule(); loadModuleCall = null; } else { visitGoogCall(t, n); } break; default: break; } } private void visitGoogCall(NodeTraversal t, Node n) { if (!n.hasChildren() || !n.getFirstChild().isGetProp() || !n.getFirstChild().isQualifiedName()) { return; } Node getprop = n.getFirstChild(); if (getprop.matchesQualifiedName(GOOG_PROVIDE)) { currentModule.setModuleType(ModuleType.GOOG_PROVIDE, t, n); if (n.hasTwoChildren() && n.getLastChild().isString()) { String namespace = n.getLastChild().getString(); currentModule.addGoogNamespace(namespace); checkDuplicates(namespace, t, n); } else { t.report(n, ClosureRewriteModule.INVALID_PROVIDE_NAMESPACE); } } else if (getprop.matchesQualifiedName(GOOG_MODULE)) { currentModule.setModuleType(ModuleType.GOOG_MODULE, t, n); if (n.hasTwoChildren() && n.getLastChild().isString()) { String namespace = n.getLastChild().getString(); currentModule.addGoogNamespace(namespace); checkDuplicates(namespace, t, n); } else { t.report(n, ClosureRewriteModule.INVALID_MODULE_NAMESPACE); } } else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) { currentModule.recordDeclareLegacyNamespace(n); } else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARNAMESPACE)) { if (currentModule.declaresNamespace != null) { t.report(n, MULTIPLE_DECLARE_MODULE_NAMESPACE); } if (n.hasTwoChildren() && n.getLastChild().isString()) { currentModule.recordDeclareNamespace(n); String namespace = n.getLastChild().getString(); currentModule.addGoogNamespace(namespace); checkDuplicates(namespace, t, n); } else { t.report(n, INVALID_DECLARE_NAMESPACE_CALL); } } } /** Checks if the given Closure namespace is a duplicate or not. */ private void checkDuplicates(String namespace, NodeTraversal t, Node n) { Module existing = modulesByGoogNamespace.get(namespace); if (existing != null) { switch (existing.moduleType) { case ES6_MODULE: case GOOG_MODULE: t.report(n, ClosureRewriteModule.DUPLICATE_MODULE); break; case GOOG_PROVIDE: t.report(n, ClosureRewriteModule.DUPLICATE_NAMESPACE); break; default: throw new IllegalStateException("Unexpected module type: " + existing.moduleType); } } } } public void process(Node externs, Node root) { finder = new Finder(); NodeTraversal.traverse(compiler, externs, finder); NodeTraversal.traverse(compiler, root, finder); } private void remove(Module module) { if (module != null) { for (String symbol : module.getGoogNamespaces()) { modulesByGoogNamespace.remove(symbol); } if (module.path != null) { modulesByPath.remove(module.path.toString()); } for (Module nested : module.nestedModules) { remove(nested); } } } public void hotSwapScript(Node scriptRoot) { Module existing = modulesByPath.get(compiler.getInput(scriptRoot.getInputId()).getPath().toString()); remove(existing); NodeTraversal.traverse(compiler, scriptRoot, finder); } /** * @return map from module path to module. These modules represent files and thus {@link * Module#getGoogNamespaces()} contains all Closure namespaces in the file. These are not the * same modules from {@link ModuleMetadata#getModulesByGoogNamespace()}. It is not valid to * call {@link Module#getGlobalName()} on {@link ModuleType#GOOG_PROVIDE} modules from this * map that have more than one Closure namespace as it is ambiguous. */ Map getModulesByPath() { return Collections.unmodifiableMap(modulesByPath); } /** * @return map from Closure namespace to module. These modules represent the Closure namespace and * thus {@link Module#getGoogNamespaces()} will have size 1. As a result, it is valid to call * {@link Module#getGlobalName()} on these modules. These are not the same modules from {@link * ModuleMetadata#getModulesByPath()}. */ Map getModulesByGoogNamespace() { return Collections.unmodifiableMap(modulesByGoogNamespace); } /** @return the {@link Module} that contains the given AST node */ @Nullable Module getContainingModule(Node n) { if (finder == null) { return null; } Module m = null; while (m == null && n != null) { m = modulesByNode.get(n); n = n.getParent(); } return m; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy