requireJsdoc.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _exportParser = _interopRequireDefault(require("../exportParser.js"));
  7. var _iterateJsdoc = require("../iterateJsdoc.js");
  8. var _jsdocUtils = _interopRequireDefault(require("../jsdocUtils.js"));
  9. var _jsdoccomment = require("@es-joy/jsdoccomment");
  10. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  11. /**
  12. * @typedef {{
  13. * ancestorsOnly: boolean,
  14. * esm: boolean,
  15. * initModuleExports: boolean,
  16. * initWindow: boolean
  17. * }} RequireJsdocOpts
  18. */
  19. /** @type {import('json-schema').JSONSchema4} */
  20. const OPTIONS_SCHEMA = {
  21. additionalProperties: false,
  22. properties: {
  23. checkConstructors: {
  24. default: true,
  25. type: 'boolean'
  26. },
  27. checkGetters: {
  28. anyOf: [{
  29. type: 'boolean'
  30. }, {
  31. enum: ['no-setter'],
  32. type: 'string'
  33. }],
  34. default: true
  35. },
  36. checkSetters: {
  37. anyOf: [{
  38. type: 'boolean'
  39. }, {
  40. enum: ['no-getter'],
  41. type: 'string'
  42. }],
  43. default: true
  44. },
  45. contexts: {
  46. items: {
  47. anyOf: [{
  48. type: 'string'
  49. }, {
  50. additionalProperties: false,
  51. properties: {
  52. context: {
  53. type: 'string'
  54. },
  55. inlineCommentBlock: {
  56. type: 'boolean'
  57. },
  58. minLineCount: {
  59. type: 'integer'
  60. }
  61. },
  62. type: 'object'
  63. }]
  64. },
  65. type: 'array'
  66. },
  67. enableFixer: {
  68. default: true,
  69. type: 'boolean'
  70. },
  71. exemptEmptyConstructors: {
  72. default: false,
  73. type: 'boolean'
  74. },
  75. exemptEmptyFunctions: {
  76. default: false,
  77. type: 'boolean'
  78. },
  79. fixerMessage: {
  80. default: '',
  81. type: 'string'
  82. },
  83. minLineCount: {
  84. type: 'integer'
  85. },
  86. publicOnly: {
  87. oneOf: [{
  88. default: false,
  89. type: 'boolean'
  90. }, {
  91. additionalProperties: false,
  92. default: {},
  93. properties: {
  94. ancestorsOnly: {
  95. type: 'boolean'
  96. },
  97. cjs: {
  98. type: 'boolean'
  99. },
  100. esm: {
  101. type: 'boolean'
  102. },
  103. window: {
  104. type: 'boolean'
  105. }
  106. },
  107. type: 'object'
  108. }]
  109. },
  110. require: {
  111. additionalProperties: false,
  112. default: {},
  113. properties: {
  114. ArrowFunctionExpression: {
  115. default: false,
  116. type: 'boolean'
  117. },
  118. ClassDeclaration: {
  119. default: false,
  120. type: 'boolean'
  121. },
  122. ClassExpression: {
  123. default: false,
  124. type: 'boolean'
  125. },
  126. FunctionDeclaration: {
  127. default: true,
  128. type: 'boolean'
  129. },
  130. FunctionExpression: {
  131. default: false,
  132. type: 'boolean'
  133. },
  134. MethodDefinition: {
  135. default: false,
  136. type: 'boolean'
  137. }
  138. },
  139. type: 'object'
  140. }
  141. },
  142. type: 'object'
  143. };
  144. /**
  145. * @param {import('eslint').Rule.RuleContext} context
  146. * @param {import('json-schema').JSONSchema4Object} baseObject
  147. * @param {string} option
  148. * @param {string} key
  149. * @returns {boolean|undefined}
  150. */
  151. const getOption = (context, baseObject, option, key) => {
  152. if (context.options[0] && option in context.options[0] && (
  153. // Todo: boolean shouldn't be returning property, but
  154. // tests currently require
  155. typeof context.options[0][option] === 'boolean' || key in context.options[0][option])) {
  156. return context.options[0][option][key];
  157. }
  158. return /** @type {{[key: string]: {default?: boolean|undefined}}} */baseObject.properties[key].default;
  159. };
  160. /**
  161. * @param {import('eslint').Rule.RuleContext} context
  162. * @param {import('../iterateJsdoc.js').Settings} settings
  163. * @returns {{
  164. * contexts: (string|{
  165. * context: string,
  166. * inlineCommentBlock: boolean,
  167. * minLineCount: import('../iterateJsdoc.js').Integer
  168. * })[],
  169. * enableFixer: boolean,
  170. * exemptEmptyConstructors: boolean,
  171. * exemptEmptyFunctions: boolean,
  172. * fixerMessage: string,
  173. * minLineCount: undefined|import('../iterateJsdoc.js').Integer,
  174. * publicOnly: boolean|{[key: string]: boolean|undefined}
  175. * require: {[key: string]: boolean|undefined}
  176. * }}
  177. */
  178. const getOptions = (context, settings) => {
  179. const {
  180. publicOnly,
  181. contexts = settings.contexts || [],
  182. exemptEmptyConstructors = true,
  183. exemptEmptyFunctions = false,
  184. enableFixer = true,
  185. fixerMessage = '',
  186. minLineCount = undefined
  187. } = context.options[0] || {};
  188. return {
  189. contexts,
  190. enableFixer,
  191. exemptEmptyConstructors,
  192. exemptEmptyFunctions,
  193. fixerMessage,
  194. minLineCount,
  195. publicOnly: (baseObj => {
  196. if (!publicOnly) {
  197. return false;
  198. }
  199. /** @type {{[key: string]: boolean|undefined}} */
  200. const properties = {};
  201. for (const prop of Object.keys( /** @type {import('json-schema').JSONSchema4Object} */
  202. /** @type {import('json-schema').JSONSchema4Object} */baseObj.properties)) {
  203. const opt = getOption(context, /** @type {import('json-schema').JSONSchema4Object} */baseObj, 'publicOnly', prop);
  204. properties[prop] = opt;
  205. }
  206. return properties;
  207. })( /** @type {import('json-schema').JSONSchema4Object} */
  208. ( /** @type {import('json-schema').JSONSchema4Object} */
  209. ( /** @type {import('json-schema').JSONSchema4Object} */
  210. OPTIONS_SCHEMA.properties.publicOnly).oneOf)[1]),
  211. require: (baseObj => {
  212. /** @type {{[key: string]: boolean|undefined}} */
  213. const properties = {};
  214. for (const prop of Object.keys( /** @type {import('json-schema').JSONSchema4Object} */
  215. /** @type {import('json-schema').JSONSchema4Object} */baseObj.properties)) {
  216. const opt = getOption(context, /** @type {import('json-schema').JSONSchema4Object} */
  217. baseObj, 'require', prop);
  218. properties[prop] = opt;
  219. }
  220. return properties;
  221. })( /** @type {import('json-schema').JSONSchema4Object} */
  222. OPTIONS_SCHEMA.properties.require)
  223. };
  224. };
  225. /** @type {import('eslint').Rule.RuleModule} */
  226. var _default = exports.default = {
  227. create(context) {
  228. // istanbul ignore next -- Fallback to deprecated method
  229. const {
  230. sourceCode = context.getSourceCode()
  231. } = context;
  232. const settings = (0, _iterateJsdoc.getSettings)(context);
  233. if (!settings) {
  234. return {};
  235. }
  236. const opts = getOptions(context, settings);
  237. const {
  238. require: requireOption,
  239. contexts,
  240. exemptEmptyFunctions,
  241. exemptEmptyConstructors,
  242. enableFixer,
  243. fixerMessage,
  244. minLineCount
  245. } = opts;
  246. const publicOnly =
  247. /**
  248. * @type {{
  249. * [key: string]: boolean | undefined;
  250. * }}
  251. */
  252. opts.publicOnly;
  253. /**
  254. * @type {import('../iterateJsdoc.js').CheckJsdoc}
  255. */
  256. const checkJsDoc = (info, _handler, node) => {
  257. if (
  258. // Optimize
  259. minLineCount !== undefined || contexts.some(ctxt => {
  260. if (typeof ctxt === 'string') {
  261. return false;
  262. }
  263. const {
  264. minLineCount: count
  265. } = ctxt;
  266. return count !== undefined;
  267. })) {
  268. /**
  269. * @param {undefined|import('../iterateJsdoc.js').Integer} count
  270. */
  271. const underMinLine = count => {
  272. var _sourceCode$getText$m;
  273. return count !== undefined && count > (((_sourceCode$getText$m = sourceCode.getText(node).match(/\n/gu)) === null || _sourceCode$getText$m === void 0 ? void 0 : _sourceCode$getText$m.length) ?? 0) + 1;
  274. };
  275. if (underMinLine(minLineCount)) {
  276. return;
  277. }
  278. const {
  279. minLineCount: contextMinLineCount
  280. } =
  281. /**
  282. * @type {{
  283. * context: string;
  284. * inlineCommentBlock: boolean;
  285. * minLineCount: number;
  286. * }}
  287. */
  288. contexts.find(ctxt => {
  289. if (typeof ctxt === 'string') {
  290. return false;
  291. }
  292. const {
  293. context: ctx
  294. } = ctxt;
  295. return ctx === (info.selector || node.type);
  296. }) || {};
  297. if (underMinLine(contextMinLineCount)) {
  298. return;
  299. }
  300. }
  301. const jsDocNode = (0, _jsdoccomment.getJSDocComment)(sourceCode, node, settings);
  302. if (jsDocNode) {
  303. return;
  304. }
  305. // For those who have options configured against ANY constructors (or
  306. // setters or getters) being reported
  307. if (_jsdocUtils.default.exemptSpeciaMethods({
  308. description: '',
  309. inlineTags: [],
  310. problems: [],
  311. source: [],
  312. tags: []
  313. }, node, context, [OPTIONS_SCHEMA])) {
  314. return;
  315. }
  316. if (
  317. // Avoid reporting param-less, return-less functions (when
  318. // `exemptEmptyFunctions` option is set)
  319. exemptEmptyFunctions && info.isFunctionContext ||
  320. // Avoid reporting param-less, return-less constructor methods (when
  321. // `exemptEmptyConstructors` option is set)
  322. exemptEmptyConstructors && _jsdocUtils.default.isConstructor(node)) {
  323. const functionParameterNames = _jsdocUtils.default.getFunctionParameterNames(node);
  324. if (!functionParameterNames.length && !_jsdocUtils.default.hasReturnValue(node)) {
  325. return;
  326. }
  327. }
  328. const fix = /** @type {import('eslint').Rule.ReportFixer} */fixer => {
  329. // Default to one line break if the `minLines`/`maxLines` settings allow
  330. const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines;
  331. /** @type {import('eslint').Rule.Node|import('@typescript-eslint/types').TSESTree.Decorator} */
  332. let baseNode = (0, _jsdoccomment.getReducedASTNode)(node, sourceCode);
  333. const decorator = (0, _jsdoccomment.getDecorator)(baseNode);
  334. if (decorator) {
  335. baseNode = decorator;
  336. }
  337. const indent = _jsdocUtils.default.getIndent({
  338. text: sourceCode.getText( /** @type {import('eslint').Rule.Node} */baseNode, /** @type {import('eslint').AST.SourceLocation} */
  339. ( /** @type {import('eslint').Rule.Node} */baseNode.loc).start.column)
  340. });
  341. const {
  342. inlineCommentBlock
  343. } =
  344. /**
  345. * @type {{
  346. * context: string,
  347. * inlineCommentBlock: boolean,
  348. * minLineCount: import('../iterateJsdoc.js').Integer
  349. * }}
  350. */
  351. contexts.find(contxt => {
  352. if (typeof contxt === 'string') {
  353. return false;
  354. }
  355. const {
  356. context: ctxt
  357. } = contxt;
  358. return ctxt === node.type;
  359. }) || {};
  360. const insertion = (inlineCommentBlock ? `/** ${fixerMessage}` : `/**\n${indent}*${fixerMessage}\n${indent}`) + `*/${'\n'.repeat(lines)}${indent.slice(0, -1)}`;
  361. return fixer.insertTextBefore( /** @type {import('eslint').Rule.Node} */
  362. baseNode, insertion);
  363. };
  364. const report = () => {
  365. const {
  366. start
  367. } = /** @type {import('eslint').AST.SourceLocation} */node.loc;
  368. const loc = {
  369. end: {
  370. column: 0,
  371. line: start.line + 1
  372. },
  373. start
  374. };
  375. context.report({
  376. fix: enableFixer ? fix : null,
  377. loc,
  378. messageId: 'missingJsDoc',
  379. node
  380. });
  381. };
  382. if (publicOnly) {
  383. /** @type {RequireJsdocOpts} */
  384. const opt = {
  385. ancestorsOnly: Boolean((publicOnly === null || publicOnly === void 0 ? void 0 : publicOnly.ancestorsOnly) ?? false),
  386. esm: Boolean((publicOnly === null || publicOnly === void 0 ? void 0 : publicOnly.esm) ?? true),
  387. initModuleExports: Boolean((publicOnly === null || publicOnly === void 0 ? void 0 : publicOnly.cjs) ?? true),
  388. initWindow: Boolean((publicOnly === null || publicOnly === void 0 ? void 0 : publicOnly.window) ?? false)
  389. };
  390. const exported = _exportParser.default.isUncommentedExport(node, sourceCode, opt, settings);
  391. if (exported) {
  392. report();
  393. }
  394. } else {
  395. report();
  396. }
  397. };
  398. /**
  399. * @param {string} prop
  400. * @returns {boolean}
  401. */
  402. const hasOption = prop => {
  403. return requireOption[prop] || contexts.some(ctxt => {
  404. return typeof ctxt === 'object' ? ctxt.context === prop : ctxt === prop;
  405. });
  406. };
  407. return {
  408. ..._jsdocUtils.default.getContextObject(_jsdocUtils.default.enforcedContexts(context, [], settings), checkJsDoc),
  409. ArrowFunctionExpression(node) {
  410. if (!hasOption('ArrowFunctionExpression')) {
  411. return;
  412. }
  413. if (['VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration'].includes(node.parent.type) || ['Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition'].includes(node.parent.type) && node ===
  414. /**
  415. * @type {import('@typescript-eslint/types').TSESTree.Property|
  416. * import('@typescript-eslint/types').TSESTree.PropertyDefinition
  417. * }
  418. */
  419. node.parent.value) {
  420. checkJsDoc({
  421. isFunctionContext: true
  422. }, null, node);
  423. }
  424. },
  425. ClassDeclaration(node) {
  426. if (!hasOption('ClassDeclaration')) {
  427. return;
  428. }
  429. checkJsDoc({
  430. isFunctionContext: false
  431. }, null, node);
  432. },
  433. ClassExpression(node) {
  434. if (!hasOption('ClassExpression')) {
  435. return;
  436. }
  437. checkJsDoc({
  438. isFunctionContext: false
  439. }, null, node);
  440. },
  441. FunctionDeclaration(node) {
  442. if (!hasOption('FunctionDeclaration')) {
  443. return;
  444. }
  445. checkJsDoc({
  446. isFunctionContext: true
  447. }, null, node);
  448. },
  449. FunctionExpression(node) {
  450. if (!hasOption('FunctionExpression')) {
  451. return;
  452. }
  453. if (['VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration'].includes(node.parent.type) || ['Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition'].includes(node.parent.type) && node ===
  454. /**
  455. * @type {import('@typescript-eslint/types').TSESTree.Property|
  456. * import('@typescript-eslint/types').TSESTree.PropertyDefinition
  457. * }
  458. */
  459. node.parent.value) {
  460. checkJsDoc({
  461. isFunctionContext: true
  462. }, null, node);
  463. }
  464. },
  465. MethodDefinition(node) {
  466. if (!hasOption('MethodDefinition')) {
  467. return;
  468. }
  469. checkJsDoc({
  470. isFunctionContext: true,
  471. selector: 'MethodDefinition'
  472. }, null, /** @type {import('eslint').Rule.Node} */node.value);
  473. }
  474. };
  475. },
  476. meta: {
  477. docs: {
  478. category: 'Stylistic Issues',
  479. description: 'Require JSDoc comments',
  480. recommended: true,
  481. url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-jsdoc.md#repos-sticky-header'
  482. },
  483. fixable: 'code',
  484. messages: {
  485. missingJsDoc: 'Missing JSDoc comment.'
  486. },
  487. schema: [OPTIONS_SCHEMA],
  488. type: 'suggestion'
  489. }
  490. };
  491. module.exports = exports.default;
  492. //# sourceMappingURL=requireJsdoc.js.map