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

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

There is a newer version: 9.0.8
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 com.google.common.base.Predicates;
import com.google.javascript.jscomp.NodeTraversal.AbstractModuleCallback;
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;

/**
 * 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 here"); 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_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 GOOG_MODULE_USES_GOOG_MODULE_GET = DiagnosticType.error( "JSC_GOOG_MODULE_USES_GOOG_MODULE_GET", "It's illegal to use a 'goog.module.get' at the module top-level." + " Did you mean to use goog.require instead?"); 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 INVALID_DESTRUCTURING_FORWARD_DECLARE = DiagnosticType.error( "JSC_INVALID_DESTRUCTURING_FORWARD_DECLARE", "Cannot destructure a forward-declared type"); 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 MODULE_AND_PROVIDES = DiagnosticType.error( "JSC_MODULE_AND_PROVIDES", "A file using goog.module() may not also use goog.provide() statements."); 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_A_MODULE_LEVEL_STATEMENT = DiagnosticType.error( "JSC_EXPORT_NOT_A_MODULE_LEVEL_STATEMENT", "Exports must be a statement at the top-level of a module"); 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."); private final AbstractCompiler compiler; 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<>(); // The node of the default (non-named) export of this module, if it exists. private Node defaultExportNode = null; ModuleInfo(String moduleName) { this.name = moduleName; } } private ModuleInfo currentModule = null; public ClosureCheckModule(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, scriptRoot, this); } @Override public void enterModule(NodeTraversal t, Node scopeRoot) { Node firstStatement = scopeRoot.getFirstChild(); if (NodeUtil.isExprCall(firstStatement)) { Node call = firstStatement.getFirstChild(); Node callee = call.getFirstChild(); if (callee.matchesQualifiedName("goog.module")) { checkState(currentModule == null); String moduleName = extractFirstArgumentName(call); if (moduleName == null) { t.report(scopeRoot, ClosureRewriteModule.INVALID_MODULE_NAMESPACE); } else { currentModule = new ModuleInfo(moduleName); } } } } @Override public void exitModule(NodeTraversal t, Node scopeRoot) { currentModule = null; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (currentModule == null) { if (NodeUtil.isCallTo(n, "goog.module")) { t.report(n, GOOG_MODULE_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") && !currentModule.name.equals(extractFirstArgumentName(n))) { t.report(n, MULTIPLE_MODULES_IN_FILE); } else if (callee.matchesQualifiedName("goog.provide")) { t.report(n, MODULE_AND_PROVIDES); } else if (callee.matchesQualifiedName("goog.require") || callee.matchesQualifiedName("goog.forwardDeclare")) { checkRequireCall(t, n, parent); } else if (callee.matchesQualifiedName("goog.module.get") && t.inModuleHoistScope()) { t.report(n, GOOG_MODULE_USES_GOOG_MODULE_GET); } 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(currentModule.name)) { t.report(n, REFERENCE_TO_MODULE_GLOBAL_NAME); } else if (currentModule.importsByLongRequiredName.containsKey(n.getQualifiedName())) { Node importLhs = currentModule.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 (currentModule.importsByLongRequiredName.containsKey(type)) { Node importLhs = currentModule.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; } } } }, Predicates.alwaysTrue()); } /** Is this the LHS of a goog.module export? i.e. Either "exports" or "exports.name" */ private boolean isExportLhs(Node lhs) { if (!lhs.isQualifiedName()) { return false; } return lhs.matchesQualifiedName("exports") || (lhs.isGetProp() && lhs.getFirstChild().matchesQualifiedName("exports")); } private void checkModuleExport(NodeTraversal t, Node n, Node parent) { checkArgument(n.isAssign()); Node lhs = n.getFirstChild(); checkState(isExportLhs(lhs)); if (currentModule.defaultExportNode == null && (!t.inModuleScope() || !parent.isExprResult())) { // Invalid export location. t.report(n, EXPORT_NOT_A_MODULE_LEVEL_STATEMENT); } if (lhs.isName()) { if (currentModule.defaultExportNode != null) { // Multiple exports int previousLine = currentModule.defaultExportNode.getLineno(); t.report(n, EXPORT_REPEATED_ERROR, String.valueOf(previousLine)); } currentModule.defaultExportNode = lhs; } if ((lhs.isName() || !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 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()); if (!callNode.getLastChild().isString()) { t.report(callNode, ProcessClosurePrimitives.INVALID_ARGUMENT_ERROR, "goog.require"); return; } switch (parent.getToken()) { case EXPR_RESULT: String key = extractFirstArgumentName(callNode); if (!currentModule.importsByLongRequiredName.containsKey(key)) { currentModule.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()); } currentModule.importsByLongRequiredName.put(extractFirstArgumentName(callNode), lhs); for (Node nameNode : NodeUtil.findLhsNodesInNode(declaration)) { String name = nameNode.getString(); if (!currentModule.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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy