2 * @fileoverview Validates JSDoc comments are syntactically correct
3 * @author Nicholas C. Zakas
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const doctrine = require("doctrine");
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
22 description: "enforce valid JSDoc comments",
23 category: "Possible Errors",
25 url: "https://eslint.org/docs/rules/valid-jsdoc"
34 additionalProperties: {
40 additionalProperties: {
48 requireParamDescription: {
52 requireReturnDescription: {
68 additionalProperties: false
74 unexpectedTag: "Unexpected @{{title}} tag; function has no return statement.",
75 expected: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.",
76 use: "Use @{{name}} instead.",
77 useType: "Use '{{expectedTypeName}}' instead of '{{currentTypeName}}'.",
78 syntaxError: "JSDoc syntax error.",
79 missingBrace: "JSDoc type missing brace.",
80 missingParamDesc: "Missing JSDoc parameter description for '{{name}}'.",
81 missingParamType: "Missing JSDoc parameter type for '{{name}}'.",
82 missingReturnType: "Missing JSDoc return type.",
83 missingReturnDesc: "Missing JSDoc return description.",
84 missingReturn: "Missing JSDoc @{{returns}} for function.",
85 missingParam: "Missing JSDoc for parameter '{{name}}'.",
86 duplicateParam: "Duplicate JSDoc parameter '{{name}}'.",
87 unsatisfiedDesc: "JSDoc description does not satisfy the regex pattern."
96 const options = context.options[0] || {},
97 prefer = options.prefer || {},
98 sourceCode = context.getSourceCode(),
100 // these both default to true, so you have to explicitly make them false
101 requireReturn = options.requireReturn !== false,
102 requireParamDescription = options.requireParamDescription !== false,
103 requireReturnDescription = options.requireReturnDescription !== false,
104 requireReturnType = options.requireReturnType !== false,
105 requireParamType = options.requireParamType !== false,
106 preferType = options.preferType || {},
107 checkPreferType = Object.keys(preferType).length !== 0;
109 //--------------------------------------------------------------------------
111 //--------------------------------------------------------------------------
113 // Using a stack to store if a function returns or not (handling nested functions)
117 * Check if node type is a Class
118 * @param {ASTNode} node node to check.
119 * @returns {boolean} True is its a class
122 function isTypeClass(node) {
123 return node.type === "ClassExpression" || node.type === "ClassDeclaration";
127 * When parsing a new function, store it in our function stack.
128 * @param {ASTNode} node A function node to check.
132 function startFunction(node) {
134 returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") ||
135 isTypeClass(node) || node.async
140 * Indicate that return has been found in the current function.
141 * @param {ASTNode} node The return node.
145 function addReturn(node) {
146 const functionState = fns[fns.length - 1];
148 if (functionState && node.argument !== null) {
149 functionState.returnPresent = true;
154 * Check if return tag type is void or undefined
155 * @param {Object} tag JSDoc tag
156 * @returns {boolean} True if its of type void or undefined
159 function isValidReturnType(tag) {
160 return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral";
164 * Check if type should be validated based on some exceptions
165 * @param {Object} type JSDoc tag
166 * @returns {boolean} True if it can be validated
169 function canTypeBeValidated(type) {
170 return type !== "UndefinedLiteral" && // {undefined} as there is no name property available.
171 type !== "NullLiteral" && // {null}
172 type !== "NullableLiteral" && // {?}
173 type !== "FunctionType" && // {function(a)}
174 type !== "AllLiteral"; // {*}
178 * Extract the current and expected type based on the input type object
179 * @param {Object} type JSDoc tag
180 * @returns {{currentType: Doctrine.Type, expectedTypeName: string}} The current type annotation and
181 * the expected name of the annotation
184 function getCurrentExpectedTypes(type) {
189 } else if (type.expression) {
190 currentType = type.expression;
195 expectedTypeName: currentType && preferType[currentType.name]
200 * Gets the location of a JSDoc node in a file
201 * @param {Token} jsdocComment The comment that this node is parsed from
202 * @param {{range: number[]}} parsedJsdocNode A tag or other node which was parsed from this comment
203 * @returns {{start: SourceLocation, end: SourceLocation}} The 0-based source location for the tag
205 function getAbsoluteRange(jsdocComment, parsedJsdocNode) {
207 start: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[0]),
208 end: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[1])
213 * Validate type for a given JSDoc node
214 * @param {Object} jsdocNode JSDoc node
215 * @param {Object} type JSDoc tag
219 function validateType(jsdocNode, type) {
220 if (!type || !canTypeBeValidated(type.type)) {
224 const typesToCheck = [];
228 case "TypeApplication": // {Array.<String>}
229 elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications;
230 typesToCheck.push(getCurrentExpectedTypes(type));
232 case "RecordType": // {{20:String}}
233 elements = type.fields;
235 case "UnionType": // {String|number|Test}
236 case "ArrayType": // {[String, number, Test]}
237 elements = type.elements;
239 case "FieldType": // Array.<{count: number, votes: number}>
241 typesToCheck.push(getCurrentExpectedTypes(type.value));
245 typesToCheck.push(getCurrentExpectedTypes(type));
248 elements.forEach(validateType.bind(null, jsdocNode));
250 typesToCheck.forEach(typeToCheck => {
251 if (typeToCheck.expectedTypeName &&
252 typeToCheck.expectedTypeName !== typeToCheck.currentType.name) {
255 messageId: "useType",
256 loc: getAbsoluteRange(jsdocNode, typeToCheck.currentType),
258 currentTypeName: typeToCheck.currentType.name,
259 expectedTypeName: typeToCheck.expectedTypeName
262 return fixer.replaceTextRange(
263 typeToCheck.currentType.range.map(indexInComment => jsdocNode.range[0] + 2 + indexInComment),
264 typeToCheck.expectedTypeName
273 * Validate the JSDoc node and output warnings if anything is wrong.
274 * @param {ASTNode} node The AST node to check.
278 function checkJSDoc(node) {
279 const jsdocNode = sourceCode.getJSDocComment(node),
280 functionData = fns.pop(),
281 paramTagsByName = Object.create(null),
283 let hasReturns = false,
285 hasConstructor = false,
290 // make sure only to validate JSDoc comments
295 jsdoc = doctrine.parse(jsdocNode.value, {
303 if (/braces/iu.test(ex.message)) {
304 context.report({ node: jsdocNode, messageId: "missingBrace" });
306 context.report({ node: jsdocNode, messageId: "syntaxError" });
312 jsdoc.tags.forEach(tag => {
314 switch (tag.title.toLowerCase()) {
330 hasConstructor = true;
350 // check tag preferences
351 if (Object.prototype.hasOwnProperty.call(prefer, tag.title) && tag.title !== prefer[tag.title]) {
352 const entireTagRange = getAbsoluteRange(jsdocNode, tag);
358 start: entireTagRange.start,
360 line: entireTagRange.start.line,
361 column: entireTagRange.start.column + `@${tag.title}`.length
364 data: { name: prefer[tag.title] },
366 return fixer.replaceTextRange(
368 jsdocNode.range[0] + tag.range[0] + 3,
369 jsdocNode.range[0] + tag.range[0] + tag.title.length + 3
377 // validate the types
378 if (checkPreferType && tag.type) {
379 validateType(jsdocNode, tag.type);
383 paramTags.forEach(param => {
384 if (requireParamType && !param.type) {
387 messageId: "missingParamType",
388 loc: getAbsoluteRange(jsdocNode, param),
389 data: { name: param.name }
392 if (!param.description && requireParamDescription) {
395 messageId: "missingParamDesc",
396 loc: getAbsoluteRange(jsdocNode, param),
397 data: { name: param.name }
400 if (paramTagsByName[param.name]) {
403 messageId: "duplicateParam",
404 loc: getAbsoluteRange(jsdocNode, param),
405 data: { name: param.name }
407 } else if (param.name.indexOf(".") === -1) {
408 paramTagsByName[param.name] = param;
413 if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
416 messageId: "unexpectedTag",
417 loc: getAbsoluteRange(jsdocNode, returnsTag),
419 title: returnsTag.title
423 if (requireReturnType && !returnsTag.type) {
424 context.report({ node: jsdocNode, messageId: "missingReturnType" });
427 if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
428 context.report({ node: jsdocNode, messageId: "missingReturnDesc" });
433 // check for functions missing @returns
434 if (!isOverride && !hasReturns && !hasConstructor && !isInterface &&
435 node.parent.kind !== "get" && node.parent.kind !== "constructor" &&
436 node.parent.kind !== "set" && !isTypeClass(node)) {
437 if (requireReturn || (functionData.returnPresent && !node.async)) {
440 messageId: "missingReturn",
442 returns: prefer.returns || "returns"
448 // check the parameters
449 const jsdocParamNames = Object.keys(paramTagsByName);
452 node.params.forEach((param, paramsIndex) => {
453 const bindingParam = param.type === "AssignmentPattern"
457 // TODO(nzakas): Figure out logical things to do with destructured, default, rest params
458 if (bindingParam.type === "Identifier") {
459 const name = bindingParam.name;
461 if (jsdocParamNames[paramsIndex] && (name !== jsdocParamNames[paramsIndex])) {
464 messageId: "expected",
465 loc: getAbsoluteRange(jsdocNode, paramTagsByName[jsdocParamNames[paramsIndex]]),
468 jsdocName: jsdocParamNames[paramsIndex]
471 } else if (!paramTagsByName[name] && !isOverride) {
474 messageId: "missingParam",
484 if (options.matchDescription) {
485 const regex = new RegExp(options.matchDescription, "u");
487 if (!regex.test(jsdoc.description)) {
488 context.report({ node: jsdocNode, messageId: "unsatisfiedDesc" });
496 //--------------------------------------------------------------------------
498 //--------------------------------------------------------------------------
501 ArrowFunctionExpression: startFunction,
502 FunctionExpression: startFunction,
503 FunctionDeclaration: startFunction,
504 ClassExpression: startFunction,
505 ClassDeclaration: startFunction,
506 "ArrowFunctionExpression:exit": checkJSDoc,
507 "FunctionExpression:exit": checkJSDoc,
508 "FunctionDeclaration:exit": checkJSDoc,
509 "ClassExpression:exit": checkJSDoc,
510 "ClassDeclaration:exit": checkJSDoc,
511 ReturnStatement: addReturn