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.

The 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 org.jspecify.nullness.Nullable;

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

  public static final DiagnosticType MISPLACED_MSG_ANNOTATION =
      DiagnosticType.disabled(
          "JSC_MISPLACED_MSG_ANNOTATION",
          "Misplaced message annotation. @desc, @meaning, and @alternateMessageId"
              + " annotations should be only 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 BAD_REST_PARAMETER_ANNOTATION =
      DiagnosticType.warning(
          "JSC_BAD_REST_PARAMETER_ANNOTATION",
          "Missing \"...\" in type annotation for rest parameter.");

  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);
  }

  /**
   * 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);
    validateNoCollapse(n, info);
    validateClassLevelJsDoc(n, info);
    validateArrowFunction(n);
    validateRestParameter(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);
    validateTsType(n, info);
    validateJsDocTypeNames(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:
      case MEMBER_FIELD_DEF:
      case COMPUTED_FIELD_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 WITH:
        if (containsOnlySuppressionFor(info, "with")) {
          return;
        }
        break;

      default:
        break;
    }
    if (containsOnlySuppressionFor(info, "missingRequire")) {
      return;
    }
    if (n.getParent().isExprResult()) {
      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.
   */
  private static @Nullable 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() && isClassDecl(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;
  }

  /**
   * Warns when nocollapse annotations are present on nodes
   * which are not eligible for property collapsing.
   */
  private void validateNoCollapse(Node n, JSDocInfo info) {
    if (info == null || !info.isNoCollapse()) {
      return;
    }
    if (n.isFromExterns()) {
      // @nocollapse has no effect in externs
      reportMisplaced(n, "nocollapse", "This JSDoc has no effect in externs.");
      return;
    }
    if (NodeUtil.isPrototypePropertyDeclaration(n.getParent())
        || (n.getParent().isClassMembers() && !n.isStaticMember())) {
      reportMisplaced(
          n,
          "nocollapse",
          "This JSDoc has no effect on prototype properties and non-static fields.");
    }
    if (n.isAssign()) {
      final Node assignee = n.getFirstChild();
      if (assignee.isQualifiedName()) {
        final Node rootOfQname = NodeUtil.getRootOfQualifiedName(assignee);
        if (!rootOfQname.isName()) {
          reportMisplaced(n, "nocollapse", "This JSDoc has no effect.");
        }
      }
    }
  }

  /** 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: case MEMBER_FIELD_DEF: case COMPUTED_FIELD_DEF: 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 @meaning} and * {@code @alternateMessageId}) 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; } boolean hasNonDescMsgTag = info.isHidden() || info.getMeaning() != null || info.getAlternateMessageId() != null; if (hasNonDescMsgTag // Don't error on TS gencode using @desc on a non-message. There's a lot of code that // uses @desc as a general purpose "@desc" tag || (info.getDescription() != null && !isFromTs(n))) { 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.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: case MEMBER_FIELD_DEF: case COMPUTED_FIELD_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 rest parameter has JSDoc marked as variadic. */ private void validateRestParameter(Node restParam) { if (!restParam.isRest() || !restParam.getParent().isParamList()) { return; } Node paramList = restParam.getParent(); JSDocInfo inlineInfo = restParam.getFirstChild().getJSDocInfo(); JSDocInfo functionInfo = NodeUtil.getBestJSDocInfo(paramList.getParent()); final JSTypeExpression paramTypeAnnotation; if (inlineInfo != null) { paramTypeAnnotation = inlineInfo.getType(); } else if (functionInfo != null) { if (restParam.getFirstChild().isName()) { String paramName = restParam.getFirstChild().getString(); paramTypeAnnotation = functionInfo.getParameterType(paramName); } else { // destructuring rest param. use the nth JSDoc parameter if present. the name will not match int indexOfRest = paramList.getIndexOfChild(restParam); paramTypeAnnotation = functionInfo.getParameterCount() >= indexOfRest ? functionInfo.getParameterType(functionInfo.getParameterNameAt(indexOfRest)) : null; } } else { paramTypeAnnotation = null; } if (paramTypeAnnotation != null && paramTypeAnnotation.getRoot().getToken() != Token.ITER_REST) { compiler.report(JSError.make(restParam, BAD_REST_PARAMETER_ANNOTATION)); } } /** * 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 @modifies annotations are only present in externs. */ private void validateNoSideEffects(Node n, JSDocInfo info) { // Cannot have @modifies 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); } } /** * 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); } } /** Checks that a @tsType is on a function in a supported file */ private void validateTsType(Node n, JSDocInfo info) { if (info == null || info.getTsTypes().isEmpty()) { return; } if (!isJSDocOnFunctionNode(n, info)) { report(n, MISPLACED_ANNOTATION, "tsType", "must be on a function node"); } } private static boolean isFromTs(Node n) { return n.getSourceFileName().endsWith(".closure.js"); } private final CheckJsdocTypes checkJsDocTypesVisitor = new CheckJsdocTypes(); private void validateJsDocTypeNames(JSDocInfo info) { if (info == null) { return; } for (Node typeNode : info.getTypeNodes()) { NodeUtil.visitPreOrder(typeNode, checkJsDocTypesVisitor); } } /** Ban any references to compiler internal implementation details */ private static final class CheckJsdocTypes implements NodeUtil.Visitor { @Override public void visit(Node typeRefNode) { if (!typeRefNode.isStringLit()) { return; } // A type name that might be simple like "Foo" or qualified like "foo.Bar". final String typeName = typeRefNode.getString(); int dot = typeName.indexOf('.'); String rootOfType = dot == -1 ? typeName : typeName.substring(0, dot); // Prevent handwritten JS from referencing a module export or module content name that's // synthesized by ClosureRewriteModule. Prefix the JSDoc references with // "UnrecognizedType_" and leave it to the typechecker to report a JSC_UNRECOGNIZED_TYPE_ERROR // * why not report an error here? even if we did report an error, we // should still add the prefix to ensure the typechecker doesn't resolve this type. Some // builds and/or files suppress type errors. // TODO(lharker): consider reporting an unsuppressible error instead of doing this // rewriting, if we can clean up all existing violations of this error. // * why do this here instead of in the ClosureRewriteModule pass? the Es6RewriteModule runs // before ClosureRewriteModule and may add references to these module export names. // * note: for references in code, not JSDoc, undefined variable checks will handle this. // TODO(b/144593112): remove this when ClosureRewriteModule always runs after typechecking if (ClosureRewriteModule.isModuleExport(rootOfType) || ClosureRewriteModule.isModuleContent(rootOfType)) { typeRefNode.setString("UnrecognizedType_" + typeName); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy