index.cjs.cjs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  1. 'use strict';
  2. var jsdocTypePrattParser = require('jsdoc-type-pratt-parser');
  3. var esquery = require('esquery');
  4. var commentParser = require('comment-parser');
  5. /**
  6. * Removes initial and ending brackets from `rawType`
  7. * @param {JsdocTypeLine[]|JsdocTag} container
  8. * @param {boolean} [isArr]
  9. * @returns {void}
  10. */
  11. const stripEncapsulatingBrackets = (container, isArr) => {
  12. if (isArr) {
  13. const firstItem = /** @type {JsdocTypeLine[]} */container[0];
  14. firstItem.rawType = firstItem.rawType.replace(/^\{/u, '');
  15. const lastItem = /** @type {JsdocTypeLine} */
  16. /** @type {JsdocTypeLine[]} */container.at(-1);
  17. lastItem.rawType = lastItem.rawType.replace(/\}$/u, '');
  18. return;
  19. }
  20. /** @type {JsdocTag} */
  21. container.rawType = /** @type {JsdocTag} */container.rawType.replace(/^\{/u, '').replace(/\}$/u, '');
  22. };
  23. /**
  24. * @typedef {{
  25. * delimiter: string,
  26. * postDelimiter: string,
  27. * rawType: string,
  28. * initial: string,
  29. * type: "JsdocTypeLine"
  30. * }} JsdocTypeLine
  31. */
  32. /**
  33. * @typedef {{
  34. * delimiter: string,
  35. * description: string,
  36. * postDelimiter: string,
  37. * initial: string,
  38. * type: "JsdocDescriptionLine"
  39. * }} JsdocDescriptionLine
  40. */
  41. /**
  42. * @typedef {{
  43. * format: 'pipe' | 'plain' | 'prefix' | 'space',
  44. * namepathOrURL: string,
  45. * tag: string,
  46. * text: string,
  47. * }} JsdocInlineTagNoType
  48. */
  49. /**
  50. * @typedef {JsdocInlineTagNoType & {
  51. * type: "JsdocInlineTag"
  52. * }} JsdocInlineTag
  53. */
  54. /**
  55. * @typedef {{
  56. * delimiter: string,
  57. * description: string,
  58. * descriptionLines: JsdocDescriptionLine[],
  59. * initial: string,
  60. * inlineTags: JsdocInlineTag[]
  61. * name: string,
  62. * postDelimiter: string,
  63. * postName: string,
  64. * postTag: string,
  65. * postType: string,
  66. * rawType: string,
  67. * parsedType: import('jsdoc-type-pratt-parser').RootResult|null
  68. * tag: string,
  69. * type: "JsdocTag",
  70. * typeLines: JsdocTypeLine[],
  71. * }} JsdocTag
  72. */
  73. /**
  74. * @typedef {number} Integer
  75. */
  76. /**
  77. * @typedef {{
  78. * delimiter: string,
  79. * description: string,
  80. * descriptionEndLine?: Integer,
  81. * descriptionLines: JsdocDescriptionLine[],
  82. * descriptionStartLine?: Integer,
  83. * hasPreterminalDescription: 0|1,
  84. * hasPreterminalTagDescription?: 1,
  85. * initial: string,
  86. * inlineTags: JsdocInlineTag[]
  87. * lastDescriptionLine?: Integer,
  88. * endLine: Integer,
  89. * lineEnd: string,
  90. * postDelimiter: string,
  91. * tags: JsdocTag[],
  92. * terminal: string,
  93. * type: "JsdocBlock",
  94. * }} JsdocBlock
  95. */
  96. /**
  97. * @param {object} cfg
  98. * @param {string} cfg.text
  99. * @param {string} cfg.tag
  100. * @param {'pipe' | 'plain' | 'prefix' | 'space'} cfg.format
  101. * @param {string} cfg.namepathOrURL
  102. * @returns {JsdocInlineTag}
  103. */
  104. const inlineTagToAST = ({
  105. text,
  106. tag,
  107. format,
  108. namepathOrURL
  109. }) => ({
  110. text,
  111. tag,
  112. format,
  113. namepathOrURL,
  114. type: 'JsdocInlineTag'
  115. });
  116. /**
  117. * Converts comment parser AST to ESTree format.
  118. * @param {import('./index.js').JsdocBlockWithInline} jsdoc
  119. * @param {import('jsdoc-type-pratt-parser').ParseMode} mode
  120. * @param {object} opts
  121. * @param {boolean} [opts.throwOnTypeParsingErrors]
  122. * @returns {JsdocBlock}
  123. */
  124. const commentParserToESTree = (jsdoc, mode, {
  125. throwOnTypeParsingErrors = false
  126. } = {}) => {
  127. /**
  128. * Strips brackets from a tag's `rawType` values and adds `parsedType`
  129. * @param {JsdocTag} lastTag
  130. * @returns {void}
  131. */
  132. const cleanUpLastTag = lastTag => {
  133. // Strip out `}` that encapsulates and is not part of
  134. // the type
  135. stripEncapsulatingBrackets(lastTag);
  136. if (lastTag.typeLines.length) {
  137. stripEncapsulatingBrackets(lastTag.typeLines, true);
  138. }
  139. // With even a multiline type now in full, add parsing
  140. let parsedType = null;
  141. try {
  142. parsedType = jsdocTypePrattParser.parse(lastTag.rawType, mode);
  143. } catch (err) {
  144. // Ignore
  145. if (lastTag.rawType && throwOnTypeParsingErrors) {
  146. /** @type {Error} */err.message = `Tag @${lastTag.tag} with raw type ` + `\`${lastTag.rawType}\` had parsing error: ${
  147. /** @type {Error} */err.message}`;
  148. throw err;
  149. }
  150. }
  151. lastTag.parsedType = parsedType;
  152. };
  153. const {
  154. source,
  155. inlineTags: blockInlineTags
  156. } = jsdoc;
  157. const {
  158. tokens: {
  159. delimiter: delimiterRoot,
  160. lineEnd: lineEndRoot,
  161. postDelimiter: postDelimiterRoot,
  162. start: startRoot,
  163. end: endRoot
  164. }
  165. } = source[0];
  166. const endLine = source.length - 1;
  167. /** @type {JsdocBlock} */
  168. const ast = {
  169. delimiter: delimiterRoot,
  170. description: '',
  171. descriptionLines: [],
  172. inlineTags: blockInlineTags.map(t => inlineTagToAST(t)),
  173. initial: startRoot,
  174. tags: [],
  175. // `terminal` will be overwritten if there are other entries
  176. terminal: endRoot,
  177. hasPreterminalDescription: 0,
  178. endLine,
  179. postDelimiter: postDelimiterRoot,
  180. lineEnd: lineEndRoot,
  181. type: 'JsdocBlock'
  182. };
  183. /**
  184. * @type {JsdocTag[]}
  185. */
  186. const tags = [];
  187. /** @type {Integer|undefined} */
  188. let lastDescriptionLine;
  189. /** @type {JsdocTag|null} */
  190. let lastTag = null;
  191. let descLineStateOpen = true;
  192. source.forEach((info, idx) => {
  193. const {
  194. tokens
  195. } = info;
  196. const {
  197. delimiter,
  198. description,
  199. postDelimiter,
  200. start: initial,
  201. tag,
  202. end,
  203. type: rawType
  204. } = tokens;
  205. if (!tag && description && descLineStateOpen) {
  206. if (ast.descriptionStartLine === undefined) {
  207. ast.descriptionStartLine = idx;
  208. }
  209. ast.descriptionEndLine = idx;
  210. }
  211. if (tag || end) {
  212. descLineStateOpen = false;
  213. if (lastDescriptionLine === undefined) {
  214. lastDescriptionLine = idx;
  215. }
  216. // Clean-up with last tag before end or new tag
  217. if (lastTag) {
  218. cleanUpLastTag(lastTag);
  219. }
  220. // Stop the iteration when we reach the end
  221. // but only when there is no tag earlier in the line
  222. // to still process
  223. if (end && !tag) {
  224. ast.terminal = end;
  225. if (description) {
  226. if (lastTag) {
  227. ast.hasPreterminalTagDescription = 1;
  228. } else {
  229. ast.hasPreterminalDescription = 1;
  230. }
  231. const holder = lastTag || ast;
  232. holder.description += (holder.description ? '\n' : '') + description;
  233. holder.descriptionLines.push({
  234. delimiter,
  235. description,
  236. postDelimiter,
  237. initial,
  238. type: 'JsdocDescriptionLine'
  239. });
  240. }
  241. return;
  242. }
  243. const {
  244. // eslint-disable-next-line no-unused-vars -- Discarding
  245. end: ed,
  246. delimiter: de,
  247. postDelimiter: pd,
  248. start: init,
  249. ...tkns
  250. } = tokens;
  251. if (!tokens.name) {
  252. let i = 1;
  253. while (source[idx + i]) {
  254. const {
  255. tokens: {
  256. name,
  257. postName,
  258. postType,
  259. tag: tg
  260. }
  261. } = source[idx + i];
  262. if (tg) {
  263. break;
  264. }
  265. if (name) {
  266. tkns.postType = postType;
  267. tkns.name = name;
  268. tkns.postName = postName;
  269. break;
  270. }
  271. i++;
  272. }
  273. }
  274. /**
  275. * @type {JsdocInlineTag[]}
  276. */
  277. let tagInlineTags = [];
  278. if (tag) {
  279. // Assuming the tags from `source` are in the same order as `jsdoc.tags`
  280. // we can use the `tags` length as index into the parser result tags.
  281. tagInlineTags =
  282. /**
  283. * @type {import('comment-parser').Spec & {
  284. * inlineTags: JsdocInlineTagNoType[]
  285. * }}
  286. */
  287. jsdoc.tags[tags.length].inlineTags.map(t => inlineTagToAST(t));
  288. }
  289. /** @type {JsdocTag} */
  290. const tagObj = {
  291. ...tkns,
  292. initial: endLine ? init : '',
  293. postDelimiter: lastDescriptionLine ? pd : '',
  294. delimiter: lastDescriptionLine ? de : '',
  295. descriptionLines: [],
  296. inlineTags: tagInlineTags,
  297. parsedType: null,
  298. rawType: '',
  299. type: 'JsdocTag',
  300. typeLines: []
  301. };
  302. tagObj.tag = tagObj.tag.replace(/^@/u, '');
  303. lastTag = tagObj;
  304. tags.push(tagObj);
  305. }
  306. if (rawType) {
  307. // Will strip rawType brackets after this tag
  308. /** @type {JsdocTag} */
  309. lastTag.typeLines.push( /** @type {JsdocTag} */lastTag.typeLines.length ? {
  310. delimiter,
  311. postDelimiter,
  312. rawType,
  313. initial,
  314. type: 'JsdocTypeLine'
  315. } : {
  316. delimiter: '',
  317. postDelimiter: '',
  318. rawType,
  319. initial: '',
  320. type: 'JsdocTypeLine'
  321. });
  322. /** @type {JsdocTag} */
  323. lastTag.rawType += /** @type {JsdocTag} */lastTag.rawType ? '\n' + rawType : rawType;
  324. }
  325. if (description) {
  326. const holder = lastTag || ast;
  327. holder.descriptionLines.push(holder.descriptionLines.length ? {
  328. delimiter,
  329. description,
  330. postDelimiter,
  331. initial,
  332. type: 'JsdocDescriptionLine'
  333. } : lastTag ? {
  334. delimiter: '',
  335. description,
  336. postDelimiter: '',
  337. initial: '',
  338. type: 'JsdocDescriptionLine'
  339. } : {
  340. delimiter,
  341. description,
  342. postDelimiter,
  343. initial,
  344. type: 'JsdocDescriptionLine'
  345. });
  346. if (!tag) {
  347. holder.description += !holder.description && !lastTag ? description : '\n' + description;
  348. }
  349. }
  350. // Clean-up where last line itself has tag content
  351. if (end && tag) {
  352. ast.terminal = end;
  353. ast.hasPreterminalTagDescription = 1;
  354. cleanUpLastTag( /** @type {JsdocTag} */lastTag);
  355. }
  356. });
  357. ast.lastDescriptionLine = lastDescriptionLine;
  358. ast.tags = tags;
  359. return ast;
  360. };
  361. const jsdocVisitorKeys = {
  362. JsdocBlock: ['descriptionLines', 'tags', 'inlineTags'],
  363. JsdocDescriptionLine: [],
  364. JsdocTypeLine: [],
  365. JsdocTag: ['parsedType', 'typeLines', 'descriptionLines', 'inlineTags'],
  366. JsdocInlineTag: []
  367. };
  368. /**
  369. * @typedef {import('./index.js').CommentHandler} CommentHandler
  370. */
  371. /**
  372. * @param {{[name: string]: any}} settings
  373. * @returns {CommentHandler}
  374. */
  375. const commentHandler = settings => {
  376. /**
  377. * @type {CommentHandler}
  378. */
  379. return (commentSelector, jsdoc) => {
  380. const {
  381. mode
  382. } = settings;
  383. const selector = esquery.parse(commentSelector);
  384. const ast = commentParserToESTree(jsdoc, mode);
  385. const _ast = /** @type {unknown} */ast;
  386. return esquery.matches( /** @type {import('estree').Node} */
  387. _ast, selector, undefined, {
  388. visitorKeys: {
  389. ...jsdocTypePrattParser.visitorKeys,
  390. ...jsdocVisitorKeys
  391. }
  392. });
  393. };
  394. };
  395. /**
  396. * @param {string} str
  397. * @returns {string}
  398. */
  399. const toCamelCase = str => {
  400. return str.toLowerCase().replaceAll(/^[a-z]/gu, init => {
  401. return init.toUpperCase();
  402. }).replaceAll(/_(?<wordInit>[a-z])/gu, (_, n1, o, s, {
  403. wordInit
  404. }) => {
  405. return wordInit.toUpperCase();
  406. });
  407. };
  408. /**
  409. * @param {RegExpMatchArray & {
  410. * indices: {
  411. * groups: {
  412. * [key: string]: [number, number]
  413. * }
  414. * }
  415. * groups: {[key: string]: string}
  416. * }} match An inline tag regexp match.
  417. * @returns {'pipe' | 'plain' | 'prefix' | 'space'}
  418. */
  419. function determineFormat(match) {
  420. const {
  421. separator,
  422. text
  423. } = match.groups;
  424. const [, textEnd] = match.indices.groups.text;
  425. const [tagStart] = match.indices.groups.tag;
  426. if (!text) {
  427. return 'plain';
  428. } else if (separator === '|') {
  429. return 'pipe';
  430. } else if (textEnd < tagStart) {
  431. return 'prefix';
  432. }
  433. return 'space';
  434. }
  435. /**
  436. * @typedef {import('./index.js').InlineTag} InlineTag
  437. */
  438. /**
  439. * Extracts inline tags from a description.
  440. * @param {string} description
  441. * @returns {InlineTag[]} Array of inline tags from the description.
  442. */
  443. function parseDescription(description) {
  444. /** @type {InlineTag[]} */
  445. const result = [];
  446. // This could have been expressed in a single pattern,
  447. // but having two avoids a potentially exponential time regex.
  448. const prefixedTextPattern = new RegExp(/(?:\[(?<text>[^\]]+)\])\{@(?<tag>[^}\s]+)\s?(?<namepathOrURL>[^}\s|]*)\}/gu, 'gud');
  449. // The pattern used to match for text after tag uses a negative lookbehind
  450. // on the ']' char to avoid matching the prefixed case too.
  451. const suffixedAfterPattern = new RegExp(/(?<!\])\{@(?<tag>[^}\s]+)\s?(?<namepathOrURL>[^}\s|]*)\s*(?<separator>[\s|])?\s*(?<text>[^}]*)\}/gu, 'gud');
  452. const matches = [...description.matchAll(prefixedTextPattern), ...description.matchAll(suffixedAfterPattern)];
  453. for (const mtch of matches) {
  454. const match =
  455. /**
  456. * @type {RegExpMatchArray & {
  457. * indices: {
  458. * groups: {
  459. * [key: string]: [number, number]
  460. * }
  461. * }
  462. * groups: {[key: string]: string}
  463. * }}
  464. */
  465. mtch;
  466. const {
  467. tag,
  468. namepathOrURL,
  469. text
  470. } = match.groups;
  471. const [start, end] = match.indices[0];
  472. const format = determineFormat(match);
  473. result.push({
  474. tag,
  475. namepathOrURL,
  476. text,
  477. format,
  478. start,
  479. end
  480. });
  481. }
  482. return result;
  483. }
  484. /**
  485. * Splits the `{@prefix}` from remaining `Spec.lines[].token.description`
  486. * into the `inlineTags` tokens, and populates `spec.inlineTags`
  487. * @param {import('comment-parser').Block} block
  488. * @returns {import('./index.js').JsdocBlockWithInline}
  489. */
  490. function parseInlineTags(block) {
  491. const inlineTags =
  492. /**
  493. * @type {(import('./commentParserToESTree.js').JsdocInlineTagNoType & {
  494. * line?: import('./commentParserToESTree.js').Integer
  495. * })[]}
  496. */
  497. parseDescription(block.description);
  498. /** @type {import('./index.js').JsdocBlockWithInline} */
  499. block.inlineTags = inlineTags;
  500. for (const tag of block.tags) {
  501. /**
  502. * @type {import('./index.js').JsdocTagWithInline}
  503. */
  504. tag.inlineTags = parseDescription(tag.description);
  505. }
  506. return (
  507. /**
  508. * @type {import('./index.js').JsdocBlockWithInline}
  509. */
  510. block
  511. );
  512. }
  513. /* eslint-disable prefer-named-capture-group -- Temporary */
  514. const {
  515. name: nameTokenizer,
  516. tag: tagTokenizer,
  517. type: typeTokenizer,
  518. description: descriptionTokenizer
  519. } = commentParser.tokenizers;
  520. /**
  521. * @param {import('comment-parser').Spec} spec
  522. * @returns {boolean}
  523. */
  524. const hasSeeWithLink = spec => {
  525. return spec.tag === 'see' && /\{@link.+?\}/u.test(spec.source[0].source);
  526. };
  527. const defaultNoTypes = ['default', 'defaultvalue', 'description', 'example', 'file', 'fileoverview', 'license', 'overview', 'see', 'summary'];
  528. const defaultNoNames = ['access', 'author', 'default', 'defaultvalue', 'description', 'example', 'exception', 'file', 'fileoverview', 'kind', 'license', 'overview', 'return', 'returns', 'since', 'summary', 'throws', 'version', 'variation'];
  529. const optionalBrackets = /^\[(?<name>[^=]*)=[^\]]*\]/u;
  530. const preserveTypeTokenizer = typeTokenizer('preserve');
  531. const preserveDescriptionTokenizer = descriptionTokenizer('preserve');
  532. const plainNameTokenizer = nameTokenizer();
  533. /**
  534. * Can't import `comment-parser/es6/parser/tokenizers/index.js`,
  535. * so we redefine here.
  536. * @typedef {(spec: import('comment-parser').Spec) =>
  537. * import('comment-parser').Spec} CommentParserTokenizer
  538. */
  539. /**
  540. * @param {object} [cfg]
  541. * @param {string[]} [cfg.noTypes]
  542. * @param {string[]} [cfg.noNames]
  543. * @returns {CommentParserTokenizer[]}
  544. */
  545. const getTokenizers = ({
  546. noTypes = defaultNoTypes,
  547. noNames = defaultNoNames
  548. } = {}) => {
  549. // trim
  550. return [
  551. // Tag
  552. tagTokenizer(),
  553. /**
  554. * Type tokenizer.
  555. * @param {import('comment-parser').Spec} spec
  556. * @returns {import('comment-parser').Spec}
  557. */
  558. spec => {
  559. if (noTypes.includes(spec.tag)) {
  560. return spec;
  561. }
  562. return preserveTypeTokenizer(spec);
  563. },
  564. /**
  565. * Name tokenizer.
  566. * @param {import('comment-parser').Spec} spec
  567. * @returns {import('comment-parser').Spec}
  568. */
  569. spec => {
  570. if (spec.tag === 'template') {
  571. // const preWS = spec.postTag;
  572. const remainder = spec.source[0].tokens.description;
  573. const pos = remainder.search(/(?<![\s,])\s/u);
  574. let name = pos === -1 ? remainder : remainder.slice(0, pos);
  575. const extra = remainder.slice(pos);
  576. let postName = '',
  577. description = '',
  578. lineEnd = '';
  579. if (pos > -1) {
  580. [, postName, description, lineEnd] = /** @type {RegExpMatchArray} */
  581. extra.match(/(\s*)([^\r]*)(\r)?/u);
  582. }
  583. if (optionalBrackets.test(name)) {
  584. var _name$match;
  585. name =
  586. /** @type {string} */
  587. /** @type {RegExpMatchArray} */
  588. (_name$match = name.match(optionalBrackets)) === null || _name$match === void 0 || (_name$match = _name$match.groups) === null || _name$match === void 0 ? void 0 : _name$match.name;
  589. spec.optional = true;
  590. } else {
  591. spec.optional = false;
  592. }
  593. spec.name = name;
  594. const {
  595. tokens
  596. } = spec.source[0];
  597. tokens.name = name;
  598. tokens.postName = postName;
  599. tokens.description = description;
  600. tokens.lineEnd = lineEnd || '';
  601. return spec;
  602. }
  603. if (noNames.includes(spec.tag) || hasSeeWithLink(spec)) {
  604. return spec;
  605. }
  606. return plainNameTokenizer(spec);
  607. },
  608. /**
  609. * Description tokenizer.
  610. * @param {import('comment-parser').Spec} spec
  611. * @returns {import('comment-parser').Spec}
  612. */
  613. spec => {
  614. return preserveDescriptionTokenizer(spec);
  615. }];
  616. };
  617. /**
  618. * Accepts a comment token and converts it into `comment-parser` AST.
  619. * @param {{value: string}} commentNode
  620. * @param {string} [indent] Whitespace
  621. * @returns {import('./index.js').JsdocBlockWithInline}
  622. */
  623. const parseComment = (commentNode, indent = '') => {
  624. // Preserve JSDoc block start/end indentation.
  625. const [block] = commentParser.parse(`${indent}/*${commentNode.value}*/`, {
  626. // @see https://github.com/yavorskiy/comment-parser/issues/21
  627. tokenizers: getTokenizers()
  628. });
  629. return parseInlineTags(block);
  630. };
  631. /* eslint-disable jsdoc/imports-as-dependencies -- https://github.com/gajus/eslint-plugin-jsdoc/issues/1114 */
  632. /**
  633. * Obtained originally from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313}.
  634. *
  635. * @license MIT
  636. */
  637. /**
  638. * @typedef {import('eslint').AST.Token | import('estree').Comment | {
  639. * type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
  640. * range: [number, number],
  641. * value: string
  642. * }} Token
  643. */
  644. /**
  645. * @typedef {import('eslint').Rule.Node|
  646. * import('@typescript-eslint/types').TSESTree.Node} ESLintOrTSNode
  647. */
  648. /**
  649. * @typedef {number} int
  650. */
  651. /**
  652. * Checks if the given token is a comment token or not.
  653. *
  654. * @param {Token} token - The token to check.
  655. * @returns {boolean} `true` if the token is a comment token.
  656. */
  657. const isCommentToken = token => {
  658. return token.type === 'Line' || token.type === 'Block' || token.type === 'Shebang';
  659. };
  660. /**
  661. * @param {(import('estree').Comment|import('eslint').Rule.Node) & {
  662. * declaration?: any,
  663. * decorators?: any[],
  664. * parent?: import('eslint').Rule.Node & {
  665. * decorators?: any[]
  666. * }
  667. * }} node
  668. * @returns {import('@typescript-eslint/types').TSESTree.Decorator|undefined}
  669. */
  670. const getDecorator = node => {
  671. var _node$declaration, _node$decorators, _node$parent;
  672. return (node === null || node === void 0 || (_node$declaration = node.declaration) === null || _node$declaration === void 0 || (_node$declaration = _node$declaration.decorators) === null || _node$declaration === void 0 ? void 0 : _node$declaration[0]) || (node === null || node === void 0 || (_node$decorators = node.decorators) === null || _node$decorators === void 0 ? void 0 : _node$decorators[0]) || (node === null || node === void 0 || (_node$parent = node.parent) === null || _node$parent === void 0 || (_node$parent = _node$parent.decorators) === null || _node$parent === void 0 ? void 0 : _node$parent[0]);
  673. };
  674. /**
  675. * Check to see if it is a ES6 export declaration.
  676. *
  677. * @param {import('eslint').Rule.Node} astNode An AST node.
  678. * @returns {boolean} whether the given node represents an export declaration.
  679. * @private
  680. */
  681. const looksLikeExport = function (astNode) {
  682. return astNode.type === 'ExportDefaultDeclaration' || astNode.type === 'ExportNamedDeclaration' || astNode.type === 'ExportAllDeclaration' || astNode.type === 'ExportSpecifier';
  683. };
  684. /**
  685. * @param {import('eslint').Rule.Node} astNode
  686. * @returns {import('eslint').Rule.Node}
  687. */
  688. const getTSFunctionComment = function (astNode) {
  689. const {
  690. parent
  691. } = astNode;
  692. /* c8 ignore next 3 */
  693. if (!parent) {
  694. return astNode;
  695. }
  696. const grandparent = parent.parent;
  697. /* c8 ignore next 3 */
  698. if (!grandparent) {
  699. return astNode;
  700. }
  701. const greatGrandparent = grandparent.parent;
  702. const greatGreatGrandparent = greatGrandparent && greatGrandparent.parent;
  703. // istanbul ignore if
  704. if ( /** @type {ESLintOrTSNode} */parent.type !== 'TSTypeAnnotation') {
  705. return astNode;
  706. }
  707. switch ( /** @type {ESLintOrTSNode} */grandparent.type) {
  708. // @ts-expect-error
  709. case 'PropertyDefinition':
  710. case 'ClassProperty':
  711. case 'TSDeclareFunction':
  712. case 'TSMethodSignature':
  713. case 'TSPropertySignature':
  714. return grandparent;
  715. case 'ArrowFunctionExpression':
  716. /* c8 ignore next 3 */
  717. if (!greatGrandparent) {
  718. return astNode;
  719. }
  720. // istanbul ignore else
  721. if (greatGrandparent.type === 'VariableDeclarator'
  722. // && greatGreatGrandparent.parent.type === 'VariableDeclaration'
  723. ) {
  724. /* c8 ignore next 3 */
  725. if (!greatGreatGrandparent || !greatGreatGrandparent.parent) {
  726. return astNode;
  727. }
  728. return greatGreatGrandparent.parent;
  729. }
  730. // istanbul ignore next
  731. return astNode;
  732. case 'FunctionExpression':
  733. /* c8 ignore next 3 */
  734. if (!greatGreatGrandparent) {
  735. return astNode;
  736. }
  737. // istanbul ignore else
  738. if (greatGrandparent.type === 'MethodDefinition') {
  739. return greatGrandparent;
  740. }
  741. // Fallthrough
  742. default:
  743. // istanbul ignore if
  744. if (grandparent.type !== 'Identifier') {
  745. // istanbul ignore next
  746. return astNode;
  747. }
  748. }
  749. /* c8 ignore next 3 */
  750. if (!greatGreatGrandparent) {
  751. return astNode;
  752. }
  753. // istanbul ignore next
  754. switch (greatGrandparent.type) {
  755. case 'ArrowFunctionExpression':
  756. // istanbul ignore else
  757. if (greatGreatGrandparent.type === 'VariableDeclarator' && greatGreatGrandparent.parent.type === 'VariableDeclaration') {
  758. return greatGreatGrandparent.parent;
  759. }
  760. // istanbul ignore next
  761. return astNode;
  762. case 'FunctionDeclaration':
  763. return greatGrandparent;
  764. case 'VariableDeclarator':
  765. // istanbul ignore else
  766. if (greatGreatGrandparent.type === 'VariableDeclaration') {
  767. return greatGreatGrandparent;
  768. }
  769. // Fallthrough
  770. default:
  771. // istanbul ignore next
  772. return astNode;
  773. }
  774. };
  775. const invokedExpression = new Set(['CallExpression', 'OptionalCallExpression', 'NewExpression']);
  776. const allowableCommentNode = new Set(['AssignmentPattern', 'VariableDeclaration', 'ExpressionStatement', 'MethodDefinition', 'Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition', 'ExportDefaultDeclaration', 'ReturnStatement']);
  777. /**
  778. * Reduces the provided node to the appropriate node for evaluating
  779. * JSDoc comment status.
  780. *
  781. * @param {import('eslint').Rule.Node} node An AST node.
  782. * @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode.
  783. * @returns {import('eslint').Rule.Node} The AST node that
  784. * can be evaluated for appropriate JSDoc comments.
  785. */
  786. const getReducedASTNode = function (node, sourceCode) {
  787. let {
  788. parent
  789. } = node;
  790. switch ( /** @type {ESLintOrTSNode} */node.type) {
  791. case 'TSFunctionType':
  792. return getTSFunctionComment(node);
  793. case 'TSInterfaceDeclaration':
  794. case 'TSTypeAliasDeclaration':
  795. case 'TSEnumDeclaration':
  796. case 'ClassDeclaration':
  797. case 'FunctionDeclaration':
  798. /* c8 ignore next 3 */
  799. if (!parent) {
  800. return node;
  801. }
  802. return looksLikeExport(parent) ? parent : node;
  803. case 'TSDeclareFunction':
  804. case 'ClassExpression':
  805. case 'ObjectExpression':
  806. case 'ArrowFunctionExpression':
  807. case 'TSEmptyBodyFunctionExpression':
  808. case 'FunctionExpression':
  809. /* c8 ignore next 3 */
  810. if (!parent) {
  811. return node;
  812. }
  813. if (!invokedExpression.has(parent.type)) {
  814. /**
  815. * @type {import('eslint').Rule.Node|Token|null}
  816. */
  817. let token = node;
  818. do {
  819. token = sourceCode.getTokenBefore( /** @type {import('eslint').Rule.Node|import('eslint').AST.Token} */
  820. token, {
  821. includeComments: true
  822. });
  823. } while (token && token.type === 'Punctuator' && token.value === '(');
  824. if (token && token.type === 'Block') {
  825. return node;
  826. }
  827. if (sourceCode.getCommentsBefore(node).length) {
  828. return node;
  829. }
  830. while (!sourceCode.getCommentsBefore(parent).length && !/Function/u.test(parent.type) && !allowableCommentNode.has(parent.type)) {
  831. ({
  832. parent
  833. } = parent);
  834. if (!parent) {
  835. break;
  836. }
  837. }
  838. if (parent && parent.type !== 'FunctionDeclaration' && parent.type !== 'Program') {
  839. if (parent.parent && parent.parent.type === 'ExportNamedDeclaration') {
  840. return parent.parent;
  841. }
  842. return parent;
  843. }
  844. }
  845. return node;
  846. default:
  847. return node;
  848. }
  849. };
  850. /**
  851. * Checks for the presence of a JSDoc comment for the given node and returns it.
  852. *
  853. * @param {import('eslint').Rule.Node} astNode The AST node to get
  854. * the comment for.
  855. * @param {import('eslint').SourceCode} sourceCode
  856. * @param {{maxLines: int, minLines: int, [name: string]: any}} settings
  857. * @returns {Token|null} The Block comment token containing the JSDoc comment
  858. * for the given node or null if not found.
  859. * @private
  860. */
  861. const findJSDocComment = (astNode, sourceCode, settings) => {
  862. var _parenthesisToken, _parenthesisToken2;
  863. const {
  864. minLines,
  865. maxLines
  866. } = settings;
  867. /** @type {import('eslint').Rule.Node|import('estree').Comment} */
  868. let currentNode = astNode;
  869. let tokenBefore = null;
  870. let parenthesisToken = null;
  871. while (currentNode) {
  872. const decorator = getDecorator(currentNode);
  873. if (decorator) {
  874. const dec = /** @type {unknown} */decorator;
  875. currentNode = /** @type {import('eslint').Rule.Node} */dec;
  876. }
  877. tokenBefore = sourceCode.getTokenBefore(currentNode, {
  878. includeComments: true
  879. });
  880. if (tokenBefore && tokenBefore.type === 'Punctuator' && tokenBefore.value === '(') {
  881. parenthesisToken = tokenBefore;
  882. [tokenBefore] = sourceCode.getTokensBefore(currentNode, {
  883. count: 2,
  884. includeComments: true
  885. });
  886. }
  887. if (!tokenBefore || !isCommentToken(tokenBefore)) {
  888. return null;
  889. }
  890. if (tokenBefore.type === 'Line') {
  891. currentNode = tokenBefore;
  892. continue;
  893. }
  894. break;
  895. }
  896. /* c8 ignore next 3 */
  897. if (!tokenBefore || !currentNode.loc || !tokenBefore.loc) {
  898. return null;
  899. }
  900. if (tokenBefore.type === 'Block' && /^\*\s/u.test(tokenBefore.value) && currentNode.loc.start.line - ( /** @type {import('eslint').AST.Token} */(_parenthesisToken = parenthesisToken) !== null && _parenthesisToken !== void 0 ? _parenthesisToken : tokenBefore).loc.end.line >= minLines && currentNode.loc.start.line - ( /** @type {import('eslint').AST.Token} */(_parenthesisToken2 = parenthesisToken) !== null && _parenthesisToken2 !== void 0 ? _parenthesisToken2 : tokenBefore).loc.end.line <= maxLines) {
  901. return tokenBefore;
  902. }
  903. return null;
  904. };
  905. /**
  906. * Retrieves the JSDoc comment for a given node.
  907. *
  908. * @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
  909. * @param {import('eslint').Rule.Node} node The AST node to get
  910. * the comment for.
  911. * @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
  912. * settings in context
  913. * @returns {Token|null} The Block comment
  914. * token containing the JSDoc comment for the given node or
  915. * null if not found.
  916. * @public
  917. */
  918. const getJSDocComment = function (sourceCode, node, settings) {
  919. const reducedNode = getReducedASTNode(node, sourceCode);
  920. return findJSDocComment(reducedNode, sourceCode, settings);
  921. };
  922. /**
  923. * @typedef {import('./index.js').ESTreeToStringOptions} ESTreeToStringOptions
  924. */
  925. const stringifiers = {
  926. /**
  927. * @param {import('./commentParserToESTree.js').JsdocBlock} node
  928. * @param {ESTreeToStringOptions} opts
  929. * @param {string[]} descriptionLines
  930. * @param {string[]} tags
  931. * @returns {string}
  932. */
  933. JsdocBlock({
  934. delimiter,
  935. postDelimiter,
  936. lineEnd,
  937. initial,
  938. terminal,
  939. endLine
  940. }, opts, descriptionLines, tags) {
  941. var _descriptionLines$at, _tags$at;
  942. const alreadyHasLine = descriptionLines.length && !tags.length && ((_descriptionLines$at = descriptionLines.at(-1)) === null || _descriptionLines$at === void 0 ? void 0 : _descriptionLines$at.endsWith('\n')) || tags.length && ((_tags$at = tags.at(-1)) === null || _tags$at === void 0 ? void 0 : _tags$at.endsWith('\n'));
  943. return `${initial}${delimiter}${postDelimiter}${endLine ? `
  944. ` : ''}${
  945. // Could use `node.description` (and `node.lineEnd`), but lines may have
  946. // been modified
  947. descriptionLines.length ? descriptionLines.join(lineEnd + '\n') + (tags.length ? lineEnd + '\n' : '') : ''}${tags.length ? tags.join(lineEnd + '\n') : ''}${endLine && !alreadyHasLine ? `${lineEnd}
  948. ${initial}` : endLine ? ` ${initial}` : ''}${terminal}`;
  949. },
  950. /**
  951. * @param {import('./commentParserToESTree.js').JsdocDescriptionLine} node
  952. * @returns {string}
  953. */
  954. JsdocDescriptionLine({
  955. initial,
  956. delimiter,
  957. postDelimiter,
  958. description
  959. }) {
  960. return `${initial}${delimiter}${postDelimiter}${description}`;
  961. },
  962. /**
  963. * @param {import('./commentParserToESTree.js').JsdocTypeLine} node
  964. * @returns {string}
  965. */
  966. JsdocTypeLine({
  967. initial,
  968. delimiter,
  969. postDelimiter,
  970. rawType
  971. }) {
  972. return `${initial}${delimiter}${postDelimiter}${rawType}`;
  973. },
  974. /**
  975. * @param {import('./commentParserToESTree.js').JsdocInlineTag} node
  976. */
  977. JsdocInlineTag({
  978. format,
  979. namepathOrURL,
  980. tag,
  981. text
  982. }) {
  983. return format === 'pipe' ? `{@${tag} ${namepathOrURL}|${text}}` : format === 'plain' ? `{@${tag} ${namepathOrURL}}` : format === 'prefix' ? `[${text}]{@${tag} ${namepathOrURL}}`
  984. // "space"
  985. : `{@${tag} ${namepathOrURL} ${text}}`;
  986. },
  987. /**
  988. * @param {import('./commentParserToESTree.js').JsdocTag} node
  989. * @param {ESTreeToStringOptions} opts
  990. * @param {string} parsedType
  991. * @param {string[]} typeLines
  992. * @param {string[]} descriptionLines
  993. * @returns {string}
  994. */
  995. JsdocTag(node, opts, parsedType, typeLines, descriptionLines) {
  996. const {
  997. description,
  998. name,
  999. postName,
  1000. postTag,
  1001. postType,
  1002. initial,
  1003. delimiter,
  1004. postDelimiter,
  1005. tag
  1006. // , rawType
  1007. } = node;
  1008. return `${initial}${delimiter}${postDelimiter}@${tag}${postTag}${
  1009. // Could do `rawType` but may have been changed; could also do
  1010. // `typeLines` but not as likely to be changed
  1011. // parsedType
  1012. // Comment this out later in favor of `parsedType`
  1013. // We can't use raw `typeLines` as first argument has delimiter on it
  1014. opts.preferRawType || !parsedType ? typeLines.length ? `{${typeLines.join('\n')}}` : '' : parsedType}${postType}${name ? `${name}${postName || (description ? '\n' : '')}` : ''}${descriptionLines.join('\n')}`;
  1015. }
  1016. };
  1017. const visitorKeys = {
  1018. ...jsdocVisitorKeys,
  1019. ...jsdocTypePrattParser.visitorKeys
  1020. };
  1021. /**
  1022. * @todo convert for use by escodegen (until may be patched to support
  1023. * custom entries?).
  1024. * @param {import('./commentParserToESTree.js').JsdocBlock|
  1025. * import('./commentParserToESTree.js').JsdocDescriptionLine|
  1026. * import('./commentParserToESTree.js').JsdocTypeLine|
  1027. * import('./commentParserToESTree.js').JsdocTag|
  1028. * import('./commentParserToESTree.js').JsdocInlineTag|
  1029. * import('jsdoc-type-pratt-parser').RootResult
  1030. * } node
  1031. * @param {ESTreeToStringOptions} opts
  1032. * @throws {Error}
  1033. * @returns {string}
  1034. */
  1035. function estreeToString(node, opts = {}) {
  1036. if (Object.prototype.hasOwnProperty.call(stringifiers, node.type)) {
  1037. const childNodeOrArray = visitorKeys[node.type];
  1038. const args = /** @type {(string[]|string|null)[]} */
  1039. childNodeOrArray.map(key => {
  1040. // @ts-expect-error
  1041. return Array.isArray(node[key])
  1042. // @ts-expect-error
  1043. ? node[key].map((
  1044. /**
  1045. * @type {import('./commentParserToESTree.js').JsdocBlock|
  1046. * import('./commentParserToESTree.js').JsdocDescriptionLine|
  1047. * import('./commentParserToESTree.js').JsdocTypeLine|
  1048. * import('./commentParserToESTree.js').JsdocTag|
  1049. * import('./commentParserToESTree.js').JsdocInlineTag}
  1050. */
  1051. item) => {
  1052. return estreeToString(item, opts);
  1053. })
  1054. // @ts-expect-error
  1055. : node[key] === undefined || node[key] === null ? null
  1056. // @ts-expect-error
  1057. : estreeToString(node[key], opts);
  1058. });
  1059. return stringifiers[
  1060. /**
  1061. * @type {import('./commentParserToESTree.js').JsdocBlock|
  1062. * import('./commentParserToESTree.js').JsdocDescriptionLine|
  1063. * import('./commentParserToESTree.js').JsdocTypeLine|
  1064. * import('./commentParserToESTree.js').JsdocTag}
  1065. */
  1066. node.type](node, opts,
  1067. // @ts-expect-error
  1068. ...args);
  1069. }
  1070. // We use raw type instead but it is a key as other apps may wish to traverse
  1071. if (node.type.startsWith('JsdocType')) {
  1072. return opts.preferRawType ? '' : `{${jsdocTypePrattParser.stringify( /** @type {import('jsdoc-type-pratt-parser').RootResult} */
  1073. node)}}`;
  1074. }
  1075. throw new Error(`Unhandled node type: ${node.type}`);
  1076. }
  1077. Object.defineProperty(exports, 'jsdocTypeVisitorKeys', {
  1078. enumerable: true,
  1079. get: function () { return jsdocTypePrattParser.visitorKeys; }
  1080. });
  1081. exports.commentHandler = commentHandler;
  1082. exports.commentParserToESTree = commentParserToESTree;
  1083. exports.defaultNoNames = defaultNoNames;
  1084. exports.defaultNoTypes = defaultNoTypes;
  1085. exports.estreeToString = estreeToString;
  1086. exports.findJSDocComment = findJSDocComment;
  1087. exports.getDecorator = getDecorator;
  1088. exports.getJSDocComment = getJSDocComment;
  1089. exports.getReducedASTNode = getReducedASTNode;
  1090. exports.getTokenizers = getTokenizers;
  1091. exports.hasSeeWithLink = hasSeeWithLink;
  1092. exports.jsdocVisitorKeys = jsdocVisitorKeys;
  1093. exports.parseComment = parseComment;
  1094. exports.toCamelCase = toCamelCase;
  1095. Object.keys(jsdocTypePrattParser).forEach(function (k) {
  1096. if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
  1097. enumerable: true,
  1098. get: function () { return jsdocTypePrattParser[k]; }
  1099. });
  1100. });