@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.
- package/README.md +84 -63
- package/base.json +57 -51
- package/comments/plugin.js +30 -0
- package/comments/rules/block-comments.js +70 -0
- package/comments/rules/configured-api-calls.js +137 -0
- package/comments/rules/function-documentation.js +111 -0
- package/comments/rules/jsdoc-tag-formatting.js +70 -0
- package/comments/rules/line-comments.js +86 -0
- package/comments/rules/max-line-length.js +159 -0
- package/comments/rules/placement.js +295 -0
- package/comments/rules/sentence-punctuation.js +275 -0
- package/comments/rules/variable-declarations.js +71 -0
- package/comments/rules/vue-component-documentation.js +169 -0
- package/comments/rules/vue-emit-documentation.js +123 -0
- package/comments/rules/vue-prop-documentation.js +224 -0
- package/comments/utils/documentation.js +321 -0
- package/comments/utils/jsdoc.js +756 -0
- package/comments/utils/source.js +312 -0
- package/comments/utils/vue-macro.js +70 -0
- package/comments/utils/wrap.js +118 -0
- package/comments.json +22 -0
- package/package.json +11 -3
|
@@ -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
|
+
}
|