@lewishowles/lint-config 0.1.3 → 0.3.0

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,756 @@
1
+ import { getCommentText, getLineIndent, getNewline } from "./source.js";
2
+ import { addTerminalPunctuation, capitaliseSentence, formatSentence, wrapWords } from "./wrap.js";
3
+
4
+ // The JSDoc tags this package formats, in their required output order.
5
+ const tagOrder = ["param", "throws", "returns"];
6
+ // Tags whose section content is kept verbatim rather than reflowed.
7
+ const preservedSectionTags = new Set(["example"]);
8
+ // Fast lookup set built from tagOrder.
9
+ const targetTags = new Set(tagOrder);
10
+
11
+ /**
12
+ * Return whether source text is a JSDoc-style block comment.
13
+ *
14
+ * @param {string} commentText
15
+ * The comment source text.
16
+ *
17
+ * @returns {boolean}
18
+ * Whether the comment starts with JSDoc syntax.
19
+ */
20
+ export function isJSDoc(commentText) {
21
+ return commentText.startsWith("/**");
22
+ }
23
+
24
+ /**
25
+ * Read the undecorated content lines of a JSDoc comment.
26
+ *
27
+ * @param {string} commentText
28
+ * The comment source text.
29
+ *
30
+ * @returns {string[]}
31
+ * The comment content lines.
32
+ */
33
+ export function getJSDocContent(commentText) {
34
+ // Strips the /** and */ delimiters, leaving the raw comment body.
35
+ const body = commentText.slice(3, -2);
36
+
37
+ // Removes each line's leading indentation and star decoration.
38
+ const lines = body.split(/\r\n|\n|\r/).map((line) => {
39
+ if (/^\s*\*/.test(line)) {
40
+ return line.replace(/^\s*\* ?/, "");
41
+ }
42
+
43
+ return line.trim();
44
+ });
45
+
46
+ while (lines[0] === "") {
47
+ lines.shift();
48
+ }
49
+
50
+ while (lines.at(-1) === "") {
51
+ lines.pop();
52
+ }
53
+
54
+ return lines;
55
+ }
56
+
57
+ /**
58
+ * Split JSDoc content into prose and tag lines.
59
+ *
60
+ * @param {string[]} contentLines
61
+ * The undecorated comment content.
62
+ *
63
+ * @returns {object}
64
+ * Prose and tag content.
65
+ */
66
+ function splitJSDocContent(contentLines) {
67
+ // Finds where the tag section begins, if there is one.
68
+ const firstTagIndex = contentLines.findIndex((line) => /^@\w+\b/.test(line.trim()));
69
+
70
+ if (firstTagIndex < 0) {
71
+ return { proseLines: contentLines, tagLines: [] };
72
+ }
73
+
74
+ return {
75
+ proseLines: contentLines.slice(0, firstTagIndex),
76
+ tagLines: contentLines.slice(firstTagIndex),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Return the tag name at the beginning of a JSDoc line.
82
+ *
83
+ * @param {string} line
84
+ * The undecorated JSDoc line.
85
+ *
86
+ * @returns {string|null}
87
+ * The tag name, or null when the line is not a tag.
88
+ */
89
+ function getJSDocTagName(line) {
90
+ // Matches the leading @tagName, ignoring any indentation.
91
+ const match = line.trim().match(/^@([a-zA-Z][\w-]*)\b/);
92
+
93
+ return match?.[1] ?? null;
94
+ }
95
+
96
+ /**
97
+ * Return whether a line is a target JSDoc tag.
98
+ *
99
+ * @param {string} line
100
+ * The undecorated JSDoc line.
101
+ *
102
+ * @returns {boolean}
103
+ * Whether the line is one of the Phase 1 tags.
104
+ */
105
+ function isTargetTag(line) {
106
+ // The tag name, or null when the line isn't a tag at all.
107
+ const tagName = getJSDocTagName(line);
108
+
109
+ return tagName !== null && targetTags.has(tagName);
110
+ }
111
+
112
+ /**
113
+ * Return whether a JSDoc tag starts a section whose content stays verbatim.
114
+ *
115
+ * @param {string} line
116
+ * The undecorated JSDoc line.
117
+ *
118
+ * @returns {boolean}
119
+ * Whether the line starts a preserved section.
120
+ */
121
+ function isPreservedSectionTag(line) {
122
+ return preservedSectionTags.has(getJSDocTagName(line));
123
+ }
124
+
125
+ /**
126
+ * Parse target JSDoc tag entries.
127
+ *
128
+ * @param {string[]} tagLines
129
+ * The undecorated tag content.
130
+ *
131
+ * @returns {object[]}
132
+ * Parsed target tag entries.
133
+ */
134
+ function parseTargetTags(tagLines) {
135
+ // The parsed tag entries, in source order.
136
+ const entries = [];
137
+
138
+ // The tag entry currently collecting description lines.
139
+ let currentEntry = null;
140
+
141
+ for (const line of tagLines) {
142
+ // Matches a new @param/@throws/@returns tag line.
143
+ const match = line.trim().match(/^@(param|throws|returns)\b(.*)$/);
144
+
145
+ if (match) {
146
+ currentEntry = {
147
+ description: [],
148
+ rest: match[2].trim(),
149
+ type: match[1],
150
+ };
151
+ entries.push(currentEntry);
152
+ } else if (currentEntry) {
153
+ currentEntry.description.push(line);
154
+ }
155
+ }
156
+
157
+ return entries;
158
+ }
159
+
160
+ /**
161
+ * Format a target JSDoc tag header.
162
+ *
163
+ * @param {object} entry
164
+ * The parsed tag entry.
165
+ *
166
+ * @returns {string}
167
+ * The aligned tag header.
168
+ */
169
+ function formatTagHeader(entry) {
170
+ // Splits the tag's remainder into its type, name, and description.
171
+ const typeMatch = entry.rest.match(/^(\{[^}]+\})(?:\s+(\S+))?(?:\s+(.*))?$/);
172
+
173
+ if (!typeMatch) {
174
+ return `@${entry.type} ${entry.rest}`.trimEnd();
175
+ }
176
+
177
+ // The bare {type} annotation.
178
+ const type = typeMatch[1];
179
+ // The parameter name, when the tag has one.
180
+ const name = typeMatch[2];
181
+
182
+ if (entry.type === "param" && name) {
183
+ return `@param ${type} ${name}`;
184
+ }
185
+
186
+ return `@${entry.type} ${type}`;
187
+ }
188
+
189
+ /**
190
+ * Return the inline description from a target tag.
191
+ *
192
+ * @param {object} entry
193
+ * The parsed tag entry.
194
+ *
195
+ * @returns {string}
196
+ * The inline description, when present.
197
+ */
198
+ function getInlineTagDescription(entry) {
199
+ // Splits the tag's remainder into its type, name, and description.
200
+ const typeMatch = entry.rest.match(/^(\{[^}]+\})(?:\s+(\S+))?(?:\s+(.*))?$/);
201
+
202
+ return typeMatch?.[3] ?? "";
203
+ }
204
+
205
+ /**
206
+ * Format prose without changing its existing line wrapping.
207
+ *
208
+ * @param {string[]} lines
209
+ * The prose content lines.
210
+ * @param {boolean} addPunctuation
211
+ * Whether to format each paragraph as a sentence.
212
+ *
213
+ * @returns {string[]}
214
+ * The formatted prose lines.
215
+ */
216
+ function formatUnwrappedProse(lines, addPunctuation) {
217
+ // The formatted lines, built up in place.
218
+ const result = lines.map((line) => line.trim());
219
+
220
+ // The index of the current paragraph's first line, or null between paragraphs.
221
+ let paragraphStart = null;
222
+
223
+ for (let index = 0; index < result.length; index += 1) {
224
+ if (result[index] !== "") {
225
+ if (paragraphStart === null) {
226
+ paragraphStart = index;
227
+ }
228
+
229
+ continue;
230
+ }
231
+
232
+ if (paragraphStart !== null && addPunctuation) {
233
+ result[paragraphStart] = capitaliseSentence(result[paragraphStart]);
234
+ result[index - 1] = addTerminalPunctuation(result[index - 1]);
235
+ }
236
+
237
+ paragraphStart = null;
238
+ }
239
+
240
+ if (paragraphStart !== null && addPunctuation) {
241
+ result[paragraphStart] = capitaliseSentence(result[paragraphStart]);
242
+ result[result.length - 1] = addTerminalPunctuation(result[result.length - 1]);
243
+ }
244
+
245
+ return result;
246
+ }
247
+
248
+ /**
249
+ * Format prose paragraphs to the block-comment width.
250
+ *
251
+ * @param {string[]} lines
252
+ * The prose content lines.
253
+ * @param {number} width
254
+ * The available content width.
255
+ * @param {boolean} addPunctuation
256
+ * Whether to format each paragraph as a sentence.
257
+ *
258
+ * @returns {string[]}
259
+ * Formatted prose content lines.
260
+ */
261
+ function formatProse(lines, width, addPunctuation) {
262
+ // The formatted lines, built up in place.
263
+ const result = [];
264
+
265
+ // The prose lines collected for the paragraph in progress.
266
+ let paragraph = [];
267
+
268
+ /**
269
+ * Format the collected paragraph and append it to the result.
270
+ */
271
+ function flushParagraph() {
272
+ if (paragraph.length === 0) {
273
+ return;
274
+ }
275
+
276
+ // The paragraph text, punctuated as a sentence when requested.
277
+ let text = paragraph.join(" ").trim();
278
+
279
+ if (addPunctuation) {
280
+ text = formatSentence(text);
281
+ }
282
+
283
+ result.push(...wrapWords(text, width));
284
+ paragraph = [];
285
+ }
286
+
287
+ for (const line of lines) {
288
+ if (line.trim() === "") {
289
+ flushParagraph();
290
+ if (result.at(-1) !== "") {
291
+ result.push("");
292
+ }
293
+ } else {
294
+ paragraph.push(line.trim());
295
+ }
296
+ }
297
+
298
+ flushParagraph();
299
+
300
+ while (result.at(-1) === "") {
301
+ result.pop();
302
+ }
303
+
304
+ return result;
305
+ }
306
+
307
+ /**
308
+ * Format the target JSDoc tags, including their group order.
309
+ *
310
+ * @param {string[]} tagLines
311
+ * The undecorated tag content.
312
+ * @param {number} width
313
+ * The available description width.
314
+ * @param {boolean} addPunctuation
315
+ * Whether to format tag descriptions as sentences.
316
+ * @param {boolean} normaliseTags
317
+ * Whether to normalise tag spacing and group order.
318
+ *
319
+ * @returns {string[]}
320
+ * Formatted tag content lines.
321
+ */
322
+ function formatTags(tagLines, width, addPunctuation, normaliseTags) {
323
+ // The parsed @param/@throws/@returns entries.
324
+ const entries = parseTargetTags(tagLines);
325
+ // Whether the block also carries a tag this formatter doesn't normalise.
326
+ const hasUnknownTag = tagLines.some((line) => line.trim().startsWith("@") && !isTargetTag(line));
327
+
328
+ if (!normaliseTags || entries.length === 0) {
329
+ return formatTagDescriptions(tagLines, width, addPunctuation);
330
+ }
331
+
332
+ if (hasUnknownTag) {
333
+ return formatMixedTags(tagLines, width, addPunctuation);
334
+ }
335
+
336
+ // The formatted lines, built up in place.
337
+ const result = [];
338
+
339
+ // The tag type of the previously written entry, used to detect group changes.
340
+ let lastType = null;
341
+
342
+ // The entries regrouped into the required tag order.
343
+ const orderedEntries = tagOrder.flatMap((type) => entries.filter((entry) => entry.type === type));
344
+
345
+ for (const entry of orderedEntries) {
346
+ if (lastType !== null && lastType !== entry.type) {
347
+ result.push("");
348
+ }
349
+
350
+ result.push(formatTagHeader(entry));
351
+
352
+ // The entry's inline and multi-line description text, combined.
353
+ const description = [getInlineTagDescription(entry), ...entry.description]
354
+ .filter((line) => line.trim() !== "")
355
+ .map((line) => line.trim());
356
+
357
+ // The description text, punctuated as a sentence when requested.
358
+ let descriptionText = description.join(" ");
359
+
360
+ if (addPunctuation && descriptionText !== "") {
361
+ descriptionText = formatSentence(descriptionText);
362
+ }
363
+
364
+ if (descriptionText !== "") {
365
+ result.push(...wrapWords(descriptionText, width).map((line) => ` ${line}`));
366
+ }
367
+
368
+ lastType = entry.type;
369
+ }
370
+
371
+ return result;
372
+ }
373
+
374
+ /**
375
+ * Format target tags in place when a JSDoc block also has other tags.
376
+ *
377
+ * @param {string[]} lines
378
+ * The undecorated tag content lines.
379
+ * @param {number} width
380
+ * The available description width.
381
+ * @param {boolean} addPunctuation
382
+ * Whether to format descriptions as sentences.
383
+ *
384
+ * @returns {string[]}
385
+ * Formatted mixed tag content lines.
386
+ */
387
+ function formatMixedTags(lines, width, addPunctuation) {
388
+ // The formatted lines, built up in place.
389
+ const result = [];
390
+
391
+ // The tag entry currently collecting description lines.
392
+ let currentEntry = null;
393
+ // Whether the current tag's content is copied through unchanged.
394
+ let preserveSection = false;
395
+
396
+ for (const line of lines) {
397
+ // The tag name, or null when the line isn't a tag at all.
398
+ const tagName = getJSDocTagName(line);
399
+ // Matches a new @param/@throws/@returns tag line.
400
+ const match = line.trim().match(/^@(param|throws|returns)\b(.*)$/);
401
+
402
+ if (match) {
403
+ currentEntry = {
404
+ description: [],
405
+ rest: match[2].trim(),
406
+ type: match[1],
407
+ };
408
+ preserveSection = false;
409
+ result.push(formatTagHeader(currentEntry));
410
+ } else if (tagName !== null) {
411
+ currentEntry = null;
412
+ preserveSection = isPreservedSectionTag(line);
413
+ result.push(line.trim());
414
+ } else if (preserveSection) {
415
+ result.push(line);
416
+ } else if (line.trim() === "") {
417
+ currentEntry = null;
418
+ if (result.at(-1) !== "") {
419
+ result.push("");
420
+ }
421
+ } else {
422
+ // The description text, punctuated as a sentence when requested.
423
+ let text = line.trim();
424
+
425
+ if (addPunctuation && currentEntry) {
426
+ text = formatSentence(text);
427
+ }
428
+
429
+ result.push(...wrapWords(text, width).map((wrappedLine) => ` ${wrappedLine}`));
430
+ }
431
+ }
432
+
433
+ while (result.at(-1) === "") {
434
+ result.pop();
435
+ }
436
+
437
+ return result;
438
+ }
439
+
440
+ /**
441
+ * Format descriptions while retaining non-target tag lines.
442
+ *
443
+ * @param {string[]} lines
444
+ * The undecorated tag content lines.
445
+ * @param {number} width
446
+ * The available description width.
447
+ * @param {boolean} addPunctuation
448
+ * Whether to format descriptions as sentences.
449
+ *
450
+ * @returns {string[]}
451
+ * Formatted tag content lines.
452
+ */
453
+ function formatTagDescriptions(lines, width, addPunctuation) {
454
+ // The formatted lines, built up in place.
455
+ const result = [];
456
+
457
+ // The description lines collected for the tag in progress.
458
+ let description = [];
459
+ // Whether the current tag's content is copied through unchanged.
460
+ let preserveSection = false;
461
+
462
+ /**
463
+ * Format the collected description and append it to the result.
464
+ */
465
+ function flushDescription() {
466
+ if (description.length === 0) {
467
+ return;
468
+ }
469
+
470
+ // The description text, punctuated as a sentence when requested.
471
+ let text = description.join(" ").trim();
472
+
473
+ if (addPunctuation) {
474
+ text = formatSentence(text);
475
+ }
476
+
477
+ result.push(...wrapWords(text, width).map((line) => ` ${line}`));
478
+ description = [];
479
+ }
480
+
481
+ for (const line of lines) {
482
+ // The tag name, or null when the line isn't a tag at all.
483
+ const tagName = getJSDocTagName(line);
484
+
485
+ if (tagName !== null) {
486
+ flushDescription();
487
+ result.push(line.trim());
488
+ preserveSection = isPreservedSectionTag(line);
489
+ } else if (preserveSection) {
490
+ result.push(line);
491
+ } else if (line.trim() === "") {
492
+ flushDescription();
493
+ if (result.at(-1) !== "") {
494
+ result.push("");
495
+ }
496
+ } else {
497
+ description.push(line.trim());
498
+ }
499
+ }
500
+
501
+ flushDescription();
502
+
503
+ while (result.at(-1) === "") {
504
+ result.pop();
505
+ }
506
+
507
+ return result;
508
+ }
509
+
510
+ /**
511
+ * Return the details needed to format a JSDoc block comment.
512
+ *
513
+ * @param {object} sourceCode
514
+ * The Oxlint source code object.
515
+ * @param {object} comment
516
+ * The JSDoc comment token.
517
+ *
518
+ * @returns {object}
519
+ * The JSDoc content and layout details.
520
+ */
521
+ function getJSDocFormattingContext(sourceCode, comment) {
522
+ // The raw comment source text.
523
+ const commentText = getCommentText(sourceCode, comment);
524
+ // The indentation the comment's lines are aligned to.
525
+ const indent = getLineIndent(sourceCode, comment.range[0]) ?? "";
526
+ // The newline style used by the surrounding source.
527
+ const newline = getNewline(sourceCode.text);
528
+ // The available content width, allowing for the indent and " * " prefix.
529
+ const width = Math.max(1, 80 - indent.length - 3);
530
+ // The undecorated comment content lines.
531
+ const content = getJSDocContent(commentText);
532
+ // The content split into its prose and tag sections.
533
+ const { proseLines, tagLines } = splitJSDocContent(content);
534
+
535
+ return {
536
+ hasTagSeparator: proseLines.at(-1)?.trim() === "",
537
+ indent,
538
+ newline,
539
+ proseLines,
540
+ tagLines,
541
+ width,
542
+ };
543
+ }
544
+
545
+ /**
546
+ * Append tags after a blank line when the JSDoc format requires one.
547
+ *
548
+ * @param {string[]} outputLines
549
+ * The formatted prose lines.
550
+ * @param {string[]} tags
551
+ * The formatted tag lines.
552
+ */
553
+ function appendJSDocTags(outputLines, tags) {
554
+ if (tags.length === 0) {
555
+ return;
556
+ }
557
+
558
+ if (outputLines.length > 0 && outputLines.at(-1) !== "") {
559
+ outputLines.push("");
560
+ }
561
+
562
+ outputLines.push(...tags);
563
+ }
564
+
565
+ /**
566
+ * Append tags while retaining the source's existing separator state.
567
+ *
568
+ * @param {string[]} outputLines
569
+ * The formatted prose lines.
570
+ * @param {string[]} tags
571
+ * The formatted tag lines.
572
+ * @param {boolean} hasTagSeparator
573
+ * Whether the source had a prose-to-tag separator.
574
+ */
575
+ function appendJSDocTagsWithExistingSeparator(outputLines, tags, hasTagSeparator) {
576
+ if (tags.length === 0) {
577
+ return;
578
+ }
579
+
580
+ if (hasTagSeparator) {
581
+ appendJSDocTags(outputLines, tags);
582
+
583
+ return;
584
+ }
585
+
586
+ outputLines.push(...tags);
587
+ }
588
+
589
+ /**
590
+ * Render a JSDoc block comment from its formatted content.
591
+ *
592
+ * @param {object} formattingContext
593
+ * The JSDoc layout details.
594
+ * @param {string[]} outputLines
595
+ * The formatted prose and tag lines.
596
+ *
597
+ * @returns {string}
598
+ * The formatted comment text.
599
+ */
600
+ function renderJSDocComment(formattingContext, outputLines) {
601
+ // The comment's indent and newline style, from the formatting context.
602
+ const { indent, newline } = formattingContext;
603
+
604
+ return [
605
+ "/**",
606
+ ...outputLines.map((line) => (line === "" ? `${indent} *` : `${indent} * ${line}`)),
607
+ `${indent} */`,
608
+ ].join(newline);
609
+ }
610
+
611
+ /**
612
+ * Format JSDoc block structure and delimiters.
613
+ *
614
+ * @param {object} sourceCode
615
+ * The Oxlint source code object.
616
+ * @param {object} comment
617
+ * The JSDoc comment token.
618
+ *
619
+ * @returns {string}
620
+ * The formatted comment text.
621
+ */
622
+ export function formatJSDocBlockStructure(sourceCode, comment) {
623
+ // The comment's parsed content and layout details.
624
+ const formattingContext = getJSDocFormattingContext(sourceCode, comment);
625
+ // The prose lines, rewrapped without changing existing line breaks.
626
+ const prose = formatUnwrappedProse(formattingContext.proseLines, false);
627
+
628
+ // The tag lines, without spacing or grouping normalisation.
629
+ const tags = formatTags(
630
+ formattingContext.tagLines,
631
+ Math.max(1, formattingContext.width - 4),
632
+ false,
633
+ false,
634
+ );
635
+
636
+ // The formatted comment content, before tags are appended.
637
+ const outputLines = [...prose];
638
+
639
+ appendJSDocTags(outputLines, tags);
640
+
641
+ return renderJSDocComment(formattingContext, outputLines);
642
+ }
643
+
644
+ /**
645
+ * Format JSDoc tag spacing, order, and grouping.
646
+ *
647
+ * @param {object} sourceCode
648
+ * The Oxlint source code object.
649
+ * @param {object} comment
650
+ * The JSDoc comment token.
651
+ *
652
+ * @returns {string}
653
+ * The formatted comment text.
654
+ */
655
+ export function formatJSDocTagFormatting(sourceCode, comment) {
656
+ // The comment's parsed content and layout details.
657
+ const formattingContext = getJSDocFormattingContext(sourceCode, comment);
658
+ // The prose, rewrapped to the comment's available width.
659
+ const prose = formatProse(formattingContext.proseLines, formattingContext.width, false);
660
+
661
+ // The tag lines, with spacing and grouping normalised.
662
+ const tags = formatTags(
663
+ formattingContext.tagLines,
664
+ Math.max(1, formattingContext.width - 4),
665
+ false,
666
+ true,
667
+ );
668
+
669
+ // The formatted comment content, before tags are appended.
670
+ const outputLines = [...prose];
671
+
672
+ appendJSDocTagsWithExistingSeparator(outputLines, tags, formattingContext.hasTagSeparator);
673
+
674
+ return renderJSDocComment(formattingContext, outputLines);
675
+ }
676
+
677
+ /**
678
+ * Format JSDoc sentence capitalisation and punctuation.
679
+ *
680
+ * @param {object} sourceCode
681
+ * The Oxlint source code object.
682
+ * @param {object} comment
683
+ * The JSDoc comment token.
684
+ *
685
+ * @returns {string}
686
+ * The formatted comment text.
687
+ */
688
+ export function formatJSDocPunctuation(sourceCode, comment) {
689
+ // The comment's parsed content and layout details.
690
+ const formattingContext = getJSDocFormattingContext(sourceCode, comment);
691
+ // The prose, capitalised and punctuated as sentences.
692
+ const prose = formatUnwrappedProse(formattingContext.proseLines, true);
693
+
694
+ // The tag lines, with descriptions punctuated as sentences.
695
+ const tags = formatTags(
696
+ formattingContext.tagLines,
697
+ Math.max(1, formattingContext.width - 4),
698
+ true,
699
+ false,
700
+ );
701
+
702
+ // The formatted comment content, before tags are appended.
703
+ const outputLines = [...prose];
704
+
705
+ appendJSDocTagsWithExistingSeparator(outputLines, tags, formattingContext.hasTagSeparator);
706
+
707
+ return renderJSDocComment(formattingContext, outputLines);
708
+ }
709
+
710
+ /**
711
+ * Format JSDoc prose and tags to the configured line width.
712
+ *
713
+ * @param {object} sourceCode
714
+ * The Oxlint source code object.
715
+ * @param {object} comment
716
+ * The JSDoc comment token.
717
+ *
718
+ * @returns {string}
719
+ * The formatted comment text.
720
+ */
721
+ export function formatJSDocWrapping(sourceCode, comment) {
722
+ // The comment's parsed content and layout details.
723
+ const formattingContext = getJSDocFormattingContext(sourceCode, comment);
724
+ // The prose, rewrapped to the comment's available width.
725
+ const prose = formatProse(formattingContext.proseLines, formattingContext.width, false);
726
+
727
+ // The tag lines, without spacing or grouping normalisation.
728
+ const tags = formatTags(
729
+ formattingContext.tagLines,
730
+ Math.max(1, formattingContext.width - 4),
731
+ false,
732
+ false,
733
+ );
734
+
735
+ // The formatted comment content, before tags are appended.
736
+ const outputLines = [...prose];
737
+
738
+ appendJSDocTags(outputLines, tags);
739
+
740
+ return renderJSDocComment(formattingContext, outputLines);
741
+ }
742
+
743
+ /**
744
+ * Return whether a JSDoc comment contains a target tag.
745
+ *
746
+ * @param {object} sourceCode
747
+ * The Oxlint source code object.
748
+ * @param {object} comment
749
+ * The comment token.
750
+ *
751
+ * @returns {boolean}
752
+ * Whether a Phase 1 tag is present.
753
+ */
754
+ export function hasTargetJSDocTag(sourceCode, comment) {
755
+ return getJSDocContent(getCommentText(sourceCode, comment)).some(isTargetTag);
756
+ }