@lewishowles/lint-config 0.4.0 → 0.6.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/CHANGELOG.md +23 -0
- package/README.md +38 -6
- package/base.json +23 -4
- package/comments/plugin.js +2 -12
- package/comments/rules/class-documentation.js +8 -5
- package/comments/rules/configured-api-calls.js +3 -3
- package/comments/rules/formatting.js +868 -0
- package/comments/rules/variable-declarations.js +41 -4
- package/comments/rules/vue-component-documentation.js +10 -8
- package/comments/rules/vue-emit-documentation.js +2 -1
- package/comments/utils/documentation.js +0 -1
- package/comments/utils/jsdoc.js +84 -63
- package/comments/utils/source.js +15 -2
- package/comments/utils/wrap.js +123 -2
- package/comments.json +10 -7
- package/package.json +3 -2
- package/comments/rules/block-comments.js +0 -70
- package/comments/rules/jsdoc-tag-formatting.js +0 -70
- package/comments/rules/line-comments.js +0 -86
- package/comments/rules/max-line-length.js +0 -159
- package/comments/rules/placement.js +0 -290
- package/comments/rules/sentence-punctuation.js +0 -275
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatJSDocBlockStructure,
|
|
3
|
+
formatJSDocPunctuation,
|
|
4
|
+
formatJSDocTagFormatting,
|
|
5
|
+
formatJSDocWrapping,
|
|
6
|
+
hasTargetJSDocTag,
|
|
7
|
+
isJSDoc,
|
|
8
|
+
} from "../utils/jsdoc.js";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getCommentNeighbours,
|
|
12
|
+
getCommentText,
|
|
13
|
+
getDisplayWidth,
|
|
14
|
+
getLineCommentGroups,
|
|
15
|
+
getLineIndent,
|
|
16
|
+
getLineStart,
|
|
17
|
+
getNewline,
|
|
18
|
+
isDirectiveComment,
|
|
19
|
+
isLeadingComment,
|
|
20
|
+
replaceMinimalComment,
|
|
21
|
+
} from "../utils/source.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
addTerminalPunctuation,
|
|
25
|
+
capitaliseSentence,
|
|
26
|
+
formatSentence,
|
|
27
|
+
refillCommentLines,
|
|
28
|
+
wrapWords,
|
|
29
|
+
} from "../utils/wrap.js";
|
|
30
|
+
|
|
31
|
+
// The line length this rule wraps comments to.
|
|
32
|
+
const maximumLineLength = 80;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return a line comment's source text with a new value after the `//`.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} sourceCode
|
|
38
|
+
* The Oxlint source code object.
|
|
39
|
+
* @param {object} comment
|
|
40
|
+
* The line comment token.
|
|
41
|
+
* @param {string} value
|
|
42
|
+
* The replacement comment value.
|
|
43
|
+
*
|
|
44
|
+
* @returns {string}
|
|
45
|
+
* The replacement comment text.
|
|
46
|
+
*/
|
|
47
|
+
function replaceLineCommentValue(sourceCode, comment, value) {
|
|
48
|
+
// The comment's raw source text.
|
|
49
|
+
const commentText = getCommentText(sourceCode, comment);
|
|
50
|
+
|
|
51
|
+
return `${commentText.slice(0, 2)}${value}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wrap a line comment to the configured maximum width.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} sourceCode
|
|
58
|
+
* The Oxlint source code object.
|
|
59
|
+
* @param {object} comment
|
|
60
|
+
* The line comment token.
|
|
61
|
+
* @param {string} indentation
|
|
62
|
+
* The indentation shared by the comment group.
|
|
63
|
+
* @param {string} commentText
|
|
64
|
+
* The comment text to wrap.
|
|
65
|
+
*
|
|
66
|
+
* @returns {string|null}
|
|
67
|
+
* The wrapped comment without leading indentation, or null when it is a
|
|
68
|
+
* directive.
|
|
69
|
+
*/
|
|
70
|
+
function formatLineComment(sourceCode, comment, indentation, commentText) {
|
|
71
|
+
// The available width, allowing for the indent and "// " prefix.
|
|
72
|
+
const width = maximumLineLength - getDisplayWidth(indentation) - 3;
|
|
73
|
+
// The comment's undecorated text.
|
|
74
|
+
const text = commentText.slice(2).trim();
|
|
75
|
+
|
|
76
|
+
if (isDirectiveComment(comment)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (text === "") {
|
|
81
|
+
return "//";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return wrapWords(text, Math.max(1, width))
|
|
85
|
+
.map((line) => `// ${line}`)
|
|
86
|
+
.join(getNewline(sourceCode.text));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Return the part of a line's indentation that goes beyond the comment's own
|
|
91
|
+
* indentation, so nested lines keep their offset when the comment moves.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} indentation
|
|
94
|
+
* The indentation of one line inside the comment.
|
|
95
|
+
* @param {string} commentIndent
|
|
96
|
+
* The indentation of the comment's first line.
|
|
97
|
+
*
|
|
98
|
+
* @returns {string}
|
|
99
|
+
* The extra indentation, or an empty string when the line does not start
|
|
100
|
+
* with the comment's indentation.
|
|
101
|
+
*/
|
|
102
|
+
function getRelativeIndent(indentation, commentIndent) {
|
|
103
|
+
return indentation.startsWith(commentIndent) ? indentation.slice(commentIndent.length) : "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reindent a formatted leading comment while preserving inner indentation.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} commentText
|
|
110
|
+
* The formatted comment source text.
|
|
111
|
+
* @param {string} commentIndent
|
|
112
|
+
* The comment's current indentation.
|
|
113
|
+
* @param {string} expectedIndent
|
|
114
|
+
* The documented code's indentation.
|
|
115
|
+
* @param {string} newline
|
|
116
|
+
* The source file's newline sequence.
|
|
117
|
+
*
|
|
118
|
+
* @returns {string}
|
|
119
|
+
* The reindented comment with its leading indentation.
|
|
120
|
+
*/
|
|
121
|
+
function getReindentedCommentText(commentText, commentIndent, expectedIndent, newline) {
|
|
122
|
+
return commentText
|
|
123
|
+
.split(/\r\n|\n|\r/)
|
|
124
|
+
.map((line, lineIndex) => {
|
|
125
|
+
if (lineIndex === 0) {
|
|
126
|
+
return `${expectedIndent}${line}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The line's current indentation.
|
|
130
|
+
const lineIndent = line.match(/^[ \t]*/)[0];
|
|
131
|
+
// The indentation to preserve relative to the comment's own indent.
|
|
132
|
+
const relativeIndent = getRelativeIndent(lineIndent, commentIndent);
|
|
133
|
+
|
|
134
|
+
return `${expectedIndent}${relativeIndent}${line.slice(lineIndent.length)}`;
|
|
135
|
+
})
|
|
136
|
+
.join(newline);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply a prose formatter to a block-comment line, keeping its leading `*`.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} line
|
|
143
|
+
* The block-comment line.
|
|
144
|
+
* @param {function} formatProse
|
|
145
|
+
* The formatter for the line's prose.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string}
|
|
148
|
+
* The formatted block-comment line.
|
|
149
|
+
*/
|
|
150
|
+
function formatBlockCommentLine(line, formatProse) {
|
|
151
|
+
// The line's leading `*` decoration, when present.
|
|
152
|
+
const marker = line.match(/^\s*\*\s*/);
|
|
153
|
+
|
|
154
|
+
if (marker === null) {
|
|
155
|
+
return line;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return `${marker[0]}${formatProse(line.slice(marker[0].length).trim())}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format prose in an ordinary block comment as complete sentences.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} sourceCode
|
|
165
|
+
* The Oxlint source code object.
|
|
166
|
+
* @param {object} comment
|
|
167
|
+
* The block comment token.
|
|
168
|
+
*
|
|
169
|
+
* @returns {string}
|
|
170
|
+
* The sentence-formatted comment text.
|
|
171
|
+
*/
|
|
172
|
+
function formatOrdinaryBlockComment(sourceCode, comment) {
|
|
173
|
+
// The comment's raw source text.
|
|
174
|
+
const commentText = getCommentText(sourceCode, comment);
|
|
175
|
+
// The indentation the comment's lines are aligned to.
|
|
176
|
+
const indentation = getLineIndent(sourceCode, comment.range[0]);
|
|
177
|
+
// The comment body, stripped of its /* */ delimiters.
|
|
178
|
+
const content = commentText.slice(2, -2).trim();
|
|
179
|
+
|
|
180
|
+
if (indentation === null || content === "" || isDirectiveComment(comment)) {
|
|
181
|
+
return commentText;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!commentText.includes("\n") && !commentText.includes("\r")) {
|
|
185
|
+
return `/* ${formatSentence(content)} */`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The comment's individual source lines.
|
|
189
|
+
const lines = commentText.split(/\r\n|\n|\r/);
|
|
190
|
+
|
|
191
|
+
// The indexes of lines carrying prose, excluding the delimiter lines.
|
|
192
|
+
const proseLineIndexes = lines
|
|
193
|
+
.slice(1, -1)
|
|
194
|
+
.map((line, index) => ({ index: index + 1, text: line.replace(/^\s*\*?\s?/, "").trim() }))
|
|
195
|
+
.filter((line) => line.text !== "")
|
|
196
|
+
.map((line) => line.index);
|
|
197
|
+
|
|
198
|
+
if (lines[0] === "/*" && lines.at(-1).trim() === "*/" && proseLineIndexes.length > 0) {
|
|
199
|
+
// The comment lines, formatted in place.
|
|
200
|
+
const formattedLines = [...lines];
|
|
201
|
+
// The first prose line index, which starts the sentence.
|
|
202
|
+
const firstProseLine = proseLineIndexes[0];
|
|
203
|
+
// The last prose line index, which ends the sentence.
|
|
204
|
+
const lastProseLine = proseLineIndexes.at(-1);
|
|
205
|
+
|
|
206
|
+
formattedLines[firstProseLine] = formatBlockCommentLine(
|
|
207
|
+
formattedLines[firstProseLine],
|
|
208
|
+
capitaliseSentence,
|
|
209
|
+
);
|
|
210
|
+
formattedLines[lastProseLine] = formatBlockCommentLine(
|
|
211
|
+
formattedLines[lastProseLine],
|
|
212
|
+
addTerminalPunctuation,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return formattedLines.join(getNewline(sourceCode.text));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The comment's prose, joined into a single paragraph.
|
|
219
|
+
const paragraphs = content
|
|
220
|
+
.split(/\r\n|\n|\r/)
|
|
221
|
+
.map((line) => line.replace(/^\s*\*?\s?/, "").trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(" ");
|
|
224
|
+
|
|
225
|
+
return ["/*", `${indentation} * ${formatSentence(paragraphs)}`, `${indentation} */`].join(
|
|
226
|
+
getNewline(sourceCode.text),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Wrap an ordinary block comment to the configured maximum width.
|
|
232
|
+
*
|
|
233
|
+
* @param {object} sourceCode
|
|
234
|
+
* The Oxlint source code object.
|
|
235
|
+
* @param {string} commentText
|
|
236
|
+
* The sentence-formatted comment text to wrap.
|
|
237
|
+
* @param {string|null} indentation
|
|
238
|
+
* The indentation used by the wrapped comment.
|
|
239
|
+
*
|
|
240
|
+
* @returns {string|null}
|
|
241
|
+
* The wrapped comment, or null when it is not a standalone comment.
|
|
242
|
+
*/
|
|
243
|
+
function formatBlockComment(sourceCode, commentText, indentation) {
|
|
244
|
+
if (indentation === null) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The comment body, without its delimiters or line markers.
|
|
249
|
+
const text = commentText
|
|
250
|
+
.slice(2, -2)
|
|
251
|
+
.split(/\r\n|\n|\r/)
|
|
252
|
+
.map((line) => line.replace(/^\s*\*?\s?/, "").trim())
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join(" ");
|
|
255
|
+
|
|
256
|
+
// The available width, allowing for the indent and " * " prefix.
|
|
257
|
+
const width = maximumLineLength - getDisplayWidth(indentation) - 3;
|
|
258
|
+
// The comment body, rewrapped to the available width.
|
|
259
|
+
const lines = wrapWords(text, Math.max(1, width));
|
|
260
|
+
|
|
261
|
+
return ["/*", ...lines.map((line) => `${indentation} * ${line}`), `${indentation} */`].join(
|
|
262
|
+
getNewline(sourceCode.text),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Reindent and refill an ordinary block comment.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} commentText
|
|
270
|
+
* The sentence-formatted comment text.
|
|
271
|
+
* @param {string} indentation
|
|
272
|
+
* The comment's current indentation.
|
|
273
|
+
* @param {string} expectedIndent
|
|
274
|
+
* The indentation used by the formatted comment.
|
|
275
|
+
* @param {string} newline
|
|
276
|
+
* The source file's newline sequence.
|
|
277
|
+
*
|
|
278
|
+
* @returns {string}
|
|
279
|
+
* The refilled comment text.
|
|
280
|
+
*/
|
|
281
|
+
function refillBlockComment(commentText, indentation, expectedIndent, newline) {
|
|
282
|
+
if (!commentText.includes("\n") && !commentText.includes("\r")) {
|
|
283
|
+
return commentText;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// The comment after applying the indentation used to measure its width.
|
|
287
|
+
const reindentedComment = getReindentedCommentText(
|
|
288
|
+
commentText,
|
|
289
|
+
indentation,
|
|
290
|
+
expectedIndent,
|
|
291
|
+
newline,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// The comment's individual source lines, without the first line's outer
|
|
295
|
+
// indent.
|
|
296
|
+
const lines = reindentedComment.split(/\r\n|\n|\r/);
|
|
297
|
+
|
|
298
|
+
lines[0] = lines[0].slice(expectedIndent.length);
|
|
299
|
+
|
|
300
|
+
if (lines[0] !== "/*" || lines.at(-1).trim() !== "*/") {
|
|
301
|
+
return commentText;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The comment prose lines with their existing display prefixes.
|
|
305
|
+
const proseLines = lines.slice(1, -1).map((line) => {
|
|
306
|
+
// The line's `*` decoration and the indentation around it.
|
|
307
|
+
const prefix = line.match(/^\s*\*\s*/)?.[0] ?? "";
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
prefix,
|
|
311
|
+
text: line.slice(prefix.length),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// The prose after moving words from early-wrapped lines.
|
|
316
|
+
const refilledLines = refillCommentLines(proseLines, maximumLineLength);
|
|
317
|
+
|
|
318
|
+
return [
|
|
319
|
+
"/*",
|
|
320
|
+
...refilledLines.map(({ prefix, text }) => `${prefix}${text}`.trimEnd()),
|
|
321
|
+
`${expectedIndent} */`,
|
|
322
|
+
].join(newline);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Return the display lines for a formatted block comment.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} commentText
|
|
329
|
+
* The formatted comment text.
|
|
330
|
+
* @param {string} indentation
|
|
331
|
+
* The indentation used to measure the comment.
|
|
332
|
+
*
|
|
333
|
+
* @returns {string[]}
|
|
334
|
+
* The comment lines as they appear on screen.
|
|
335
|
+
*/
|
|
336
|
+
function getBlockCommentDisplayLines(commentText, indentation) {
|
|
337
|
+
// The comment's individual source lines.
|
|
338
|
+
const lines = commentText.split(/\r\n|\n|\r/);
|
|
339
|
+
|
|
340
|
+
return [`${indentation}${lines[0]}`, ...lines.slice(1)];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Work out where a comment above code should sit: the code's indentation, and
|
|
345
|
+
* the end of the gap to replace so exactly one line break separates them.
|
|
346
|
+
*
|
|
347
|
+
* The gap stops before a directive comment between the comment and its code, so
|
|
348
|
+
* the fix never edits the directive. When an ordinary comment sits between them
|
|
349
|
+
* instead, only the indentation is fixed and the gap is left alone.
|
|
350
|
+
*
|
|
351
|
+
* @param {object} sourceCode
|
|
352
|
+
* The Oxlint source code object.
|
|
353
|
+
* @param {object} comment
|
|
354
|
+
* The first comment in the formatted unit.
|
|
355
|
+
* @param {object} lastComment
|
|
356
|
+
* The last comment in the formatted unit.
|
|
357
|
+
* @param {object[]} comments
|
|
358
|
+
* Every comment token in source order.
|
|
359
|
+
*
|
|
360
|
+
* @returns {object|null}
|
|
361
|
+
* Placement details with `actualIndent`, `changed`, `expectedIndent`,
|
|
362
|
+
* `gap`, and `rangeEnd`; or null when the comment does not sit above code.
|
|
363
|
+
*/
|
|
364
|
+
function getLeadingCommentPlacement(sourceCode, comment, lastComment, comments) {
|
|
365
|
+
// The code token the comment documents, and the token before the comment.
|
|
366
|
+
const { next, previous } = getCommentNeighbours(sourceCode, comment);
|
|
367
|
+
|
|
368
|
+
if (next === null || !isLeadingComment(sourceCode, comment, previous)) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The indentation required by the documented source token.
|
|
373
|
+
const expectedIndent = getLineIndent(sourceCode, next.range[0]);
|
|
374
|
+
// The comment's current indentation.
|
|
375
|
+
const actualIndent = getLineIndent(sourceCode, comment.range[0]);
|
|
376
|
+
|
|
377
|
+
if (expectedIndent === null || actualIndent === null) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// The next comment after this unit, when one exists.
|
|
382
|
+
const followingComment = comments.find((candidate) => candidate.range[0] > lastComment.range[1]);
|
|
383
|
+
|
|
384
|
+
// Whether another comment sits between this one and its documented code.
|
|
385
|
+
const followingCommentIntervenes =
|
|
386
|
+
followingComment !== undefined && followingComment.range[0] <= next.range[0];
|
|
387
|
+
|
|
388
|
+
if (followingCommentIntervenes && !isDirectiveComment(followingComment)) {
|
|
389
|
+
return {
|
|
390
|
+
actualIndent,
|
|
391
|
+
changed: actualIndent !== expectedIndent,
|
|
392
|
+
expectedIndent,
|
|
393
|
+
gap: "",
|
|
394
|
+
rangeEnd: lastComment.range[1],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Stop before an intervening directive so the replacement never overlaps
|
|
399
|
+
// it.
|
|
400
|
+
const rangeEnd = followingCommentIntervenes ? followingComment.range[0] : next.range[0];
|
|
401
|
+
// The source gap after the final comment, up to the code or directive.
|
|
402
|
+
const sourceGap = sourceCode.text.slice(lastComment.range[1], rangeEnd);
|
|
403
|
+
// The gap the documented code's indentation requires.
|
|
404
|
+
const gap = `${getNewline(sourceCode.text)}${expectedIndent}`;
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
actualIndent,
|
|
408
|
+
changed: actualIndent !== expectedIndent || sourceGap !== gap,
|
|
409
|
+
expectedIndent,
|
|
410
|
+
gap,
|
|
411
|
+
rangeEnd,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Return the diagnostic message for a comment formatting report.
|
|
417
|
+
*
|
|
418
|
+
* @param {boolean} sentenceChanged
|
|
419
|
+
* Whether the comment's sentence punctuation needs changing.
|
|
420
|
+
* @param {boolean} placementChanged
|
|
421
|
+
* Whether the comment's placement needs changing.
|
|
422
|
+
*
|
|
423
|
+
* @returns {string}
|
|
424
|
+
* The diagnostic message.
|
|
425
|
+
*/
|
|
426
|
+
function getReportMessage(sentenceChanged, placementChanged) {
|
|
427
|
+
if (sentenceChanged) {
|
|
428
|
+
return "Comment text must be a complete sentence.";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (placementChanged) {
|
|
432
|
+
return "Comment must be immediately before the documented code.";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return "Format this comment.";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Report line comments that trail code on their source line.
|
|
440
|
+
*
|
|
441
|
+
* @param {object} context
|
|
442
|
+
* The Oxlint rule context.
|
|
443
|
+
*/
|
|
444
|
+
function reportTrailingLineComments(context) {
|
|
445
|
+
// Every comment in the file, used to find trailing line comments.
|
|
446
|
+
const comments = context.sourceCode.getAllComments();
|
|
447
|
+
|
|
448
|
+
for (const comment of comments) {
|
|
449
|
+
if (
|
|
450
|
+
comment.type !== "Line" ||
|
|
451
|
+
isDirectiveComment(comment) ||
|
|
452
|
+
getLineIndent(context.sourceCode, comment.range[0]) !== null
|
|
453
|
+
) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// The source line where the trailing comment begins.
|
|
458
|
+
const commentLineStart = getLineStart(context.sourceCode, comment.range[0]);
|
|
459
|
+
// The text before the comment on its source line.
|
|
460
|
+
const linePrefix = context.sourceCode.text.slice(commentLineStart, comment.range[0]);
|
|
461
|
+
// The indentation shared by the code and the moved comment line.
|
|
462
|
+
const lineIndent = linePrefix.match(/^[ \t]*/)[0];
|
|
463
|
+
// Whether the comment gets sentence formatting; empty comments and
|
|
464
|
+
// comments that start with a tag are left as written.
|
|
465
|
+
const formatPunctuation = comment.value.trim() !== "" && !comment.value.trim().startsWith("@");
|
|
466
|
+
// The comment's value after sentence formatting.
|
|
467
|
+
const formattedValue = formatPunctuation ? formatSentence(comment.value) : comment.value;
|
|
468
|
+
// The comment source text after sentence formatting.
|
|
469
|
+
const commentText = replaceLineCommentValue(context.sourceCode, comment, formattedValue);
|
|
470
|
+
// The comment text with the line's indentation for width measurement.
|
|
471
|
+
const reindentedText = `${lineIndent}${commentText}`;
|
|
472
|
+
|
|
473
|
+
// The comment text after wrapping it to the line's available width.
|
|
474
|
+
const wrappedComment =
|
|
475
|
+
getDisplayWidth(reindentedText) > maximumLineLength
|
|
476
|
+
? (formatLineComment(context.sourceCode, comment, lineIndent, commentText) ?? commentText)
|
|
477
|
+
: commentText;
|
|
478
|
+
|
|
479
|
+
// The formatted comment with indentation on every wrapped line.
|
|
480
|
+
const formattedComment = wrappedComment
|
|
481
|
+
.split(getNewline(context.sourceCode.text))
|
|
482
|
+
.map((line) => `${lineIndent}${line}`)
|
|
483
|
+
.join(getNewline(context.sourceCode.text));
|
|
484
|
+
|
|
485
|
+
// Whether sentence punctuation or capitalisation changed the comment.
|
|
486
|
+
const sentenceChanged = formattedValue !== comment.value;
|
|
487
|
+
|
|
488
|
+
context.report({
|
|
489
|
+
fix: getTrailingCommentFix(
|
|
490
|
+
context.sourceCode,
|
|
491
|
+
comment,
|
|
492
|
+
formattedComment,
|
|
493
|
+
commentLineStart,
|
|
494
|
+
linePrefix,
|
|
495
|
+
),
|
|
496
|
+
message: sentenceChanged
|
|
497
|
+
? "Comment text must be a complete sentence."
|
|
498
|
+
: "Line comments must be on their own line.",
|
|
499
|
+
node: comment,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Move a trailing line comment above its source line.
|
|
506
|
+
*
|
|
507
|
+
* @param {object} sourceCode
|
|
508
|
+
* The Oxlint source code object.
|
|
509
|
+
* @param {object} comment
|
|
510
|
+
* The trailing line comment.
|
|
511
|
+
* @param {string} formattedComment
|
|
512
|
+
* The formatted comment text, including line indentation.
|
|
513
|
+
* @param {number} commentLineStart
|
|
514
|
+
* The offset at which the comment's source line starts.
|
|
515
|
+
* @param {string} linePrefix
|
|
516
|
+
* The source text before the comment on its line.
|
|
517
|
+
*
|
|
518
|
+
* @returns {function}
|
|
519
|
+
* A fixer callback that inserts the comment above its source line and
|
|
520
|
+
* removes it from the code line.
|
|
521
|
+
*/
|
|
522
|
+
function getTrailingCommentFix(
|
|
523
|
+
sourceCode,
|
|
524
|
+
comment,
|
|
525
|
+
formattedComment,
|
|
526
|
+
commentLineStart,
|
|
527
|
+
linePrefix,
|
|
528
|
+
) {
|
|
529
|
+
// The whitespace separating the code from the comment.
|
|
530
|
+
const trailingWhitespace = linePrefix.match(/[ \t]*$/)[0];
|
|
531
|
+
// The first character removed after the code on the comment's line.
|
|
532
|
+
const removalStart = comment.range[0] - trailingWhitespace.length;
|
|
533
|
+
// The newline sequence used by the source file.
|
|
534
|
+
const newline = getNewline(sourceCode.text);
|
|
535
|
+
|
|
536
|
+
return (fixer) => [
|
|
537
|
+
fixer.replaceTextRange([commentLineStart, commentLineStart], `${formattedComment}${newline}`),
|
|
538
|
+
fixer.removeRange([removalStart, comment.range[1]]),
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Report line-comment groups that need punctuation, reindentation or wrapping.
|
|
544
|
+
*
|
|
545
|
+
* @param {object} context
|
|
546
|
+
* The Oxlint rule context.
|
|
547
|
+
*/
|
|
548
|
+
function reportLineCommentGroups(context) {
|
|
549
|
+
// Every comment in the file, used to find what follows each group.
|
|
550
|
+
const comments = context.sourceCode.getAllComments();
|
|
551
|
+
|
|
552
|
+
// Groups without comments that trail code, because those comments have
|
|
553
|
+
// their own placement fix.
|
|
554
|
+
const standaloneCommentGroups = getLineCommentGroups(context.sourceCode).map((commentGroup) =>
|
|
555
|
+
commentGroup.filter((comment) => getLineIndent(context.sourceCode, comment.range[0]) !== null),
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
for (const commentGroup of standaloneCommentGroups) {
|
|
559
|
+
if (commentGroup.length === 0) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// The comment whose indentation the rest of the group follows.
|
|
564
|
+
const firstStandaloneComment = commentGroup[0];
|
|
565
|
+
// The indentation applied to every standalone comment in the group.
|
|
566
|
+
const firstIndent = getLineIndent(context.sourceCode, firstStandaloneComment.range[0]);
|
|
567
|
+
|
|
568
|
+
// The placement of a leading group, when it documents the next token.
|
|
569
|
+
const placement = getLeadingCommentPlacement(
|
|
570
|
+
context.sourceCode,
|
|
571
|
+
firstStandaloneComment,
|
|
572
|
+
commentGroup.at(-1),
|
|
573
|
+
comments,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// The indentation applied to the comment group's replacement.
|
|
577
|
+
const expectedIndent = placement?.expectedIndent ?? firstIndent;
|
|
578
|
+
|
|
579
|
+
// A stand-in for a comment token: replaceMinimalComment only reads its
|
|
580
|
+
// range, and the range spans every standalone comment plus its
|
|
581
|
+
// indentation.
|
|
582
|
+
const groupToken = {
|
|
583
|
+
range: [
|
|
584
|
+
getLineStart(context.sourceCode, firstStandaloneComment.range[0]),
|
|
585
|
+
placement?.rangeEnd ?? commentGroup.at(-1).range[1],
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// The group's current source text.
|
|
590
|
+
const sourceText = context.sourceCode.text.slice(...groupToken.range);
|
|
591
|
+
// The first comment's undecorated text.
|
|
592
|
+
const firstText = commentGroup[0].value.trim();
|
|
593
|
+
// Whether sentence punctuation applies to this group.
|
|
594
|
+
const formatPunctuation = firstText !== "" && !firstText.startsWith("@");
|
|
595
|
+
|
|
596
|
+
// The first comment's value, formatted as a sentence when needed.
|
|
597
|
+
let firstValue = commentGroup[0].value;
|
|
598
|
+
// The last comment's value, given a full stop when punctuation applies.
|
|
599
|
+
let lastValue = commentGroup.at(-1).value;
|
|
600
|
+
|
|
601
|
+
if (formatPunctuation) {
|
|
602
|
+
if (commentGroup.length === 1) {
|
|
603
|
+
firstValue = formatSentence(firstValue);
|
|
604
|
+
lastValue = firstValue;
|
|
605
|
+
} else {
|
|
606
|
+
firstValue = capitaliseSentence(firstValue);
|
|
607
|
+
lastValue = addTerminalPunctuation(lastValue);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Whether sentence punctuation changes the group.
|
|
612
|
+
const sentenceChanged =
|
|
613
|
+
formatPunctuation &&
|
|
614
|
+
(firstValue !== commentGroup[0].value || lastValue !== commentGroup.at(-1).value);
|
|
615
|
+
|
|
616
|
+
// The group's lines after punctuation, reindentation, and line
|
|
617
|
+
// wrapping.
|
|
618
|
+
const formattedLines = commentGroup.flatMap((comment, index) => {
|
|
619
|
+
// The comment's value, using the group's first and last values.
|
|
620
|
+
let value = comment.value;
|
|
621
|
+
|
|
622
|
+
if (index === 0) {
|
|
623
|
+
value = firstValue;
|
|
624
|
+
} else if (index === commentGroup.length - 1) {
|
|
625
|
+
value = lastValue;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// The comment's source text after sentence formatting.
|
|
629
|
+
const commentText = replaceLineCommentValue(context.sourceCode, comment, value);
|
|
630
|
+
// The comment's current indentation, falling back to the leader's.
|
|
631
|
+
const commentIndent = getLineIndent(context.sourceCode, comment.range[0]) ?? firstIndent;
|
|
632
|
+
// The comment's extra indentation beyond the group leader's.
|
|
633
|
+
const relativeIndent = getRelativeIndent(commentIndent, firstIndent);
|
|
634
|
+
// The indentation this comment gets once the group is moved.
|
|
635
|
+
const commentExpectedIndent = expectedIndent + relativeIndent;
|
|
636
|
+
// The comment's text after applying the group's indentation.
|
|
637
|
+
const reindentedText = `${commentExpectedIndent}${commentText}`;
|
|
638
|
+
|
|
639
|
+
// The wrapped comment text, when the reindented line exceeds the
|
|
640
|
+
// limit.
|
|
641
|
+
const formattedComment =
|
|
642
|
+
getDisplayWidth(reindentedText) > maximumLineLength
|
|
643
|
+
? (formatLineComment(context.sourceCode, comment, commentExpectedIndent, commentText) ??
|
|
644
|
+
commentText)
|
|
645
|
+
: commentText;
|
|
646
|
+
|
|
647
|
+
return formattedComment.split(getNewline(context.sourceCode.text)).map((line) => {
|
|
648
|
+
// The line without its `//` marker.
|
|
649
|
+
const lineText = line.slice(2);
|
|
650
|
+
// The line's leading whitespace after the comment marker.
|
|
651
|
+
const leadingWhitespace = lineText.match(/^\s*/)[0];
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
prefix: `${commentExpectedIndent}//${leadingWhitespace}`,
|
|
655
|
+
text: lineText.slice(leadingWhitespace.length).trimEnd(),
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// The group's lines after refilling words from early-wrapped lines.
|
|
661
|
+
const refilledLines = refillCommentLines(formattedLines, maximumLineLength);
|
|
662
|
+
|
|
663
|
+
// The group's text after applying its final line formatting and
|
|
664
|
+
// placement gap.
|
|
665
|
+
const formattedText =
|
|
666
|
+
refilledLines
|
|
667
|
+
.map(({ prefix, text }) => (text === "" ? prefix.trimEnd() : `${prefix}${text}`))
|
|
668
|
+
.join(getNewline(context.sourceCode.text)) + (placement?.gap ?? "");
|
|
669
|
+
|
|
670
|
+
if (formattedText === sourceText) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// The diagnostic message for the group's changes.
|
|
675
|
+
const message = getReportMessage(sentenceChanged, placement?.changed ?? false);
|
|
676
|
+
|
|
677
|
+
context.report({
|
|
678
|
+
/**
|
|
679
|
+
* Apply the group's combined formatting fix.
|
|
680
|
+
*
|
|
681
|
+
* @param {object} fixer
|
|
682
|
+
* The Oxlint fixer.
|
|
683
|
+
*
|
|
684
|
+
* @returns {object}
|
|
685
|
+
* The fix for the complete comment group.
|
|
686
|
+
*/
|
|
687
|
+
fix: (fixer) => replaceMinimalComment(fixer, groupToken, sourceText, formattedText),
|
|
688
|
+
message,
|
|
689
|
+
node: firstStandaloneComment,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Report block comments that need punctuation or wrapping.
|
|
696
|
+
*
|
|
697
|
+
* @param {object} context
|
|
698
|
+
* The Oxlint rule context.
|
|
699
|
+
*/
|
|
700
|
+
function reportBlockComments(context) {
|
|
701
|
+
// Every comment in the file, used to find what follows each comment.
|
|
702
|
+
const comments = context.sourceCode.getAllComments();
|
|
703
|
+
|
|
704
|
+
for (const comment of comments) {
|
|
705
|
+
if (comment.type === "Shebang" || comment.type === "Line" || isDirectiveComment(comment)) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// The comment's raw source text.
|
|
710
|
+
const commentText = getCommentText(context.sourceCode, comment);
|
|
711
|
+
// The placement of a leading comment, when it documents the next token.
|
|
712
|
+
const placement = getLeadingCommentPlacement(context.sourceCode, comment, comment, comments);
|
|
713
|
+
// The comment's current indentation, or an empty string for inline
|
|
714
|
+
// comments.
|
|
715
|
+
const actualIndent = getLineIndent(context.sourceCode, comment.range[0]) ?? "";
|
|
716
|
+
// The indentation the formatted comment should use.
|
|
717
|
+
const expectedIndent = placement?.expectedIndent ?? actualIndent;
|
|
718
|
+
|
|
719
|
+
// The comment with its layout and sentence punctuation fixed, before
|
|
720
|
+
// wrapping.
|
|
721
|
+
let punctuatedComment;
|
|
722
|
+
// Whether fixing sentence capitalisation or punctuation changed the
|
|
723
|
+
// comment.
|
|
724
|
+
let sentenceChanged;
|
|
725
|
+
|
|
726
|
+
// The indentation and newline style that the JSDoc formatters keep.
|
|
727
|
+
const jsdocLayout = {
|
|
728
|
+
indent: expectedIndent,
|
|
729
|
+
newline: getNewline(context.sourceCode.text),
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
if (isJSDoc(commentText)) {
|
|
733
|
+
// The JSDoc comment with its block structure fixed before tag
|
|
734
|
+
// formatting.
|
|
735
|
+
const structuredComment = formatJSDocBlockStructure(commentText, jsdocLayout);
|
|
736
|
+
|
|
737
|
+
// The JSDoc comment with its tags spaced, ordered, and grouped.
|
|
738
|
+
// Comments without the tags this formats keep their prose line
|
|
739
|
+
// breaks.
|
|
740
|
+
const laidOutComment = hasTargetJSDocTag(commentText)
|
|
741
|
+
? formatJSDocTagFormatting(structuredComment, jsdocLayout)
|
|
742
|
+
: structuredComment;
|
|
743
|
+
|
|
744
|
+
punctuatedComment = formatJSDocPunctuation(laidOutComment, jsdocLayout);
|
|
745
|
+
sentenceChanged = punctuatedComment !== laidOutComment;
|
|
746
|
+
} else {
|
|
747
|
+
punctuatedComment = formatOrdinaryBlockComment(context.sourceCode, comment);
|
|
748
|
+
sentenceChanged = punctuatedComment !== commentText;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// The comment after refilling prose lines that ended early.
|
|
752
|
+
const refilledComment = isJSDoc(commentText)
|
|
753
|
+
? punctuatedComment
|
|
754
|
+
: refillBlockComment(
|
|
755
|
+
punctuatedComment,
|
|
756
|
+
actualIndent,
|
|
757
|
+
expectedIndent,
|
|
758
|
+
getNewline(context.sourceCode.text),
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// Whether refilling changed the comment's layout.
|
|
762
|
+
const refillChanged = refilledComment !== punctuatedComment;
|
|
763
|
+
// The comment lines as they appear on screen after punctuation and
|
|
764
|
+
// refill.
|
|
765
|
+
const displayLines = getBlockCommentDisplayLines(refilledComment, expectedIndent);
|
|
766
|
+
|
|
767
|
+
// The comment, rewrapped when punctuation leaves a line over the limit.
|
|
768
|
+
let formattedComment = refilledComment;
|
|
769
|
+
|
|
770
|
+
// Whether any formatted line exceeds the width limit.
|
|
771
|
+
const hasOverlongLine = displayLines.some((line) => getDisplayWidth(line) > maximumLineLength);
|
|
772
|
+
|
|
773
|
+
if (hasOverlongLine) {
|
|
774
|
+
if (isJSDoc(commentText)) {
|
|
775
|
+
formattedComment = formatJSDocWrapping(punctuatedComment, jsdocLayout);
|
|
776
|
+
} else {
|
|
777
|
+
formattedComment = formatBlockComment(context.sourceCode, refilledComment, expectedIndent);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (formattedComment === null || formattedComment === commentText) {
|
|
782
|
+
if (placement === null || !placement.changed) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// The indentation the formatted comment was built with. JSDoc and
|
|
788
|
+
// rewrapped comments use the new indentation; other comments keep their
|
|
789
|
+
// current one until they are moved below.
|
|
790
|
+
const formattedIndent =
|
|
791
|
+
isJSDoc(commentText) || hasOverlongLine || refillChanged ? expectedIndent : actualIndent;
|
|
792
|
+
|
|
793
|
+
// The formatted comment text with its leading indentation.
|
|
794
|
+
const replacementComment =
|
|
795
|
+
placement === null
|
|
796
|
+
? formattedComment
|
|
797
|
+
: getReindentedCommentText(
|
|
798
|
+
formattedComment,
|
|
799
|
+
formattedIndent,
|
|
800
|
+
expectedIndent,
|
|
801
|
+
getNewline(context.sourceCode.text),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// The source range includes the comment's line start and its placement
|
|
805
|
+
// gap.
|
|
806
|
+
const replacementToken =
|
|
807
|
+
placement === null
|
|
808
|
+
? comment
|
|
809
|
+
: { range: [getLineStart(context.sourceCode, comment.range[0]), placement.rangeEnd] };
|
|
810
|
+
|
|
811
|
+
// The current source text for the complete replacement.
|
|
812
|
+
const sourceText = getCommentText(context.sourceCode, replacementToken);
|
|
813
|
+
// The replacement text, including the required gap before code or a
|
|
814
|
+
// directive.
|
|
815
|
+
const replacementText = `${replacementComment}${placement?.gap ?? ""}`;
|
|
816
|
+
// The diagnostic message for the comment's changes.
|
|
817
|
+
const message = getReportMessage(sentenceChanged, placement?.changed ?? false);
|
|
818
|
+
|
|
819
|
+
context.report({
|
|
820
|
+
/**
|
|
821
|
+
* Apply the wrapped replacement to the comment.
|
|
822
|
+
*
|
|
823
|
+
* @param {object} fixer
|
|
824
|
+
* The Oxlint fixer.
|
|
825
|
+
*
|
|
826
|
+
* @returns {object}
|
|
827
|
+
* The fix to apply.
|
|
828
|
+
*/
|
|
829
|
+
fix: (fixer) => replaceMinimalComment(fixer, replacementToken, sourceText, replacementText),
|
|
830
|
+
message,
|
|
831
|
+
node: comment,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The comment-formatting rule: punctuates comments as sentences, refills
|
|
838
|
+
* early-wrapped lines, reindents line-comment groups and wraps any comment past
|
|
839
|
+
* 80 columns, replacing each comment in one edit.
|
|
840
|
+
*/
|
|
841
|
+
export default {
|
|
842
|
+
meta: {
|
|
843
|
+
docs: { description: "Format comments to the configured layout." },
|
|
844
|
+
fixable: "code",
|
|
845
|
+
type: "layout",
|
|
846
|
+
},
|
|
847
|
+
/**
|
|
848
|
+
* Create the rule's node visitors.
|
|
849
|
+
*
|
|
850
|
+
* @param {object} context
|
|
851
|
+
* The Oxlint rule context.
|
|
852
|
+
*
|
|
853
|
+
* @returns {object}
|
|
854
|
+
* The visitor functions for this rule.
|
|
855
|
+
*/
|
|
856
|
+
createOnce(context) {
|
|
857
|
+
return {
|
|
858
|
+
/**
|
|
859
|
+
* Format every non-directive comment in the file.
|
|
860
|
+
*/
|
|
861
|
+
Program() {
|
|
862
|
+
reportTrailingLineComments(context);
|
|
863
|
+
reportLineCommentGroups(context);
|
|
864
|
+
reportBlockComments(context);
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
},
|
|
868
|
+
};
|