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

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

/*
 * 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 com.google.common.base.Preconditions;
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.Map;

/**
 * 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_GOOG_MODULE", "@export is not allowed here in a non-legacy goog.module." + " Consider using goog.exportSymbol instead."); 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 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 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 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 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 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 String currentModuleName = null; private Map shortRequiredNamespaces = new HashMap<>(); private Node defaultExportNode = 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")) { Preconditions.checkState(currentModuleName == null); currentModuleName = extractFirstArgumentName(call); } } } @Override public void exitModule(NodeTraversal t, Node scopeRoot) { currentModuleName = null; shortRequiredNamespaces.clear(); defaultExportNode = null; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (currentModuleName == null) { 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") && !currentModuleName.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 (currentModuleName != null && n.matchesQualifiedName(currentModuleName)) { t.report(n, REFERENCE_TO_MODULE_GLOBAL_NAME); } else if (shortRequiredNamespaces.containsKey(n.getQualifiedName())) { String shortName = shortRequiredNamespaces.get(n.getQualifiedName()); if (shortName == null) { t.report(n, REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, n.getQualifiedName()); } else { t.report(n, REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, n.getQualifiedName(), shortName); } } 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 (shortRequiredNamespaces.containsKey(type)) { String shortName = shortRequiredNamespaces.get(type); if (shortName == null) { t.report(node, JSDOC_REFERENCE_TO_FULLY_QUALIFIED_IMPORT_NAME, type); } else if (!shortName.equals(type)) { t.report( node, JSDOC_REFERENCE_TO_SHORT_IMPORT_BY_LONG_NAME_INCLUDING_SHORT_NAME, type, shortName); } } 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) { Preconditions.checkArgument(n.isAssign()); Node lhs = n.getFirstChild(); Preconditions.checkState(isExportLhs(lhs)); if (defaultExportNode == null && (!t.inModuleScope() || !parent.isExprResult())) { // Invalid export location. t.report(n, EXPORT_NOT_A_MODULE_LEVEL_STATEMENT); } if (lhs.isName()) { if (defaultExportNode != null) { // Multiple exports t.report(n, EXPORT_REPEATED_ERROR, String.valueOf(defaultExportNode.getLineno())); } 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) { Preconditions.checkState(callNode.isCall()); switch (parent.getToken()) { case EXPR_RESULT: checkShortGoogRequireCall(t, callNode, 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) { String shortName = null; if (NodeUtil.isNameDeclaration(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); } Node lhs = declaration.getFirstChild(); if (lhs.isDestructuringLhs() && !isValidDestructuringImport(lhs)) { t.report(declaration, INVALID_DESTRUCTURING_REQUIRE); } shortName = lhs.isName() ? lhs.getString() : null; } shortRequiredNamespaces.put(extractFirstArgumentName(callNode), shortName); } private static boolean isValidDestructuringImport(Node destructuringLhs) { Preconditions.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 - 2025 Weber Informatics LLC | Privacy Policy