valid-jsdoc.js 20 KB

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