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

com.google.javascript.jscomp.ClosureCheckModule 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. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2015 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.Ascii.isUpperCase;
import static com.google.common.base.Ascii.toLowerCase;
import static com.google.common.base.Ascii.toUpperCase;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_DESTRUCTURING_FORWARD_DECLARE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.MODULE_USES_GOOG_MODULE_GET;

import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.AbstractModuleCallback;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Checks that goog.module() is used correctly.
 *
 * 

Note that this file only does checks that can be done per-file. Whole program checks happen * during goog.module rewriting, in {@link ClosureRewriteModule}. */ public final class ClosureCheckModule extends AbstractModuleCallback implements HotSwapCompilerPass { static final DiagnosticType AT_EXPORT_IN_GOOG_MODULE = DiagnosticType.error( "JSC_AT_EXPORT_IN_GOOG_MODULE", "@export has no effect on top-level names in a goog.module." + " Consider using goog.exportSymbol instead."); static final DiagnosticType AT_EXPORT_IN_NON_LEGACY_GOOG_MODULE = DiagnosticType.error( "JSC_AT_EXPORT_IN_NON_LEGACY_GOOG_MODULE", "@export is not allowed here in a non-legacy goog.module." + " Consider using goog.exportSymbol instead."); static final DiagnosticType GOOG_MODULE_IN_NON_MODULE = DiagnosticType.error( "JSC_GOOG_MODULE_IN_NON_MODULE", "goog.module() call must be the first statement in a module."); static final DiagnosticType GOOG_MODULE_MISPLACED = DiagnosticType.error( "JSC_GOOG_MODULE_MISPLACED", "goog.module() call must be the first statement in a file."); static final DiagnosticType DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE = DiagnosticType.error( "JSC_DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE", "goog.module.declareLegacyNamespace may only be called in a goog.module."); static final DiagnosticType GOOG_MODULE_REFERENCES_THIS = DiagnosticType.error( "JSC_GOOG_MODULE_REFERENCES_THIS", "The body of a goog.module cannot reference 'this'."); static final DiagnosticType GOOG_MODULE_USES_THROW = DiagnosticType.error( "JSC_GOOG_MODULE_USES_THROW", "The body of a goog.module cannot use 'throw'."); static final DiagnosticType DUPLICATE_NAME_SHORT_REQUIRE = DiagnosticType.error( "JSC_DUPLICATE_NAME_SHORT_REQUIRE", "Found multiple goog.require statements importing identifier ''{0}''."); static final DiagnosticType INVALID_DESTRUCTURING_REQUIRE = DiagnosticType.error( "JSC_INVALID_DESTRUCTURING_REQUIRE", "Destructuring goog.require must be a simple object pattern."); static final DiagnosticType LET_GOOG_REQUIRE = DiagnosticType.disabled( "JSC_LET_GOOG_REQUIRE", "Module imports must be constant. Please use ''const'' instead of ''let''."); static final DiagnosticType MULTIPLE_MODULES_IN_FILE = DiagnosticType.error( "JSC_MULTIPLE_MODULES_IN_FILE", "There should only be a single goog.module() statement per file."); static final DiagnosticType ONE_REQUIRE_PER_DECLARATION = DiagnosticType.error( "JSC_ONE_REQUIRE_PER_DECLARATION", "There may only be one goog.require() per var/let/const declaration."); static final DiagnosticType INCORRECT_SHORTNAME_CAPITALIZATION = DiagnosticType.disabled( "JSC_INCORRECT_SHORTNAME_CAPITALIZATION", "The capitalization of short name {0} is incorrect; it should be {1}."); static final DiagnosticType EXPORT_NOT_AT_MODULE_SCOPE = DiagnosticType.error( "JSC_EXPORT_NOT_AT_MODULE_SCOPE", "Exports must be at the top-level of a module"); static final DiagnosticType EXPORT_NOT_A_STATEMENT = DiagnosticType.error("JSC_EXPORT_NOT_A_STATEMENT", "Exports should be a statement."); static final DiagnosticType EXPORT_REPEATED_ERROR = DiagnosticType.error( "JSC_EXPORT_REPEATED_ERROR", "Name cannot be exported multiple times. Previous export on line {0}."); static final DiagnosticType REFERENCE_TO_MODULE_GLOBAL_NAME = DiagnosticType.error( "JSC_REFERENCE_TO_MODULE_GLOBAL_NAME", "References to the global name of a module are not allowed. Perhaps you meant exports?"); static final DiagnosticType REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME = DiagnosticType.disabled( "JSC_REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME", "Reference to fully qualified import name ''{0}''." + " Imports in goog.module should use the return value of" + " goog.require / goog.forwardDeclare instead."); public static final DiagnosticType REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME = DiagnosticType.disabled( "JSC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME", "Reference to fully qualified import name ''{0}''." + " Please use the short name ''{1}'' instead."); static final DiagnosticType JSDOC_REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME = DiagnosticType.disabled( "JSC_JSDOC_REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME", "Reference to fully qualified import name ''{0}'' in JSDoc." + " Imports in goog.module should use the return value of" + " goog.require / goog.forwardDeclare instead."); public static final DiagnosticType JSDOC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME = DiagnosticType.disabled( "JSC_JSDOC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME", "Reference to fully qualified import name ''{0}'' in JSDoc." + " Please use the short name ''{1}'' instead."); static final DiagnosticType REQUIRE_NOT_AT_TOP_LEVEL = DiagnosticType.error( "JSC_REQUIRE_NOT_AT_TOP_LEVEL", "goog.require() must be called at file scope."); static final DiagnosticType LEGACY_NAMESPACE_NOT_AT_TOP_LEVEL = DiagnosticType.error( "JSC_LEGACY_NAMESPACE_NOT_AT_TOP_LEVEL", "goog.module.declareLegacyNamespace() does not return a value"); static final DiagnosticType LEGACY_NAMESPACE_NOT_AFTER_GOOG_MODULE = DiagnosticType.error( "JSC_LEGACY_NAMESPACE_NOT_AT_TOP_LEVEL", "goog.module.declareLegacyNamespace() must be immediately after the" + " goog.module('...'); call"); private static class ModuleInfo { // Name of the module in question (i.e. the argument to goog.module) private final String name; // Mapping from fully qualified goog.required names to the import LHS node. // For standalone goog.require()s the value is the EXPR_RESULT node. private final Map importsByLongRequiredName = new HashMap<>(); // Module-local short names for goog.required symbols. private final Set shortImportNames = new HashSet<>(); // A map from the export names "exports.name" to the nodes of those named exports. // The default export is keyed with just "exports" private final Map exportNodesByName = new HashMap<>(); ModuleInfo(String moduleName) { this.name = moduleName; } } private ModuleInfo currentModuleInfo = null; public ClosureCheckModule(AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap) { super(compiler, moduleMetadataMap); } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverse(compiler, scriptRoot, this); } @Override public void enterModule(ModuleMetadata currentModule, Node moduleScopeRoot) { if (!currentModule.isGoogModule()) { return; } checkState(currentModuleInfo == null); checkState(!currentModule.googNamespaces().isEmpty()); currentModuleInfo = new ModuleInfo(Iterables.getFirst(currentModule.googNamespaces(), "")); } @Override public void exitModule(ModuleMetadata currentModule, Node moduleScopeRoot) { currentModuleInfo = null; } @Override protected void visit( NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, @Nullable Node moduleScopeRoot) { Node parent = n.getParent(); if (currentModuleInfo == null) { if (NodeUtil.isCallTo(n, "goog.module")) { t.report(n, GOOG_MODULE_IN_NON_MODULE); } else if (NodeUtil.isGoogModuleDeclareLegacyNamespaceCall(n)) { t.report(n, DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE); } return; } JSDocInfo jsDoc = n.getJSDocInfo(); if (jsDoc != null) { checkJSDoc(t, jsDoc); } switch (n.getToken()) { case CALL: Node callee = n.getFirstChild(); if (callee.matchesQualifiedName("goog.module")) { if (!currentModuleInfo.name.equals(extractFirstArgumentName(n))) { t.report(n, MULTIPLE_MODULES_IN_FILE); } else if (!isFirstExpressionInGoogModule(parent)) { t.report(n, GOOG_MODULE_MISPLACED); } } else if (callee.matchesQualifiedName("goog.require") || callee.matchesQualifiedName("goog.requireType") || callee.matchesQualifiedName("goog.forwardDeclare")) { checkRequireCall(t, n, parent); } else if (callee.matchesQualifiedName("goog.module.get") && t.inModuleHoistScope()) { t.report(n, MODULE_USES_GOOG_MODULE_GET); } else if (callee.matchesQualifiedName("goog.module.declareLegacyNamespace")) { checkLegacyNamespaceCall(t, n, parent); } break; case ASSIGN: { if (isExportLhs(n.getFirstChild())) { checkModuleExport(t, n, parent); } break; } case CLASS: case FUNCTION: if (!NodeUtil.isStatement(n)) { break; } // fallthrough case VAR: case LET: case CONST: if (t.inModuleHoistScope() && (n.isClass() || NodeUtil.getEnclosingClass(n) == null) && NodeUtil.getEnclosingType(n, Token.OBJECTLIT) == null) { JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(n); if (jsdoc != null && jsdoc.isExport()) { t.report(n, AT_EXPORT_IN_GOOG_MODULE); } } break; case THIS: if (t.inModuleHoistScope()) { t.report(n, GOOG_MODULE_REFERENCES_THIS); } break; case THROW: if (t.inModuleHoistScope()) { t.report(n, GOOG_MODULE_USES_THROW); } break; case GETPROP: if (n.matchesQualifiedName(currentModuleInfo.name)) { t.report(n, REFERENCE_TO_MODULE_GLOBAL_NAME); } else if (currentModuleInfo.importsByLongRequiredName.containsKey(n.getQualifiedName())) { Node importLhs = currentModuleInfo.importsByLongRequiredName.get(n.getQualifiedName()); if (importLhs == null) { t.report(n, REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, n.getQualifiedName()); } else if (importLhs.isName()) { t.report( n, REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, n.getQualifiedName(), importLhs.getString()); } else if (importLhs.isDestructuringLhs()) { if (parent.isGetProp()) { String shortName = parent .getQualifiedName() .substring(parent.getQualifiedName().lastIndexOf('.') + 1); Node objPattern = importLhs.getFirstChild(); checkState(objPattern.isObjectPattern(), objPattern); for (Node strKey : objPattern.children()) { // const {foo} = goog.require('ns.bar'); // Should use the short name "foo" instead of "ns.bar.foo". if (!strKey.hasChildren() && strKey.getString().equals(shortName)) { t.report( parent, REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, parent.getQualifiedName(), shortName); return; } // const {foo: barFoo} = goog.require('ns.bar'); // Should use the short name "barFoo" instead of "ns.bar.foo". if (strKey.hasOneChild() && strKey.getString().equals(shortName)) { t.report( parent, REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, parent.getQualifiedName(), strKey.getFirstChild().getString()); return; } } } t.report(n, REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, n.getQualifiedName()); } else { checkState(importLhs.isExprResult(), importLhs); t.report(n, REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, n.getQualifiedName()); } } break; default: break; } } private void checkJSDoc(NodeTraversal t, JSDocInfo jsDoc) { for (Node typeNode : jsDoc.getTypeNodes()) { checkTypeExpression(t, typeNode); } } private void checkTypeExpression(final NodeTraversal t, Node typeNode) { NodeUtil.visitPreOrder( typeNode, new NodeUtil.Visitor() { @Override public void visit(Node node) { if (!node.isString()) { return; } String type = node.getString(); while (true) { if (currentModuleInfo.importsByLongRequiredName.containsKey(type)) { Node importLhs = currentModuleInfo.importsByLongRequiredName.get(type); if (importLhs == null || !importLhs.isName()) { t.report(node, JSDOC_REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, type); } else if (!importLhs.getString().equals(type)) { t.report( node, JSDOC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, type, importLhs.getString()); } } if (type.contains(".")) { type = type.substring(0, type.lastIndexOf('.')); } else { return; } } } }); } /** Is this the LHS of a goog.module export? i.e. Either "exports" or "exports.name" */ private static boolean isExportLhs(Node lhs) { if (!lhs.isQualifiedName()) { return false; } return lhs.matchesName("exports") || (lhs.isGetProp() && lhs.getFirstChild().matchesName("exports")); } private void checkModuleExport(NodeTraversal t, Node n, Node parent) { checkArgument(n.isAssign()); Node lhs = n.getFirstChild(); checkState(isExportLhs(lhs)); // Check multiple exports of the same name Node previousDefinition = currentModuleInfo.exportNodesByName.get(lhs.getQualifiedName()); if (previousDefinition != null) { int previousLine = previousDefinition.getLineno(); t.report(n, EXPORT_REPEATED_ERROR, String.valueOf(previousLine)); } // Check exports in invalid program position Node defaultExportNode = currentModuleInfo.exportNodesByName.get("exports"); // If we have never seen an `exports =` default export assignment, or this is the // default export, then treat this assignment as an export and do the checks it is well formed. if (defaultExportNode == null || lhs.matchesName("exports")) { currentModuleInfo.exportNodesByName.put(lhs.getQualifiedName(), lhs); if (!t.inModuleScope()) { t.report(n, EXPORT_NOT_AT_MODULE_SCOPE); } else if (!parent.isExprResult()) { t.report(n, EXPORT_NOT_A_STATEMENT); } } // Check @export on a module local name if (!NodeUtil.isPrototypeProperty(lhs) && !NodeUtil.isLegacyGoogModuleFile(NodeUtil.getEnclosingScript(n))) { JSDocInfo jsDoc = n.getJSDocInfo(); if (jsDoc != null && jsDoc.isExport()) { t.report(n, AT_EXPORT_IN_NON_LEGACY_GOOG_MODULE); } } } private static String extractFirstArgumentName(Node callNode) { Node firstArg = callNode.getSecondChild(); if (firstArg != null && firstArg.isString()) { return firstArg.getString(); } return null; } private void checkRequireCall(NodeTraversal t, Node callNode, Node parent) { checkState(callNode.isCall()); checkState(callNode.getLastChild().isString()); switch (parent.getToken()) { case EXPR_RESULT: String key = extractFirstArgumentName(callNode); if (!currentModuleInfo.importsByLongRequiredName.containsKey(key)) { currentModuleInfo.importsByLongRequiredName.put(key, parent); } return; case NAME: case DESTRUCTURING_LHS: checkShortGoogRequireCall(t, callNode, parent.getParent()); return; default: break; } t.report(callNode, REQUIRE_NOT_AT_TOP_LEVEL); } private void checkShortGoogRequireCall(NodeTraversal t, Node callNode, Node declaration) { if (declaration.isLet() && !callNode.getFirstChild().matchesQualifiedName("goog.forwardDeclare")) { t.report(declaration, LET_GOOG_REQUIRE); } if (!declaration.hasOneChild()) { t.report(declaration, ONE_REQUIRE_PER_DECLARATION); return; } Node lhs = declaration.getFirstChild(); if (lhs.isDestructuringLhs()) { if (!isValidDestructuringImport(lhs)) { t.report(declaration, INVALID_DESTRUCTURING_REQUIRE); } if (callNode.getFirstChild().matchesQualifiedName("goog.forwardDeclare")) { t.report(lhs, INVALID_DESTRUCTURING_FORWARD_DECLARE); } } else { checkState(lhs.isName()); checkShortName(t, lhs, callNode.getLastChild().getString()); } currentModuleInfo.importsByLongRequiredName.put(extractFirstArgumentName(callNode), lhs); for (Node nameNode : NodeUtil.findLhsNodesInNode(declaration)) { String name = nameNode.getString(); if (!currentModuleInfo.shortImportNames.add(name)) { t.report(nameNode, DUPLICATE_NAME_SHORT_REQUIRE, name); } } } private static void checkShortName(NodeTraversal t, Node shortNameNode, String namespace) { String shortName = shortNameNode.getString(); String lastSegment = namespace.substring(namespace.lastIndexOf('.') + 1); if (shortName.equals(lastSegment) || lastSegment.isEmpty()) { return; } if (isUpperCase(shortName.charAt(0)) != isUpperCase(lastSegment.charAt(0))) { char newStartChar = isUpperCase(shortName.charAt(0)) ? toLowerCase(shortName.charAt(0)) : toUpperCase(shortName.charAt(0)); String correctedName = newStartChar + shortName.substring(1); t.report(shortNameNode, INCORRECT_SHORTNAME_CAPITALIZATION, shortName, correctedName); } } private static boolean isValidDestructuringImport(Node destructuringLhs) { checkArgument(destructuringLhs.isDestructuringLhs()); Node objectPattern = destructuringLhs.getFirstChild(); if (!objectPattern.isObjectPattern()) { return false; } for (Node stringKey : objectPattern.children()) { if (!stringKey.isStringKey()) { return false; } if (stringKey.hasChildren() && !stringKey.getFirstChild().isName()) { return false; } } return true; } /** Validates the position of a goog.module.declareLegacyNamespace(); call */ private static void checkLegacyNamespaceCall(NodeTraversal t, Node callNode, Node parent) { checkArgument(callNode.isCall()); if (!parent.isExprResult()) { t.report(callNode, LEGACY_NAMESPACE_NOT_AT_TOP_LEVEL); return; } Node prev = parent.getPrevious(); if (prev == null || !NodeUtil.isGoogModuleCall(prev)) { t.report(callNode, LEGACY_NAMESPACE_NOT_AFTER_GOOG_MODULE); } } /** Whether this is the first statement in a module */ private static boolean isFirstExpressionInGoogModule(Node node) { if (!node.isExprResult() || node.getPrevious() != null) { return false; } Node statementBlock = node.getParent(); return statementBlock.isModuleBody() || NodeUtil.isBundledGoogModuleScopeRoot(statementBlock); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy