]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/valid-jsdoc.js
import 8.4.0 source
[pve-eslint.git] / eslint / lib / rules / valid-jsdoc.js
1 /**
2 * @fileoverview Validates JSDoc comments are syntactically correct
3 * @author Nicholas C. Zakas
4 * @deprecated in ESLint v5.10.0
5 */
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const doctrine = require("doctrine");
13
14 //------------------------------------------------------------------------------
15 // Rule Definition
16 //------------------------------------------------------------------------------
17
18 /** @type {import('../shared/types').Rule} */
19 module.exports = {
20 meta: {
21 type: "suggestion",
22
23 docs: {
24 description: "enforce valid JSDoc comments",
25 recommended: false,
26 url: "https://eslint.org/docs/rules/valid-jsdoc"
27 },
28
29 schema: [
30 {
31 type: "object",
32 properties: {
33 prefer: {
34 type: "object",
35 additionalProperties: {
36 type: "string"
37 }
38 },
39 preferType: {
40 type: "object",
41 additionalProperties: {
42 type: "string"
43 }
44 },
45 requireReturn: {
46 type: "boolean",
47 default: true
48 },
49 requireParamDescription: {
50 type: "boolean",
51 default: true
52 },
53 requireReturnDescription: {
54 type: "boolean",
55 default: true
56 },
57 matchDescription: {
58 type: "string"
59 },
60 requireReturnType: {
61 type: "boolean",
62 default: true
63 },
64 requireParamType: {
65 type: "boolean",
66 default: true
67 }
68 },
69 additionalProperties: false
70 }
71 ],
72
73 fixable: "code",
74 messages: {
75 unexpectedTag: "Unexpected @{{title}} tag; function has no return statement.",
76 expected: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.",
77 use: "Use @{{name}} instead.",
78 useType: "Use '{{expectedTypeName}}' instead of '{{currentTypeName}}'.",
79 syntaxError: "JSDoc syntax error.",
80 missingBrace: "JSDoc type missing brace.",
81 missingParamDesc: "Missing JSDoc parameter description for '{{name}}'.",
82 missingParamType: "Missing JSDoc parameter type for '{{name}}'.",
83 missingReturnType: "Missing JSDoc return type.",
84 missingReturnDesc: "Missing JSDoc return description.",
85 missingReturn: "Missing JSDoc @{{returns}} for function.",
86 missingParam: "Missing JSDoc for parameter '{{name}}'.",
87 duplicateParam: "Duplicate JSDoc parameter '{{name}}'.",
88 unsatisfiedDesc: "JSDoc description does not satisfy the regex pattern."
89 },
90
91 deprecated: true,
92 replacedBy: []
93 },
94
95 create(context) {
96
97 const options = context.options[0] || {},
98 prefer = options.prefer || {},
99 sourceCode = context.getSourceCode(),
100
101 // these both default to true, so you have to explicitly make them false
102 requireReturn = options.requireReturn !== false,
103 requireParamDescription = options.requireParamDescription !== false,
104 requireReturnDescription = options.requireReturnDescription !== false,
105 requireReturnType = options.requireReturnType !== false,
106 requireParamType = options.requireParamType !== false,
107 preferType = options.preferType || {},
108 checkPreferType = Object.keys(preferType).length !== 0;
109
110 //--------------------------------------------------------------------------
111 // Helpers
112 //--------------------------------------------------------------------------
113
114 // Using a stack to store if a function returns or not (handling nested functions)
115 const fns = [];
116
117 /**
118 * Check if node type is a Class
119 * @param {ASTNode} node node to check.
120 * @returns {boolean} True is its a class
121 * @private
122 */
123 function isTypeClass(node) {
124 return node.type === "ClassExpression" || node.type === "ClassDeclaration";
125 }
126
127 /**
128 * When parsing a new function, store it in our function stack.
129 * @param {ASTNode} node A function node to check.
130 * @returns {void}
131 * @private
132 */
133 function startFunction(node) {
134 fns.push({
135 returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") ||
136 isTypeClass(node) || node.async
137 });
138 }
139
140 /**
141 * Indicate that return has been found in the current function.
142 * @param {ASTNode} node The return node.
143 * @returns {void}
144 * @private
145 */
146 function addReturn(node) {
147 const functionState = fns[fns.length - 1];
148
149 if (functionState && node.argument !== null) {
150 functionState.returnPresent = true;
151 }
152 }
153
154 /**
155 * Check if return tag type is void or undefined
156 * @param {Object} tag JSDoc tag
157 * @returns {boolean} True if its of type void or undefined
158 * @private
159 */
160 function isValidReturnType(tag) {
161 return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral";
162 }
163
164 /**
165 * Check if type should be validated based on some exceptions
166 * @param {Object} type JSDoc tag
167 * @returns {boolean} True if it can be validated
168 * @private
169 */
170 function canTypeBeValidated(type) {
171 return type !== "UndefinedLiteral" && // {undefined} as there is no name property available.
172 type !== "NullLiteral" && // {null}
173 type !== "NullableLiteral" && // {?}
174 type !== "FunctionType" && // {function(a)}
175 type !== "AllLiteral"; // {*}
176 }
177
178 /**
179 * Extract the current and expected type based on the input type object
180 * @param {Object} type JSDoc tag
181 * @returns {{currentType: Doctrine.Type, expectedTypeName: string}} The current type annotation and
182 * the expected name of the annotation
183 * @private
184 */
185 function getCurrentExpectedTypes(type) {
186 let currentType;
187
188 if (type.name) {
189 currentType = type;
190 } else if (type.expression) {
191 currentType = type.expression;
192 }
193
194 return {
195 currentType,
196 expectedTypeName: currentType && preferType[currentType.name]
197 };
198 }
199
200 /**
201 * Gets the location of a JSDoc node in a file
202 * @param {Token} jsdocComment The comment that this node is parsed from
203 * @param {{range: number[]}} parsedJsdocNode A tag or other node which was parsed from this comment
204 * @returns {{start: SourceLocation, end: SourceLocation}} The 0-based source location for the tag
205 */
206 function getAbsoluteRange(jsdocComment, parsedJsdocNode) {
207 return {
208 start: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[0]),
209 end: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[1])
210 };
211 }
212
213 /**
214 * Validate type for a given JSDoc node
215 * @param {Object} jsdocNode JSDoc node
216 * @param {Object} type JSDoc tag
217 * @returns {void}
218 * @private
219 */
220 function validateType(jsdocNode, type) {
221 if (!type || !canTypeBeValidated(type.type)) {
222 return;
223 }
224
225 const typesToCheck = [];
226 let elements = [];
227
228 switch (type.type) {
229 case "TypeApplication": // {Array.<String>}
230 elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications;
231 typesToCheck.push(getCurrentExpectedTypes(type));
232 break;
233 case "RecordType": // {{20:String}}
234 elements = type.fields;
235 break;
236 case "UnionType": // {String|number|Test}
237 case "ArrayType": // {[String, number, Test]}
238 elements = type.elements;
239 break;
240 case "FieldType": // Array.<{count: number, votes: number}>
241 if (type.value) {
242 typesToCheck.push(getCurrentExpectedTypes(type.value));
243 }
244 break;
245 default:
246 typesToCheck.push(getCurrentExpectedTypes(type));
247 }
248
249 elements.forEach(validateType.bind(null, jsdocNode));
250
251 typesToCheck.forEach(typeToCheck => {
252 if (typeToCheck.expectedTypeName &&
253 typeToCheck.expectedTypeName !== typeToCheck.currentType.name) {
254 context.report({
255 node: jsdocNode,
256 messageId: "useType",
257 loc: getAbsoluteRange(jsdocNode, typeToCheck.currentType),
258 data: {
259 currentTypeName: typeToCheck.currentType.name,
260 expectedTypeName: typeToCheck.expectedTypeName
261 },
262 fix(fixer) {
263 return fixer.replaceTextRange(
264 typeToCheck.currentType.range.map(indexInComment => jsdocNode.range[0] + 2 + indexInComment),
265 typeToCheck.expectedTypeName
266 );
267 }
268 });
269 }
270 });
271 }
272
273 /**
274 * Validate the JSDoc node and output warnings if anything is wrong.
275 * @param {ASTNode} node The AST node to check.
276 * @returns {void}
277 * @private
278 */
279 function checkJSDoc(node) {
280 const jsdocNode = sourceCode.getJSDocComment(node),
281 functionData = fns.pop(),
282 paramTagsByName = Object.create(null),
283 paramTags = [];
284 let hasReturns = false,
285 returnsTag,
286 hasConstructor = false,
287 isInterface = false,
288 isOverride = false,
289 isAbstract = false;
290
291 // make sure only to validate JSDoc comments
292 if (jsdocNode) {
293 let jsdoc;
294
295 try {
296 jsdoc = doctrine.parse(jsdocNode.value, {
297 strict: true,
298 unwrap: true,
299 sloppy: true,
300 range: true
301 });
302 } catch (ex) {
303
304 if (/braces/iu.test(ex.message)) {
305 context.report({ node: jsdocNode, messageId: "missingBrace" });
306 } else {
307 context.report({ node: jsdocNode, messageId: "syntaxError" });
308 }
309
310 return;
311 }
312
313 jsdoc.tags.forEach(tag => {
314
315 switch (tag.title.toLowerCase()) {
316
317 case "param":
318 case "arg":
319 case "argument":
320 paramTags.push(tag);
321 break;
322
323 case "return":
324 case "returns":
325 hasReturns = true;
326 returnsTag = tag;
327 break;
328
329 case "constructor":
330 case "class":
331 hasConstructor = true;
332 break;
333
334 case "override":
335 case "inheritdoc":
336 isOverride = true;
337 break;
338
339 case "abstract":
340 case "virtual":
341 isAbstract = true;
342 break;
343
344 case "interface":
345 isInterface = true;
346 break;
347
348 // no default
349 }
350
351 // check tag preferences
352 if (Object.prototype.hasOwnProperty.call(prefer, tag.title) && tag.title !== prefer[tag.title]) {
353 const entireTagRange = getAbsoluteRange(jsdocNode, tag);
354
355 context.report({
356 node: jsdocNode,
357 messageId: "use",
358 loc: {
359 start: entireTagRange.start,
360 end: {
361 line: entireTagRange.start.line,
362 column: entireTagRange.start.column + `@${tag.title}`.length
363 }
364 },
365 data: { name: prefer[tag.title] },
366 fix(fixer) {
367 return fixer.replaceTextRange(
368 [
369 jsdocNode.range[0] + tag.range[0] + 3,
370 jsdocNode.range[0] + tag.range[0] + tag.title.length + 3
371 ],
372 prefer[tag.title]
373 );
374 }
375 });
376 }
377
378 // validate the types
379 if (checkPreferType && tag.type) {
380 validateType(jsdocNode, tag.type);
381 }
382 });
383
384 paramTags.forEach(param => {
385 if (requireParamType && !param.type) {
386 context.report({
387 node: jsdocNode,
388 messageId: "missingParamType",
389 loc: getAbsoluteRange(jsdocNode, param),
390 data: { name: param.name }
391 });
392 }
393 if (!param.description && requireParamDescription) {
394 context.report({
395 node: jsdocNode,
396 messageId: "missingParamDesc",
397 loc: getAbsoluteRange(jsdocNode, param),
398 data: { name: param.name }
399 });
400 }
401 if (paramTagsByName[param.name]) {
402 context.report({
403 node: jsdocNode,
404 messageId: "duplicateParam",
405 loc: getAbsoluteRange(jsdocNode, param),
406 data: { name: param.name }
407 });
408 } else if (param.name.indexOf(".") === -1) {
409 paramTagsByName[param.name] = param;
410 }
411 });
412
413 if (hasReturns) {
414 if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
415 context.report({
416 node: jsdocNode,
417 messageId: "unexpectedTag",
418 loc: getAbsoluteRange(jsdocNode, returnsTag),
419 data: {
420 title: returnsTag.title
421 }
422 });
423 } else {
424 if (requireReturnType && !returnsTag.type) {
425 context.report({ node: jsdocNode, messageId: "missingReturnType" });
426 }
427
428 if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
429 context.report({ node: jsdocNode, messageId: "missingReturnDesc" });
430 }
431 }
432 }
433
434 // check for functions missing @returns
435 if (!isOverride && !hasReturns && !hasConstructor && !isInterface &&
436 node.parent.kind !== "get" && node.parent.kind !== "constructor" &&
437 node.parent.kind !== "set" && !isTypeClass(node)) {
438 if (requireReturn || (functionData.returnPresent && !node.async)) {
439 context.report({
440 node: jsdocNode,
441 messageId: "missingReturn",
442 data: {
443 returns: prefer.returns || "returns"
444 }
445 });
446 }
447 }
448
449 // check the parameters
450 const jsdocParamNames = Object.keys(paramTagsByName);
451
452 if (node.params) {
453 node.params.forEach((param, paramsIndex) => {
454 const bindingParam = param.type === "AssignmentPattern"
455 ? param.left
456 : param;
457
458 // TODO(nzakas): Figure out logical things to do with destructured, default, rest params
459 if (bindingParam.type === "Identifier") {
460 const name = bindingParam.name;
461
462 if (jsdocParamNames[paramsIndex] && (name !== jsdocParamNames[paramsIndex])) {
463 context.report({
464 node: jsdocNode,
465 messageId: "expected",
466 loc: getAbsoluteRange(jsdocNode, paramTagsByName[jsdocParamNames[paramsIndex]]),
467 data: {
468 name,
469 jsdocName: jsdocParamNames[paramsIndex]
470 }
471 });
472 } else if (!paramTagsByName[name] && !isOverride) {
473 context.report({
474 node: jsdocNode,
475 messageId: "missingParam",
476 data: {
477 name
478 }
479 });
480 }
481 }
482 });
483 }
484
485 if (options.matchDescription) {
486 const regex = new RegExp(options.matchDescription, "u");
487
488 if (!regex.test(jsdoc.description)) {
489 context.report({ node: jsdocNode, messageId: "unsatisfiedDesc" });
490 }
491 }
492
493 }
494
495 }
496
497 //--------------------------------------------------------------------------
498 // Public
499 //--------------------------------------------------------------------------
500
501 return {
502 ArrowFunctionExpression: startFunction,
503 FunctionExpression: startFunction,
504 FunctionDeclaration: startFunction,
505 ClassExpression: startFunction,
506 ClassDeclaration: startFunction,
507 "ArrowFunctionExpression:exit": checkJSDoc,
508 "FunctionExpression:exit": checkJSDoc,
509 "FunctionDeclaration:exit": checkJSDoc,
510 "ClassExpression:exit": checkJSDoc,
511 "ClassDeclaration:exit": checkJSDoc,
512 ReturnStatement: addReturn
513 };
514
515 }
516 };