requireParam.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.js"));
  7. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  8. /**
  9. * @typedef {[string, boolean, () => RootNamerReturn]} RootNamerReturn
  10. */
  11. /**
  12. * @param {string[]} desiredRoots
  13. * @param {number} currentIndex
  14. * @returns {RootNamerReturn}
  15. */
  16. const rootNamer = (desiredRoots, currentIndex) => {
  17. /** @type {string} */
  18. let name;
  19. let idx = currentIndex;
  20. const incremented = desiredRoots.length <= 1;
  21. if (incremented) {
  22. const base = desiredRoots[0];
  23. const suffix = idx++;
  24. name = `${base}${suffix}`;
  25. } else {
  26. name = /** @type {string} */desiredRoots.shift();
  27. }
  28. return [name, incremented, () => {
  29. return rootNamer(desiredRoots, idx);
  30. }];
  31. };
  32. /* eslint-disable complexity -- Temporary */
  33. var _default = exports.default = (0, _iterateJsdoc.default)(({
  34. jsdoc,
  35. utils,
  36. context
  37. }) => {
  38. /* eslint-enable complexity -- Temporary */
  39. if (utils.avoidDocs()) {
  40. return;
  41. }
  42. // Param type is specified by type in @type
  43. if (utils.hasTag('type')) {
  44. return;
  45. }
  46. const {
  47. autoIncrementBase = 0,
  48. checkRestProperty = false,
  49. checkDestructured = true,
  50. checkDestructuredRoots = true,
  51. checkTypesPattern = '/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/',
  52. enableFixer = true,
  53. enableRootFixer = true,
  54. enableRestElementFixer = true,
  55. unnamedRootBase = ['root'],
  56. useDefaultObjectProperties = false
  57. } = context.options[0] || {};
  58. const preferredTagName = /** @type {string} */utils.getPreferredTagName({
  59. tagName: 'param'
  60. });
  61. if (!preferredTagName) {
  62. return;
  63. }
  64. const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties);
  65. if (!functionParameterNames.length) {
  66. return;
  67. }
  68. const jsdocParameterNames =
  69. /**
  70. * @type {{
  71. * idx: import('../iterateJsdoc.js').Integer;
  72. * name: string;
  73. * type: string;
  74. * }[]}
  75. */
  76. utils.getJsdocTagsDeep(preferredTagName);
  77. const shallowJsdocParameterNames = jsdocParameterNames.filter(tag => {
  78. return !tag.name.includes('.');
  79. }).map((tag, idx) => {
  80. return {
  81. ...tag,
  82. idx
  83. };
  84. });
  85. const checkTypesRegex = utils.getRegexFromString(checkTypesPattern);
  86. /**
  87. * @type {{
  88. * functionParameterIdx: import('../iterateJsdoc.js').Integer,
  89. * functionParameterName: string,
  90. * inc: boolean|undefined,
  91. * remove?: true,
  92. * type?: string|undefined
  93. * }[]}
  94. */
  95. const missingTags = [];
  96. const flattenedRoots = utils.flattenRoots(functionParameterNames).names;
  97. /**
  98. * @type {{
  99. * [key: string]: import('../iterateJsdoc.js').Integer
  100. * }}
  101. */
  102. const paramIndex = {};
  103. /**
  104. * @param {string} cur
  105. * @returns {boolean}
  106. */
  107. const hasParamIndex = cur => {
  108. return utils.dropPathSegmentQuotes(String(cur)) in paramIndex;
  109. };
  110. /**
  111. *
  112. * @param {string|number|undefined} cur
  113. * @returns {import('../iterateJsdoc.js').Integer}
  114. */
  115. const getParamIndex = cur => {
  116. return paramIndex[utils.dropPathSegmentQuotes(String(cur))];
  117. };
  118. /**
  119. *
  120. * @param {string} cur
  121. * @param {import('../iterateJsdoc.js').Integer} idx
  122. * @returns {void}
  123. */
  124. const setParamIndex = (cur, idx) => {
  125. paramIndex[utils.dropPathSegmentQuotes(String(cur))] = idx;
  126. };
  127. for (const [idx, cur] of flattenedRoots.entries()) {
  128. setParamIndex(cur, idx);
  129. }
  130. /**
  131. *
  132. * @param {(import('@es-joy/jsdoccomment').JsdocTagWithInline & {
  133. * newAdd?: boolean
  134. * })[]} jsdocTags
  135. * @param {import('../iterateJsdoc.js').Integer} indexAtFunctionParams
  136. * @returns {import('../iterateJsdoc.js').Integer}
  137. */
  138. const findExpectedIndex = (jsdocTags, indexAtFunctionParams) => {
  139. const remainingRoots = functionParameterNames.slice(indexAtFunctionParams || 0);
  140. const foundIndex = jsdocTags.findIndex(({
  141. name,
  142. newAdd
  143. }) => {
  144. return !newAdd && remainingRoots.some(remainingRoot => {
  145. if (Array.isArray(remainingRoot)) {
  146. return (
  147. /**
  148. * @type {import('../jsdocUtils.js').FlattendRootInfo & {
  149. * annotationParamName?: string|undefined;
  150. * }}
  151. */
  152. remainingRoot[1].names.includes(name)
  153. );
  154. }
  155. if (typeof remainingRoot === 'object') {
  156. return name === remainingRoot.name;
  157. }
  158. return name === remainingRoot;
  159. });
  160. });
  161. const tags = foundIndex > -1 ? jsdocTags.slice(0, foundIndex) : jsdocTags.filter(({
  162. tag
  163. }) => {
  164. return tag === preferredTagName;
  165. });
  166. let tagLineCount = 0;
  167. for (const {
  168. source
  169. } of tags) {
  170. for (const {
  171. tokens: {
  172. end
  173. }
  174. } of source) {
  175. if (!end) {
  176. tagLineCount++;
  177. }
  178. }
  179. }
  180. return tagLineCount;
  181. };
  182. let [nextRootName, incremented, namer] = rootNamer([...unnamedRootBase], autoIncrementBase);
  183. for (const [functionParameterIdx, functionParameterName] of functionParameterNames.entries()) {
  184. let inc;
  185. if (Array.isArray(functionParameterName)) {
  186. const matchedJsdoc = shallowJsdocParameterNames[functionParameterIdx] || jsdocParameterNames[functionParameterIdx];
  187. /** @type {string} */
  188. let rootName;
  189. if (functionParameterName[0]) {
  190. rootName = functionParameterName[0];
  191. } else if (matchedJsdoc && matchedJsdoc.name) {
  192. rootName = matchedJsdoc.name;
  193. if (matchedJsdoc.type && matchedJsdoc.type.search(checkTypesRegex) === -1) {
  194. continue;
  195. }
  196. } else {
  197. rootName = nextRootName;
  198. inc = incremented;
  199. [nextRootName, incremented, namer] = namer();
  200. }
  201. const {
  202. hasRestElement,
  203. hasPropertyRest,
  204. rests,
  205. names
  206. } =
  207. /**
  208. * @type {import('../jsdocUtils.js').FlattendRootInfo & {
  209. * annotationParamName?: string | undefined;
  210. * }}
  211. */
  212. functionParameterName[1];
  213. const notCheckingNames = [];
  214. if (!enableRestElementFixer && hasRestElement) {
  215. continue;
  216. }
  217. if (!checkDestructuredRoots) {
  218. continue;
  219. }
  220. for (const [idx, paramName] of names.entries()) {
  221. // Add root if the root name is not in the docs (and is not already
  222. // in the tags to be fixed)
  223. if (!jsdocParameterNames.find(({
  224. name
  225. }) => {
  226. return name === rootName;
  227. }) && !missingTags.find(({
  228. functionParameterName: fpn
  229. }) => {
  230. return fpn === rootName;
  231. })) {
  232. const emptyParamIdx = jsdocParameterNames.findIndex(({
  233. name
  234. }) => {
  235. return !name;
  236. });
  237. if (emptyParamIdx > -1) {
  238. missingTags.push({
  239. functionParameterIdx: emptyParamIdx,
  240. functionParameterName: rootName,
  241. inc,
  242. remove: true
  243. });
  244. } else {
  245. missingTags.push({
  246. functionParameterIdx: hasParamIndex(rootName) ? getParamIndex(rootName) : getParamIndex(paramName),
  247. functionParameterName: rootName,
  248. inc
  249. });
  250. }
  251. }
  252. if (!checkDestructured) {
  253. continue;
  254. }
  255. if (!checkRestProperty && rests[idx]) {
  256. continue;
  257. }
  258. const fullParamName = `${rootName}.${paramName}`;
  259. const notCheckingName = jsdocParameterNames.find(({
  260. name,
  261. type: paramType
  262. }) => {
  263. return utils.comparePaths(name)(fullParamName) && paramType.search(checkTypesRegex) === -1 && paramType !== '';
  264. });
  265. if (notCheckingName !== undefined) {
  266. notCheckingNames.push(notCheckingName.name);
  267. }
  268. if (notCheckingNames.find(name => {
  269. return fullParamName.startsWith(name);
  270. })) {
  271. continue;
  272. }
  273. if (jsdocParameterNames && !jsdocParameterNames.find(({
  274. name
  275. }) => {
  276. return utils.comparePaths(name)(fullParamName);
  277. })) {
  278. missingTags.push({
  279. functionParameterIdx: getParamIndex(functionParameterName[0] ? fullParamName : paramName),
  280. functionParameterName: fullParamName,
  281. inc,
  282. type: hasRestElement && !hasPropertyRest ? '{...any}' : undefined
  283. });
  284. }
  285. }
  286. continue;
  287. }
  288. /** @type {string} */
  289. let funcParamName;
  290. let type;
  291. if (typeof functionParameterName === 'object') {
  292. if (!enableRestElementFixer && functionParameterName.restElement) {
  293. continue;
  294. }
  295. funcParamName = /** @type {string} */functionParameterName.name;
  296. type = '{...any}';
  297. } else {
  298. funcParamName = /** @type {string} */functionParameterName;
  299. }
  300. if (jsdocParameterNames && !jsdocParameterNames.find(({
  301. name
  302. }) => {
  303. return name === funcParamName;
  304. }) && funcParamName !== 'this') {
  305. missingTags.push({
  306. functionParameterIdx: getParamIndex(funcParamName),
  307. functionParameterName: funcParamName,
  308. inc,
  309. type
  310. });
  311. }
  312. }
  313. /**
  314. *
  315. * @param {{
  316. * functionParameterIdx: import('../iterateJsdoc.js').Integer,
  317. * functionParameterName: string,
  318. * remove?: true,
  319. * inc?: boolean,
  320. * type?: string
  321. * }} cfg
  322. */
  323. const fix = ({
  324. functionParameterIdx,
  325. functionParameterName,
  326. remove,
  327. inc,
  328. type
  329. }) => {
  330. if (inc && !enableRootFixer) {
  331. return;
  332. }
  333. /**
  334. *
  335. * @param {import('../iterateJsdoc.js').Integer} tagIndex
  336. * @param {import('../iterateJsdoc.js').Integer} sourceIndex
  337. * @param {import('../iterateJsdoc.js').Integer} spliceCount
  338. * @returns {void}
  339. */
  340. const createTokens = (tagIndex, sourceIndex, spliceCount) => {
  341. // console.log(sourceIndex, tagIndex, jsdoc.tags, jsdoc.source);
  342. const tokens = {
  343. number: sourceIndex + 1,
  344. source: '',
  345. tokens: {
  346. delimiter: '*',
  347. description: '',
  348. end: '',
  349. lineEnd: '',
  350. name: functionParameterName,
  351. newAdd: true,
  352. postDelimiter: ' ',
  353. postName: '',
  354. postTag: ' ',
  355. postType: type ? ' ' : '',
  356. start: jsdoc.source[sourceIndex].tokens.start,
  357. tag: `@${preferredTagName}`,
  358. type: type ?? ''
  359. }
  360. };
  361. /**
  362. * @type {(import('@es-joy/jsdoccomment').JsdocTagWithInline & {
  363. * newAdd?: true
  364. * })[]}
  365. */
  366. jsdoc.tags.splice(tagIndex, spliceCount, {
  367. description: '',
  368. inlineTags: [],
  369. name: functionParameterName,
  370. newAdd: true,
  371. optional: false,
  372. problems: [],
  373. source: [tokens],
  374. tag: preferredTagName,
  375. type: type ?? ''
  376. });
  377. const firstNumber = jsdoc.source[0].number;
  378. jsdoc.source.splice(sourceIndex, spliceCount, tokens);
  379. for (const [idx, src] of jsdoc.source.slice(sourceIndex).entries()) {
  380. src.number = firstNumber + sourceIndex + idx;
  381. }
  382. };
  383. const offset = jsdoc.source.findIndex(({
  384. tokens: {
  385. tag,
  386. end
  387. }
  388. }) => {
  389. return tag || end;
  390. });
  391. if (remove) {
  392. createTokens(functionParameterIdx, offset + functionParameterIdx, 1);
  393. } else {
  394. const expectedIdx = findExpectedIndex(jsdoc.tags, functionParameterIdx);
  395. createTokens(expectedIdx, offset + expectedIdx, 0);
  396. }
  397. };
  398. /**
  399. * @returns {void}
  400. */
  401. const fixer = () => {
  402. for (const missingTag of missingTags) {
  403. fix(missingTag);
  404. }
  405. };
  406. if (missingTags.length && jsdoc.source.length === 1) {
  407. utils.makeMultiline();
  408. }
  409. for (const {
  410. functionParameterName
  411. } of missingTags) {
  412. utils.reportJSDoc(`Missing JSDoc @${preferredTagName} "${functionParameterName}" declaration.`, null, enableFixer ? fixer : null);
  413. }
  414. }, {
  415. contextDefaults: true,
  416. meta: {
  417. docs: {
  418. description: 'Requires that all function parameters are documented.',
  419. url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param.md#repos-sticky-header'
  420. },
  421. fixable: 'code',
  422. schema: [{
  423. additionalProperties: false,
  424. properties: {
  425. autoIncrementBase: {
  426. default: 0,
  427. type: 'integer'
  428. },
  429. checkConstructors: {
  430. default: true,
  431. type: 'boolean'
  432. },
  433. checkDestructured: {
  434. default: true,
  435. type: 'boolean'
  436. },
  437. checkDestructuredRoots: {
  438. default: true,
  439. type: 'boolean'
  440. },
  441. checkGetters: {
  442. default: false,
  443. type: 'boolean'
  444. },
  445. checkRestProperty: {
  446. default: false,
  447. type: 'boolean'
  448. },
  449. checkSetters: {
  450. default: false,
  451. type: 'boolean'
  452. },
  453. checkTypesPattern: {
  454. type: 'string'
  455. },
  456. contexts: {
  457. items: {
  458. anyOf: [{
  459. type: 'string'
  460. }, {
  461. additionalProperties: false,
  462. properties: {
  463. comment: {
  464. type: 'string'
  465. },
  466. context: {
  467. type: 'string'
  468. }
  469. },
  470. type: 'object'
  471. }]
  472. },
  473. type: 'array'
  474. },
  475. enableFixer: {
  476. type: 'boolean'
  477. },
  478. enableRestElementFixer: {
  479. type: 'boolean'
  480. },
  481. enableRootFixer: {
  482. type: 'boolean'
  483. },
  484. exemptedBy: {
  485. items: {
  486. type: 'string'
  487. },
  488. type: 'array'
  489. },
  490. unnamedRootBase: {
  491. items: {
  492. type: 'string'
  493. },
  494. type: 'array'
  495. },
  496. useDefaultObjectProperties: {
  497. type: 'boolean'
  498. }
  499. },
  500. type: 'object'
  501. }],
  502. type: 'suggestion'
  503. },
  504. // We cannot cache comment nodes as the contexts may recur with the
  505. // same comment node but a different JS node, and we may need the different
  506. // JS node to ensure we iterate its context
  507. noTracking: true
  508. });
  509. module.exports = exports.default;
  510. //# sourceMappingURL=requireParam.js.map