Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

490 řádky
19 KiB

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