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

com.google.javascript.jscomp.CheckJSDoc 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.Preconditions.checkState;

import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * Checks for misplaced, misused or deprecated JSDoc annotations.
 */
final class CheckJSDoc extends AbstractPostOrderCallback implements HotSwapCompilerPass {

  public static final DiagnosticType MISPLACED_MSG_ANNOTATION =
      DiagnosticType.disabled(
          "JSC_MISPLACED_MSG_ANNOTATION",
          "Misplaced message annotation. @desc, @hidden, and @meaning annotations should only "
              + "be on message nodes.\nMessage constants must be prefixed with 'MSG_'.");

  public static final DiagnosticType MISPLACED_ANNOTATION =
      DiagnosticType.warning("JSC_MISPLACED_ANNOTATION",
          "Misplaced {0} annotation. {1}");

  public static final DiagnosticType ANNOTATION_DEPRECATED =
      DiagnosticType.warning("JSC_ANNOTATION_DEPRECATED",
          "The {0} annotation is deprecated. {1}");

  public static final DiagnosticType DISALLOWED_MEMBER_JSDOC =
      DiagnosticType.warning("JSC_DISALLOWED_MEMBER_JSDOC",
          "Class level JSDocs (@interface, @extends, etc.) are not allowed on class members");

  static final DiagnosticType ARROW_FUNCTION_AS_CONSTRUCTOR = DiagnosticType.error(
      "JSC_ARROW_FUNCTION_AS_CONSTRUCTOR",
      "Arrow functions cannot be used as constructors");

  static final DiagnosticType DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL = DiagnosticType.error(
      "JSC_DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL",
      "Inline JSDoc on default parameters must be marked as optional");

  public static final DiagnosticType INVALID_NO_SIDE_EFFECT_ANNOTATION =
      DiagnosticType.error(
          "JSC_INVALID_NO_SIDE_EFFECT_ANNOTATION",
          "@nosideeffects may only appear in externs files.");

  public static final DiagnosticType INVALID_MODIFIES_ANNOTATION =
      DiagnosticType.error(
          "JSC_INVALID_MODIFIES_ANNOTATION", "@modifies may only appear in externs files.");

  public static final DiagnosticType INVALID_DEFINE_ON_LET =
      DiagnosticType.error(
          "JSC_INVALID_DEFINE_ON_LET",
          "variables annotated with @define may only be declared with VARs, ASSIGNs, or CONSTs");

  public static final DiagnosticType MISPLACED_SUPPRESS =
      DiagnosticType.warning(
          "JSC_MISPLACED_SUPPRESS",
          "@suppress annotation not allowed here. See"
              + " https://github.com/google/closure-compiler/wiki/@suppress-annotations");

  public static final DiagnosticType JSDOC_IN_BLOCK_COMMENT =
      DiagnosticType.warning(
          "JSC_JSDOC_IN_BLOCK_COMMENT",
          "Non-JSDoc comment has annotations. Did you mean to start it with '/**'?");

  public static final DiagnosticType JSDOC_ON_RETURN =
      DiagnosticType.warning(
          "JSC_JSDOC_ON_RETURN", "JSDoc annotations are not supported on return.");

  private static final Pattern COMMENT_PATTERN =
      Pattern.compile("(/|(\n[ \t]*))\\*[ \t]*@[a-zA-Z]+[ \t\n{]");

  private final AbstractCompiler compiler;
  private boolean inExterns;

