项目原始demo,不改动
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Deze repo is gearchiveerd. U kunt bestanden bekijken en het klonen, maar niet pushen of problemen/pull-requests openen.
 
 
 
 

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