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