  CheckJSDoc(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    inExterns = true;
    NodeTraversal.traverse(compiler, externs, this);
    inExterns = false;
    NodeTraversal.traverse(compiler, root, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  /**
   * Checks for block comments (e.g. starting with /*) that look like they are JsDoc, and thus
   * should start with /**.
   */
  private void checkJsDocInBlockComments(String fileName) {
    if (!compiler.getOptions().preservesDetailedSourceInfo()) {
      // Comments only available if preservesDetailedSourceInfo is true.
      return;
    }

    for (Comment comment : compiler.getComments(fileName)) {
      if (comment.type == Comment.Type.BLOCK) {
        if (COMMENT_PATTERN.matcher(comment.value).find()) {
          compiler.report(
              JSError.make(
                  fileName,
                  comment.location.start.line + 1,
                  comment.location.start.column,
                  JSDOC_IN_BLOCK_COMMENT));
        }
      }
    }
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (n.isScript()) {
      checkJsDocInBlockComments(n.getSourceFileName());
    }
    JSDocInfo info = n.getJSDocInfo();
    validateTypeAnnotations(n, info);
    validateFunctionJsDoc(n, info);
    validateMsgJsDoc(n, info);
    validateDeprecatedJsDoc(n, info);
    validateNoCollapse(n, info);
    validateClassLevelJsDoc(n, info);
    validateArrowFunction(n);
    validateDefaultValue(n);
    validateTemplates(n, info);
    validateTypedefs(n, info);
    validateNoSideEffects(n, info);
    validateAbstractJsDoc(n, info);
    validateDefinesDeclaration(n, info);
    validateSuppress(n, info);
    validateImplicitCast(n, info);
    validateClosurePrimitive(n, info);
    validateReturnJsDoc(n, info);
  }

  private void validateSuppress(Node n, JSDocInfo info) {
    if (info == null || info.getSuppressions().isEmpty()) {
      return;
    }
    switch (n.getToken()) {
      case FUNCTION:
      case CLASS:
      case VAR:
      case LET:
      case CONST:
      case SCRIPT:
      case MEMBER_FUNCTION_DEF:
      case GETTER_DEF:
      case SETTER_DEF:
        // Suppressions are always valid here.
        return;

      case COMPUTED_PROP:
        if (n.getLastChild().isFunction()) {
          return; // Suppressions are valid on computed properties that declare functions.
        }
        break;

      case STRING_KEY:
        if (n.getParent().isObjectLit()) {
          return;
        }
        break;

      case ASSIGN:
      case ASSIGN_BITOR:
      case ASSIGN_BITXOR:
      case ASSIGN_BITAND:
      case ASSIGN_LSH:
      case ASSIGN_RSH:
      case ASSIGN_URSH:
      case ASSIGN_ADD:
      case ASSIGN_SUB:
      case ASSIGN_MUL:
      case ASSIGN_DIV:
      case ASSIGN_MOD:
      case ASSIGN_EXPONENT:
      case GETPROP:
        if (n.getParent().isExprResult()) {
          return;
        }
        break;

      case CALL:
        // TODO(blickly): Stop ignoring no-op extraProvide suppression.
        // We don't actually support extraProvide, but if we did, it would go on a CALL.
        if (containsOnlySuppressionFor(info, "extraRequire")
            || containsOnlySuppressionFor(info, "extraProvide")) {
          return;
        }
        break;

      case WITH:
        if (containsOnlySuppressionFor(info, "with")) {
          return;
        }
        break;

      default:
        break;
    }
    if (containsOnlySuppressionFor(info, "missingRequire")) {
      return;
    }
    compiler.report(JSError.make(n, MISPLACED_SUPPRESS));
  }

  private static boolean containsOnlySuppressionFor(JSDocInfo jsdoc, String allowedSuppression) {
    Set suppressions = jsdoc.getSuppressions();
    return suppressions.size() == 1
        && Iterables.getOnlyElement(suppressions).equals(allowedSuppression);
  }

  private void validateTypedefs(Node n, JSDocInfo info) {
    if (info == null || !info.hasTypedefType()) {
      return;
    }
    if (isClassDecl(n)) {
      reportMisplaced(n, "typedef", "@typedef is not allowed on a class declaration.");
      return;
    }
    Node lvalue = NodeUtil.isNameDeclaration(n) || n.isAssign() ? n.getFirstChild() : n;
    // Static properties for goog.defineClass are rewritten to qualified names before typechecking
    // runs and are valid as @typedefs.
    if (!lvalue.isQualifiedName() && !isGoogDefineClassStatic(lvalue)) {
      reportMisplaced(
          n,
          "typedef",
          "@typedef is only allowed on qualified name declarations. Did you mean @type?");
    } else if (isPrototypeOrInstanceDecl(lvalue)) {
      reportMisplaced(
          n,
          "typedef",
          "@typedef is not allowed on instance or prototype properties. Did you mean @type?");
    }
  }

  /** Whether this is a property in this object: {@code goog.defineClass(superClass, {statics: {} */
  private boolean isGoogDefineClassStatic(Node n) {
    return n.isStringKey()
        && n.getParent().isObjectLit()
        && n.getGrandparent().isStringKey()
        && n.getGrandparent().getString().equals("statics");
  }

  private void validateTemplates(Node n, JSDocInfo info) {
    if (info != null
        && !info.getTemplateTypeNames().isEmpty()
        && !info.isConstructorOrInterface()
        && !isClassDecl(n)
        && !info.containsFunctionDeclaration()
        && getFunctionDecl(n) == null) {
        reportMisplaced(n, "template",
            "@template is only allowed in class, constructor, interface, function "
            + "or method declarations");
    }
  }

  /**
   * @return The function node associated with the function declaration associated with the
   *     specified node, no null if no such function exists.
   */
  @Nullable
  private static Node getFunctionDecl(Node n) {
    if (n.isFunction()) {
      return n;
    }
    if (n.isMemberFunctionDef()) {
      return n.getFirstChild();
    }
    if (NodeUtil.isNameDeclaration(n)
        && n.getFirstFirstChild() != null
        && n.getFirstFirstChild().isFunction()) {
      return n.getFirstFirstChild();
    }

    if (n.isAssign() && n.getFirstChild().isQualifiedName() && n.getLastChild().isFunction()) {
      return n.getLastChild();
    }

    if (n.isStringKey() && n.getGrandparent() != null
        && ClosureRewriteClass.isGoogDefineClass(n.getGrandparent())
        && n.getFirstChild().isFunction()) {
      return n.getFirstChild();
    }

    if (n.isGetterDef() || n.isSetterDef()) {
      return n.getFirstChild();
    }

    if (n.isComputedProp() && n.getLastChild().isFunction()) {
      return n.getLastChild();
    }

    return null;
  }

  private boolean isClassDecl(Node n) {
    return isClass(n)
        || (n.isAssign() && isClass(n.getLastChild()))
        || (NodeUtil.isNameDeclaration(n) && isNameInitializeWithClass(n.getFirstChild()))
        || isNameInitializeWithClass(n);
  }

  private boolean isNameInitializeWithClass(Node n) {
    return n != null && n.isName() && n.hasChildren() && isClass(n.getFirstChild());
  }

  private boolean isClass(Node n) {
    return n.isClass()
        || (n.isCall() && compiler.getCodingConvention().isClassFactoryCall(n));
  }

  private static boolean isPrototypeOrInstanceDecl(Node n) {
    if (n.isStringKey()) {
      return false;
    }
    if (NodeUtil.isPrototypeProperty(n)) {
      return true;
    }
    Node receiver = NodeUtil.getRootOfQualifiedName(n);
    return receiver.isThis() || receiver.isSuper();
  }

  /**
   * Checks that class-level annotations like @interface/@extends are not used on member functions.
   */
  private void validateClassLevelJsDoc(Node n, JSDocInfo info) {
    if (info != null && n.isMemberFunctionDef()
        && hasClassLevelJsDoc(info)) {
      report(n, DISALLOWED_MEMBER_JSDOC);
    }
  }

  private void validateAbstractJsDoc(Node n, JSDocInfo info) {
    if (info == null || !info.isAbstract()) {
      return;
    }
    if (isClassDecl(n)) {
      return;
    }

    Node functionNode = getFunctionDecl(n);

    if (functionNode == null) {
      // @abstract annotation on a non-function
      report(
          n,
          MISPLACED_ANNOTATION,
          "@abstract",
          "only functions or non-static methods can be abstract");
      return;
    }

    if (!info.isConstructor() && NodeUtil.getFunctionBody(functionNode).hasChildren()) {
      // @abstract annotation on a function with a non-empty body
      report(n, MISPLACED_ANNOTATION, "@abstract",
          "function with a non-empty body cannot be abstract");
      return;
    }

    // TODO(b/124020008): Delete this case when `goog.defineClass` is dropped.
    boolean isGoogDefineClassConstructor =
        n.getParent().isObjectLit()
            && (n.isMemberFunctionDef() || n.isStringKey())
            && "constructor".equals(n.getString());
    if (NodeUtil.isEs6ConstructorMemberFunctionDef(n) || isGoogDefineClassConstructor) {
      // @abstract annotation on an ES6 or goog.defineClass constructor
      report(n, MISPLACED_ANNOTATION, "@abstract", "constructors cannot be abstract");
      return;
    }

    if (!info.isConstructor()
        && !n.isMemberFunctionDef()
        && !n.isStringKey()
        && !n.isComputedProp()
        && !n.isGetterDef()
        && !n.isSetterDef()
        && !NodeUtil.isPrototypeMethod(functionNode)) {
      // @abstract annotation on a non-method (or static method) in ES5
      report(
          n,
          MISPLACED_ANNOTATION,
          "@abstract",
          "only functions or non-static methods can be abstract");
      return;
    }

    if (n.isStaticMember()) {
      // @abstract annotation on a static method in ES6
      report(n, MISPLACED_ANNOTATION, "@abstract", "static methods cannot be abstract");
      return;
    }
  }

  private static boolean hasClassLevelJsDoc(JSDocInfo info) {
    return info.isConstructorOrInterface()
        || info.hasBaseType()
        || info.getImplementedInterfaceCount() != 0
        || info.getExtendedInterfacesCount() != 0;
  }

  /**
   * Checks that deprecated annotations such as @expose are not present
   */
  private void validateDeprecatedJsDoc(Node n, JSDocInfo info) {
    if (info != null && info.isExpose()) {
      report(n, ANNOTATION_DEPRECATED, "@expose",
              "Use @nocollapse or @export instead.");
    }
  }

  /**
   * Warns when nocollapse annotations are present on nodes
   * which are not eligible for property collapsing.
   */
  private void validateNoCollapse(Node n, JSDocInfo info) {
    if (n.isFromExterns()) {
      if (info != null && info.isNoCollapse()) {
        // @nocollapse has no effect in externs
        reportMisplaced(n, "nocollapse", "This JSDoc has no effect in externs.");
      }
      return;
    }
    if (!NodeUtil.isPrototypePropertyDeclaration(n.getParent())) {
      return;
    }
    JSDocInfo jsdoc = n.getJSDocInfo();
    if (jsdoc != null && jsdoc.isNoCollapse()) {
      reportMisplaced(n, "nocollapse", "This JSDoc has no effect on prototype properties.");
    }
  }

  /**
   * Checks that JSDoc intended for a function is actually attached to a
   * function.
   */
  private void validateFunctionJsDoc(Node n, JSDocInfo info) {
    if (info == null) {
      return;
    }

    if (info.containsFunctionDeclaration() && !info.hasType() && !isJSDocOnFunctionNode(n, info)) {
      // This JSDoc should be attached to a FUNCTION node, or an assignment
      // with a function as the RHS, etc.

      reportMisplaced(
          n,
          "function",
          "This JSDoc is not attached to a function node. " + "Are you missing parentheses?");
    }
  }

  /**
   * Whether this node's JSDoc may apply to a function
   *
   * 

This has some false positive cases, to allow for patterns like goog.abstractMethod. */ private boolean isJSDocOnFunctionNode(Node n, JSDocInfo info) { switch (n.getToken()) { case FUNCTION: case GETTER_DEF: case SETTER_DEF: case MEMBER_FUNCTION_DEF: case STRING_KEY: case COMPUTED_PROP: case EXPORT: return true; case GETELEM: case GETPROP: if (n.getFirstChild().isQualifiedName()) { // assume qualified names may be function declarations return true; } return false; case VAR: case LET: case CONST: case ASSIGN: { Node lhs = n.getFirstChild(); Node rhs = NodeUtil.getRValueOfLValue(lhs); if (rhs != null && isClass(rhs) && !info.isConstructor()) { return false; } // TODO(b/124081098): Check that the RHS of the assignment is a // function. Note that it can be a FUNCTION node, but it can also be // a call to goog.abstractMethod, goog.functions.constant, etc. return true; } default: return false; } } /** * Checks that annotations for messages ({@code @desc}, {@code @hidden}, * and {@code @meaning}) * are in the proper place, namely on names starting with MSG_ which * indicates they should be * extracted for translation. A later pass checks that the right side is * a call to goog.getMsg. */ private void validateMsgJsDoc(Node n, JSDocInfo info) { if (info == null) { return; } if (info.getDescription() != null || info.isHidden() || info.getMeaning() != null) { boolean descOkay = false; switch (n.getToken()) { case ASSIGN: case VAR: case LET: case CONST: descOkay = isValidMsgName(n.getFirstChild()); break; case STRING_KEY: descOkay = isValidMsgName(n); break; case GETPROP: if (n.isFromExterns() && n.isQualifiedName()) { descOkay = isValidMsgName(n); } break; default: break; } if (!descOkay) { report(n, MISPLACED_MSG_ANNOTATION); } } } /** Returns whether of not the given name is valid target for the result of goog.getMsg */ private static boolean isValidMsgName(Node nameNode) { if (nameNode.isName() || nameNode.isStringKey()) { return nameNode.getString().startsWith("MSG_"); } else if (nameNode.isQualifiedName()) { return nameNode.getLastChild().getString().startsWith("MSG_"); } else { return false; } } /** * Check that JSDoc with a {@code @type} annotation is in a valid place. */ private void validateTypeAnnotations(Node n, JSDocInfo info) { if (info != null && info.hasType()) { boolean valid = false; switch (n.getToken()) { // Function declarations are valid case FUNCTION: valid = NodeUtil.isFunctionDeclaration(n); break; // Object literal properties, catch declarations and variable // initializers are valid. case NAME: valid = isTypeAnnotationAllowedForName(n); break; case ARRAY_PATTERN: case OBJECT_PATTERN: // allow JSDoc like // function f(/** !Object */ {x}) {} // function f(/** !Array */ [x]) {} valid = n.getParent().isParamList(); break; // Casts, exports, and Object literal properties are valid. case CAST: case EXPORT: case STRING_KEY: case GETTER_DEF: case SETTER_DEF: valid = true; break; // Declarations are valid iff they only contain simple names // /** @type {number} */ var x = 3; // ok // /** @type {number} */ var {x} = obj; // forbidden case VAR: case LET: case CONST: valid = !NodeUtil.isDestructuringDeclaration(n); break; // Property assignments are valid, if at the root of an expression. case ASSIGN: { Node lvalue = n.getFirstChild(); valid = n.getParent().isExprResult() && (lvalue.isGetProp() || lvalue.isGetElem() || lvalue.matchesName("exports")); break; } case GETPROP: valid = n.getParent().isExprResult() && n.isQualifiedName(); break; case CALL: valid = info.isDefine(); break; default: break; } if (!valid) { reportMisplaced(n, "type", "Type annotations are not allowed here. " + "Are you missing parentheses?"); } } } /** Is it valid to have a type annotation on the given NAME node? */ private static boolean isTypeAnnotationAllowedForName(Node n) { checkState(n.isName(), n); // Only allow type annotations on nodes used as an lvalue. if (!NodeUtil.isLValue(n)) { return false; } // Don't allow JSDoc on a name in an assignment. Simple names should only have JSDoc on them // when originally declared. Node rootTarget = NodeUtil.getRootTarget(n); return !NodeUtil.isLhsOfAssign(rootTarget); } private void reportMisplaced(Node n, String annotationName, String note) { compiler.report(JSError.make(n, MISPLACED_ANNOTATION, annotationName, note)); } private void report(Node n, DiagnosticType type, String... arguments) { compiler.report(JSError.make(n, type, arguments)); } /** * Check that an arrow function is not annotated with {@constructor}. */ private void validateArrowFunction(Node n) { if (n.isArrowFunction()) { JSDocInfo info = NodeUtil.getBestJSDocInfo(n); if (info != null && info.isConstructorOrInterface()) { report(n, ARROW_FUNCTION_AS_CONSTRUCTOR); } } } /** * Check that a parameter with a default value is marked as optional. * TODO(bradfordcsmith): This is redundant. We shouldn't require it. */ private void validateDefaultValue(Node n) { if (n.isDefaultValue() && n.getParent().isParamList()) { Node targetNode = n.getFirstChild(); JSDocInfo info = targetNode.getJSDocInfo(); if (info == null) { return; } JSTypeExpression typeExpr = info.getType(); if (typeExpr == null) { return; } Node typeNode = typeExpr.getRoot(); if (typeNode.getToken() != Token.EQUALS) { report(typeNode, DEFAULT_PARAM_MUST_BE_MARKED_OPTIONAL); } } } /** * Check that @nosideeeffects annotations are only present in externs. */ private void validateNoSideEffects(Node n, JSDocInfo info) { // Cannot have @modifies or @nosideeffects in regular (non externs) js. Report errors. if (info == null) { return; } if (n.isFromExterns()) { return; } if (info.hasSideEffectsArgumentsAnnotation() || info.modifiesThis()) { report(n, INVALID_MODIFIES_ANNOTATION); } if (info.isNoSideEffects()) { report(n, INVALID_NO_SIDE_EFFECT_ANNOTATION); } } /** * Check that a let declaration is not used with {@defines} */ private void validateDefinesDeclaration(Node n, JSDocInfo info) { if (info != null && info.isDefine() && n.isLet()) { report(n, INVALID_DEFINE_ON_LET); } } /** Checks that an @implicitCast annotation is in the externs */ private void validateImplicitCast(Node n, JSDocInfo info) { if (!inExterns && info != null && info.isImplicitCast()) { report(n, TypeCheck.ILLEGAL_IMPLICIT_CAST); } } /** Checks that a @closurePrimitive {id} is on a function */ private void validateClosurePrimitive(Node n, JSDocInfo info) { if (info == null || !info.hasClosurePrimitiveId()) { return; } if (!isJSDocOnFunctionNode(n, info)) { report(n, MISPLACED_ANNOTATION, "closurePrimitive", "must be on a function node"); } } /** Checks that there are no annotations on return. */ private void validateReturnJsDoc(Node n, JSDocInfo info) { if (!n.isReturn() || info == null) { return; } // @type and @typedef are handled separately if (info.containsDeclaration() && !info.hasType() && !info.hasTypedefType()) { report(n, JSDOC_ON_RETURN); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy