@cjser/es-joy__jsdoccomment 0.96.0-cjser.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,525 @@
1
+ import {parse as jsdocTypePrattParse} from '@cjser/jsdoc-type-pratt-parser';
2
+
3
+ /**
4
+ * Removes initial and ending brackets from `rawType`
5
+ * @param {JsdocTypeLine[]|JsdocTag} container
6
+ * @param {boolean} [isArr]
7
+ * @returns {void}
8
+ */
9
+ const stripEncapsulatingBrackets = (container, isArr) => {
10
+ if (isArr) {
11
+ const firstItem = /** @type {JsdocTypeLine[]} */ (container)[0];
12
+ firstItem.rawType = firstItem.rawType.replace(
13
+ /^\{/v, ''
14
+ );
15
+
16
+ const lastItem = /** @type {JsdocTypeLine} */ (
17
+ /** @type {JsdocTypeLine[]} */ (
18
+ container
19
+ ).at(-1)
20
+ );
21
+ lastItem.rawType = lastItem.rawType.replace(/\}$/v, '');
22
+
23
+ return;
24
+ }
25
+ /** @type {JsdocTag} */ (container).rawType =
26
+ /** @type {JsdocTag} */ (container).rawType.replace(
27
+ /^\{/v, ''
28
+ ).replace(/\}$/v, '');
29
+ };
30
+
31
+ /**
32
+ * @typedef {{
33
+ * delimiter: string,
34
+ * postDelimiter: string,
35
+ * rawType: string,
36
+ * initial: string,
37
+ * type: "JsdocTypeLine"
38
+ * }} JsdocTypeLine
39
+ */
40
+
41
+ /**
42
+ * @typedef {{
43
+ * delimiter: string,
44
+ * description: string,
45
+ * postDelimiter: string,
46
+ * initial: string,
47
+ * type: "JsdocDescriptionLine"
48
+ * }} JsdocDescriptionLine
49
+ */
50
+
51
+ /**
52
+ * An inline tag whose `text` is the unescaped label value.
53
+ * @typedef {{
54
+ * format: 'pipe' | 'plain' | 'prefix' | 'space',
55
+ * namepathOrURL: string,
56
+ * tag: string,
57
+ * text: string,
58
+ * }} JsdocInlineTagNoType
59
+ */
60
+ /**
61
+ * @typedef {JsdocInlineTagNoType & {
62
+ * type: "JsdocInlineTag"
63
+ * }} JsdocInlineTag
64
+ */
65
+
66
+ /**
67
+ * @typedef {{
68
+ * delimiter: string,
69
+ * description: string,
70
+ * descriptionLines: JsdocDescriptionLine[],
71
+ * initial: string,
72
+ * inlineTags: JsdocInlineTag[]
73
+ * name: string,
74
+ * postDelimiter: string,
75
+ * postName: string,
76
+ * postTag: string,
77
+ * postType: string,
78
+ * rawType: string,
79
+ * parsedType: import('@cjser/jsdoc-type-pratt-parser').RootResult|null
80
+ * tag: string,
81
+ * type: "JsdocTag",
82
+ * typeLines: JsdocTypeLine[],
83
+ * }} JsdocTag
84
+ */
85
+
86
+ /**
87
+ * @typedef {number} Integer
88
+ */
89
+
90
+ /**
91
+ * @typedef {{
92
+ * delimiter: string,
93
+ * delimiterLineBreak: string,
94
+ * description: string,
95
+ * descriptionEndLine?: Integer,
96
+ * descriptionLines: JsdocDescriptionLine[],
97
+ * descriptionStartLine?: Integer,
98
+ * hasPreterminalDescription: 0|1,
99
+ * hasPreterminalTagDescription?: 1,
100
+ * initial: string,
101
+ * inlineTags: JsdocInlineTag[]
102
+ * lastDescriptionLine?: Integer,
103
+ * endLine: Integer,
104
+ * lineEnd: string,
105
+ * postDelimiter: string,
106
+ * tags: JsdocTag[],
107
+ * terminal: string,
108
+ * preterminalLineBreak: string,
109
+ * type: "JsdocBlock",
110
+ * }} JsdocBlock
111
+ */
112
+
113
+ /**
114
+ * @param {object} cfg
115
+ * @param {string} cfg.text
116
+ * @param {string} cfg.tag
117
+ * @param {'pipe' | 'plain' | 'prefix' | 'space'} cfg.format
118
+ * @param {string} cfg.namepathOrURL
119
+ * @returns {JsdocInlineTag}
120
+ */
121
+ const inlineTagToAST = ({text, tag, format, namepathOrURL}) => ({
122
+ text,
123
+ tag,
124
+ format,
125
+ namepathOrURL,
126
+ type: 'JsdocInlineTag'
127
+ });
128
+
129
+ /* eslint-disable jsdoc/reject-any-type -- API */
130
+ /**
131
+ * @typedef {{
132
+ * module?: boolean;
133
+ * strictMode?: boolean;
134
+ * asyncFunctionBody?: boolean;
135
+ * classContext?: boolean;
136
+ * computedPropertyParser?: (text: string, options?: any) => unknown;
137
+ * }} JtppOptions
138
+ */
139
+ /* eslint-enable jsdoc/reject-any-type -- API */
140
+
141
+ /**
142
+ * @typedef {object} CommentParserToESTreeOptions
143
+ * @property {'compact'|'preserve'} [spacing] By default, empty lines are
144
+ * compacted; set to 'preserve' to preserve empty comment lines.
145
+ * @property {boolean} [throwOnTypeParsingErrors]
146
+ * @property {JtppOptions} [jsdocTypePrattParserArgs]
147
+ */
148
+
149
+ /**
150
+ * Converts comment parser AST to ESTree format.
151
+ * @param {import('./index.js').JsdocBlockWithInline} jsdoc
152
+ * @param {import('@cjser/jsdoc-type-pratt-parser').ParseMode} mode
153
+ * @param {CommentParserToESTreeOptions} [opts]
154
+ * @returns {JsdocBlock}
155
+ */
156
+ const commentParserToESTree = (jsdoc, mode = 'typescript', {
157
+ spacing = 'compact',
158
+ throwOnTypeParsingErrors = false,
159
+ jsdocTypePrattParserArgs
160
+ } = {}) => {
161
+ /**
162
+ * Strips brackets from a tag's `rawType` values and adds `parsedType`
163
+ * @param {JsdocTag} lastTag
164
+ * @returns {void}
165
+ */
166
+ const cleanUpLastTag = (lastTag) => {
167
+ // Strip out `}` that encapsulates and is not part of
168
+ // the type
169
+ stripEncapsulatingBrackets(lastTag);
170
+
171
+ if (lastTag.typeLines.length) {
172
+ stripEncapsulatingBrackets(lastTag.typeLines, true);
173
+ }
174
+
175
+ // Remove single empty line description.
176
+ if (lastTag.descriptionLines.length === 1 &&
177
+ lastTag.descriptionLines[0].description === '') {
178
+ lastTag.descriptionLines.length = 0;
179
+ }
180
+
181
+ // With even a multiline type now in full, add parsing
182
+ let parsedType = null;
183
+
184
+ try {
185
+ parsedType = jsdocTypePrattParse(
186
+ lastTag.rawType, mode, jsdocTypePrattParserArgs
187
+ );
188
+ } catch (err) {
189
+ // Ignore
190
+ if (throwOnTypeParsingErrors && lastTag.rawType) {
191
+ /** @type {Error} */ (
192
+ err
193
+ ).message = `Tag @${lastTag.tag} with raw type ` +
194
+ `\`${lastTag.rawType}\` had parsing error: ${
195
+ /** @type {Error} */ (err).message}`;
196
+ throw err;
197
+ }
198
+ }
199
+
200
+ lastTag.parsedType = parsedType;
201
+ };
202
+
203
+ const {source, inlineTags: blockInlineTags} = jsdoc;
204
+
205
+ const {tokens: {
206
+ delimiter: delimiterRoot,
207
+ lineEnd: lineEndRoot,
208
+ postDelimiter: postDelimiterRoot,
209
+ start: startRoot,
210
+ end: endRoot
211
+ }} = source[0];
212
+
213
+ const endLine = source.length - 1;
214
+
215
+ /** @type {JsdocBlock} */
216
+ const ast = {
217
+ delimiter: delimiterRoot,
218
+ delimiterLineBreak: '\n',
219
+ description: '',
220
+
221
+ descriptionLines: [],
222
+ inlineTags: blockInlineTags.map((t) => inlineTagToAST(t)),
223
+
224
+ initial: startRoot,
225
+ tags: [],
226
+ // `terminal` will be overwritten if there are other entries
227
+ terminal: endRoot,
228
+ preterminalLineBreak: '\n',
229
+ hasPreterminalDescription: 0,
230
+ endLine,
231
+ postDelimiter: postDelimiterRoot,
232
+ lineEnd: lineEndRoot,
233
+
234
+ type: 'JsdocBlock'
235
+ };
236
+
237
+ /**
238
+ * @type {JsdocTag[]}
239
+ */
240
+ const tags = [];
241
+
242
+ /** @type {Integer|undefined} */
243
+ let lastDescriptionLine;
244
+
245
+ /** @type {JsdocTag|null} */
246
+ let lastTag = null;
247
+
248
+ // Tracks when first valid tag description line is seen.
249
+ let tagDescriptionSeen = false;
250
+
251
+ let descLineStateOpen = true;
252
+
253
+ source.forEach((info, idx) => {
254
+ const {tokens} = info;
255
+ const {
256
+ delimiter,
257
+ description,
258
+ postDelimiter,
259
+ start: initial,
260
+ tag,
261
+ end,
262
+ type: rawType
263
+ } = tokens;
264
+
265
+ if (!tag && description && descLineStateOpen) {
266
+ if (ast.descriptionStartLine === undefined) {
267
+ ast.descriptionStartLine = idx;
268
+ }
269
+ ast.descriptionEndLine = idx;
270
+ }
271
+
272
+ if (tag || end) {
273
+ descLineStateOpen = false;
274
+ if (lastDescriptionLine === undefined) {
275
+ lastDescriptionLine = idx;
276
+ }
277
+
278
+ // Clean-up with last tag before end or new tag
279
+ if (lastTag) {
280
+ cleanUpLastTag(lastTag);
281
+ }
282
+
283
+ // Stop the iteration when we reach the end
284
+ // but only when there is no tag earlier in the line
285
+ // to still process
286
+ if (end && !tag) {
287
+ ast.terminal = end;
288
+
289
+ // Check if there are any description lines and if not then this is a
290
+ // one line comment block.
291
+ const isDelimiterLine = ast.descriptionLines.length === 0 &&
292
+ delimiter === '/**';
293
+
294
+ // Remove delimiter line break for one line comments blocks.
295
+ if (isDelimiterLine) {
296
+ ast.delimiterLineBreak = '';
297
+ }
298
+
299
+ if (description) {
300
+ // Remove terminal line break at end when description is defined.
301
+ if (ast.terminal === '*/') {
302
+ ast.preterminalLineBreak = '';
303
+ }
304
+
305
+ if (lastTag) {
306
+ ast.hasPreterminalTagDescription = 1;
307
+ } else {
308
+ ast.hasPreterminalDescription = 1;
309
+ }
310
+
311
+ const holder = lastTag || ast;
312
+ holder.description += (holder.description ? '\n' : '') + description;
313
+
314
+ // Do not include `delimiter` / `postDelimiter` for opening
315
+ // delimiter line.
316
+
317
+ holder.descriptionLines.push({
318
+ delimiter: isDelimiterLine ? '' : delimiter,
319
+ description,
320
+ postDelimiter: isDelimiterLine ? '' : postDelimiter,
321
+ initial,
322
+ type: 'JsdocDescriptionLine'
323
+ });
324
+ }
325
+ return;
326
+ }
327
+
328
+ const {
329
+ // eslint-disable-next-line no-unused-vars -- Discarding
330
+ end: ed,
331
+ delimiter: de,
332
+ postDelimiter: pd,
333
+ start: init,
334
+ ...tkns
335
+ } = tokens;
336
+
337
+ if (!tokens.name) {
338
+ let i = 1;
339
+ while (Object.hasOwn(source, idx + i)) {
340
+ const {tokens: {
341
+ name,
342
+ postName,
343
+ postType,
344
+ tag: tg
345
+ }} = source[idx + i];
346
+ if (tg) {
347
+ break;
348
+ }
349
+ if (name) {
350
+ tkns.postType = postType;
351
+ tkns.name = name;
352
+ tkns.postName = postName;
353
+ break;
354
+ }
355
+ i++;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * @type {JsdocInlineTag[]}
361
+ */
362
+ const tagInlineTags = tag
363
+ // Assuming the tags from `source` are in the same order as `jsdoc.tags`
364
+ // we can use the `tags` length as index into the parser result tags.
365
+ // eslint-disable-next-line @stylistic/operator-linebreak -- Required
366
+ ?
367
+ /**
368
+ * @type {import('comment-parser').Spec & {
369
+ * inlineTags: JsdocInlineTagNoType[]
370
+ * }}
371
+ */ (
372
+ jsdoc.tags[tags.length]
373
+ ).inlineTags.map(
374
+ (t) => inlineTagToAST(t)
375
+ )
376
+ : [];
377
+
378
+ /** @type {JsdocTag} */
379
+ const tagObj = {
380
+ ...tkns,
381
+ initial: endLine ? init : '',
382
+ postDelimiter: lastDescriptionLine ? pd : '',
383
+ delimiter: lastDescriptionLine ? de : '',
384
+ descriptionLines: [],
385
+ inlineTags: tagInlineTags,
386
+ parsedType: null,
387
+ rawType: '',
388
+ type: 'JsdocTag',
389
+ typeLines: []
390
+ };
391
+ tagObj.tag = tagObj.tag.replace(/^@/v, '');
392
+
393
+ lastTag = tagObj;
394
+ tagDescriptionSeen = false;
395
+
396
+ tags.push(tagObj);
397
+ }
398
+
399
+ if (rawType) {
400
+ // Will strip rawType brackets after this tag
401
+ /** @type {JsdocTag} */ (lastTag).typeLines.push(
402
+ /** @type {JsdocTag} */ (lastTag).typeLines.length
403
+ ? {
404
+ delimiter,
405
+ postDelimiter,
406
+ rawType,
407
+ initial,
408
+ type: 'JsdocTypeLine'
409
+ }
410
+ : {
411
+ delimiter: '',
412
+ postDelimiter: '',
413
+ rawType,
414
+ initial: '',
415
+ type: 'JsdocTypeLine'
416
+ }
417
+ );
418
+ /** @type {JsdocTag} */ (lastTag).rawType += /** @type {JsdocTag} */ (
419
+ lastTag
420
+ ).rawType
421
+ ? '\n' + rawType
422
+ : rawType;
423
+ }
424
+
425
+ // In `compact` mode skip processing if `description` is an empty string
426
+ // unless lastTag is being processed.
427
+ //
428
+ // In `preserve` mode process when `description` is not the `empty string
429
+ // or the `delimiter` is not `/**` ensuring empty lines are preserved.
430
+ if ((lastTag || (spacing === 'compact' && description)) ||
431
+ (spacing === 'preserve' && (description || delimiter !== '/**'))) {
432
+ const holder = lastTag || ast;
433
+
434
+ // Check if there are any description lines and if not then this is a
435
+ // multi-line comment block with description on 0th line. Treat
436
+ // `delimiter` / `postDelimiter` / `initial` as being on a new line.
437
+ const isDelimiterLine = holder.descriptionLines.length === 0 &&
438
+ delimiter === '/**';
439
+
440
+ // Remove delimiter line break for one line comments blocks.
441
+ if (isDelimiterLine) {
442
+ ast.delimiterLineBreak = '';
443
+ }
444
+
445
+ // Track when the first description line is seen to avoid adding empty
446
+ // description lines for tag type lines.
447
+ tagDescriptionSeen ||= Boolean(lastTag &&
448
+ (rawType === '' || rawType?.endsWith('}')));
449
+
450
+ if (lastTag) {
451
+ if (tagDescriptionSeen) {
452
+ // The first tag description line is a continuation after type /
453
+ // name parsing.
454
+ const isFirstDescriptionLine = holder.descriptionLines.length === 0;
455
+
456
+ // For `compact` spacing must allow through first description line.
457
+ if (spacing === 'preserve' || (spacing === 'compact' &&
458
+ (description || isFirstDescriptionLine))) {
459
+ holder.descriptionLines.push({
460
+ delimiter: isFirstDescriptionLine ? '' : delimiter,
461
+ description,
462
+ postDelimiter: isFirstDescriptionLine ? '' : postDelimiter,
463
+ initial: isFirstDescriptionLine ? '' : initial,
464
+ type: 'JsdocDescriptionLine'
465
+ });
466
+ }
467
+ }
468
+ } else {
469
+ holder.descriptionLines.push({
470
+ delimiter: isDelimiterLine ? '' : delimiter,
471
+ description,
472
+ postDelimiter: isDelimiterLine ? '' : postDelimiter,
473
+ initial: isDelimiterLine ? `` : initial,
474
+ type: 'JsdocDescriptionLine'
475
+ });
476
+ }
477
+
478
+ if (!tag) {
479
+ if (lastTag) {
480
+ // For `compact` spacing must filter out any empty description lines
481
+ // after the initial `holder.description` has content.
482
+ if (tagDescriptionSeen && !(spacing === 'compact' &&
483
+ description === '' &&
484
+ holder.description)) {
485
+ holder.description += !holder.description
486
+ ? description
487
+ : '\n' + description;
488
+ }
489
+ } else {
490
+ holder.description += !holder.description
491
+ ? description
492
+ : '\n' + description;
493
+ }
494
+ }
495
+ }
496
+
497
+ // Clean-up where last line itself has tag content
498
+ if (end && tag) {
499
+ ast.terminal = end;
500
+ ast.hasPreterminalTagDescription = 1;
501
+
502
+ // Remove terminal line break at end when tag is defined on last line.
503
+ if (ast.terminal === '*/') {
504
+ ast.preterminalLineBreak = '';
505
+ }
506
+
507
+ cleanUpLastTag(/** @type {JsdocTag} */ (lastTag));
508
+ }
509
+ });
510
+
511
+ ast.lastDescriptionLine = lastDescriptionLine;
512
+ ast.tags = tags;
513
+
514
+ return ast;
515
+ };
516
+
517
+ const jsdocVisitorKeys = {
518
+ JsdocBlock: ['descriptionLines', 'tags', 'inlineTags'],
519
+ JsdocDescriptionLine: [],
520
+ JsdocTypeLine: [],
521
+ JsdocTag: ['parsedType', 'typeLines', 'descriptionLines', 'inlineTags'],
522
+ JsdocInlineTag: []
523
+ };
524
+
525
+ export {commentParserToESTree, jsdocVisitorKeys};