@d3lm/lint-preset 1.4.6 → 1.5.1
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 +166 -0
- package/dist/bin/comments.js +3215 -0
- package/dist/bin/comments.js.map +1 -0
- package/dist/{chunk-AWWQD6SG.cjs → chunk-M3BAUCCR.cjs} +19 -27
- package/dist/chunk-M3BAUCCR.cjs.map +1 -0
- package/dist/{chunk-HEPSJI2W.js → chunk-SMTCD6GW.js} +19 -27
- package/dist/chunk-SMTCD6GW.js.map +1 -0
- package/dist/comments.cjs +1462 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.d.cts +257 -0
- package/dist/comments.d.ts +257 -0
- package/dist/comments.js +1460 -0
- package/dist/comments.js.map +1 -0
- package/dist/eslint.cjs +9 -9
- package/dist/eslint.js +1 -1
- package/dist/oxlint.cjs +10 -12
- package/dist/oxlint.cjs.map +1 -1
- package/dist/oxlint.js +6 -8
- package/dist/oxlint.js.map +1 -1
- package/dist/rules.cjs +50 -11
- package/dist/rules.cjs.map +1 -1
- package/dist/rules.js +50 -11
- package/dist/rules.js.map +1 -1
- package/package.json +13 -1
- package/dist/chunk-AWWQD6SG.cjs.map +0 -1
- package/dist/chunk-HEPSJI2W.js.map +0 -1
package/dist/comments.js
ADDED
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
// src/comments/analyze.ts
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
|
+
|
|
4
|
+
// src/comments/style.ts
|
|
5
|
+
var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
|
|
6
|
+
var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
|
|
7
|
+
var DASH = /(?<!\d)\s[–-]\s(?!\d)|(?<!(?:^|\s)-[\w-]*)\s--\s(?!-)/;
|
|
8
|
+
var SEMICOLON = /;(?:\s|$)/;
|
|
9
|
+
var EMPHASIS = /(?:^|[\s(])(\*\*?)(?=[\p{L}\p{N}])[^*]+?(?<!\s)\1(?=$|[\s)"']|[.,;:!?](?:$|[\s)"']))/u;
|
|
10
|
+
var LIST_MARKER = /^(?:[-*+]|\d+[.)])\s+/;
|
|
11
|
+
var CODE_SPAN = /(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/g;
|
|
12
|
+
var COLON = /(?<=[\w)\]'"’])(?<!(?:^|[.!?]\s+)(?:[A-Z]{2,}|[Tt]odo|[Ff]ixme|[Hh]ack|[Xx]xx))(?<!\b(?:code|url|entity|[A-Za-z][a-z\d]*[A-Z]\w*|\w*_\w*)):\s+\S/;
|
|
13
|
+
function replaceKeepingLines(text, pattern, replacement) {
|
|
14
|
+
return text.replaceAll(pattern, (match) => `${replacement}${"\n".repeat(match.split("\n").length - 1)}`);
|
|
15
|
+
}
|
|
16
|
+
function stripNonProse(text) {
|
|
17
|
+
let stripped = replaceKeepingLines(text, CODE_SPAN, "code");
|
|
18
|
+
for (let previous = ""; previous !== stripped; ) {
|
|
19
|
+
previous = stripped;
|
|
20
|
+
stripped = replaceKeepingLines(stripped, /\{[^{}]*\}/g, "code");
|
|
21
|
+
}
|
|
22
|
+
return stripped.replaceAll(/\b[a-z][\w+.-]*:\/\/\S+/gi, "url").replaceAll(/&(?:[a-z][a-z\d]*|#\d+|#x[\da-f]+);/gi, "entity");
|
|
23
|
+
}
|
|
24
|
+
function hasExplanatoryColon(text) {
|
|
25
|
+
return COLON.test(text.replace(LIST_MARKER, ""));
|
|
26
|
+
}
|
|
27
|
+
function isBanner(text) {
|
|
28
|
+
const prose = stripNonProse(text.trim());
|
|
29
|
+
return BANNER.test(prose) && !COLUMN_RULE.test(prose);
|
|
30
|
+
}
|
|
31
|
+
function findStyleIssues(lines) {
|
|
32
|
+
const issues = [];
|
|
33
|
+
const prose = stripNonProse(lines.map((line) => line.text.trim()).join("\n"));
|
|
34
|
+
const proseLines = replaceKeepingLines(prose, /^@\w+\s+(?:code\s+)?(?:\S+\s+-\s+)?/gm, "").split("\n");
|
|
35
|
+
for (const [position, line] of lines.entries()) {
|
|
36
|
+
const text = proseLines[position].trim();
|
|
37
|
+
const report = (type) => {
|
|
38
|
+
issues.push({ type, index: line.index });
|
|
39
|
+
};
|
|
40
|
+
if (isBanner(line.text)) {
|
|
41
|
+
report("banner");
|
|
42
|
+
} else {
|
|
43
|
+
if (SEMICOLON.test(text)) {
|
|
44
|
+
report("semicolon");
|
|
45
|
+
}
|
|
46
|
+
if (text.includes("\u2014")) {
|
|
47
|
+
report("em-dash");
|
|
48
|
+
}
|
|
49
|
+
if (DASH.test(text)) {
|
|
50
|
+
report("dash");
|
|
51
|
+
}
|
|
52
|
+
if (hasExplanatoryColon(text)) {
|
|
53
|
+
report("label-colon");
|
|
54
|
+
}
|
|
55
|
+
if (EMPHASIS.test(text)) {
|
|
56
|
+
report("emphasis");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return issues;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/comments/wrap.ts
|
|
64
|
+
var CLAUSE_END = /[.!?,;:]["')\]*_]*$/;
|
|
65
|
+
var SENTENCE_END = /[.!?]["')\]*_]*$/;
|
|
66
|
+
var ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
67
|
+
"e.g.",
|
|
68
|
+
"i.e.",
|
|
69
|
+
"etc.",
|
|
70
|
+
"vs.",
|
|
71
|
+
"cf.",
|
|
72
|
+
"approx.",
|
|
73
|
+
"incl.",
|
|
74
|
+
"resp.",
|
|
75
|
+
"al.",
|
|
76
|
+
"viz.",
|
|
77
|
+
"esp.",
|
|
78
|
+
"ca.",
|
|
79
|
+
"fig.",
|
|
80
|
+
"figs.",
|
|
81
|
+
"eq.",
|
|
82
|
+
"vol.",
|
|
83
|
+
"pp.",
|
|
84
|
+
"mr.",
|
|
85
|
+
"mrs.",
|
|
86
|
+
"ms.",
|
|
87
|
+
"dr.",
|
|
88
|
+
"prof.",
|
|
89
|
+
"st."
|
|
90
|
+
]);
|
|
91
|
+
function isAbbreviation(word) {
|
|
92
|
+
const bare = word.replace(/^[(["']+/, "").replace(/[)\]"']+$/, "").toLowerCase();
|
|
93
|
+
return ABBREVIATIONS.has(bare) || /^(?:[a-z]\.){2,}$/.test(bare);
|
|
94
|
+
}
|
|
95
|
+
function findClosingBrace(text, start) {
|
|
96
|
+
let depth = 0;
|
|
97
|
+
for (let index = start; index < text.length; index += 1) {
|
|
98
|
+
if (text[index] === "{") {
|
|
99
|
+
depth += 1;
|
|
100
|
+
} else if (text[index] === "}") {
|
|
101
|
+
depth -= 1;
|
|
102
|
+
if (depth === 0) {
|
|
103
|
+
return index;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return -1;
|
|
108
|
+
}
|
|
109
|
+
function backtickRun(text, start) {
|
|
110
|
+
let end = start;
|
|
111
|
+
while (text[end] === "`") {
|
|
112
|
+
end += 1;
|
|
113
|
+
}
|
|
114
|
+
return end - start;
|
|
115
|
+
}
|
|
116
|
+
function findCodeSpanEnd(text, start) {
|
|
117
|
+
const run = backtickRun(text, start);
|
|
118
|
+
for (let index = text.indexOf("`", start + run); index !== -1; ) {
|
|
119
|
+
const length = backtickRun(text, index);
|
|
120
|
+
if (length === run) {
|
|
121
|
+
return index + length - 1;
|
|
122
|
+
}
|
|
123
|
+
index = text.indexOf("`", index + length);
|
|
124
|
+
}
|
|
125
|
+
return -1;
|
|
126
|
+
}
|
|
127
|
+
function tokenize(text) {
|
|
128
|
+
const tokens = [];
|
|
129
|
+
let current = "";
|
|
130
|
+
let index = 0;
|
|
131
|
+
while (index < text.length) {
|
|
132
|
+
const char = text[index];
|
|
133
|
+
const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
|
|
134
|
+
if (char === "`" && close === -1) {
|
|
135
|
+
const run = backtickRun(text, index);
|
|
136
|
+
current += text.slice(index, index + run);
|
|
137
|
+
index += run;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (close !== -1) {
|
|
141
|
+
current += text.slice(index, close + 1);
|
|
142
|
+
index = close + 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (/\s/.test(char)) {
|
|
146
|
+
if (current) {
|
|
147
|
+
tokens.push(current);
|
|
148
|
+
current = "";
|
|
149
|
+
}
|
|
150
|
+
index += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
current += char;
|
|
154
|
+
index += 1;
|
|
155
|
+
}
|
|
156
|
+
if (current) {
|
|
157
|
+
tokens.push(current);
|
|
158
|
+
}
|
|
159
|
+
return tokens;
|
|
160
|
+
}
|
|
161
|
+
function parentheticalEnd(text) {
|
|
162
|
+
let depth = 0;
|
|
163
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
164
|
+
const char = text[index];
|
|
165
|
+
if (char === "\\") {
|
|
166
|
+
index += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
|
|
170
|
+
if (close !== -1) {
|
|
171
|
+
index = close;
|
|
172
|
+
} else if (char === "(") {
|
|
173
|
+
depth += 1;
|
|
174
|
+
} else if (char === ")" && --depth === 0) {
|
|
175
|
+
return index;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
function isParenthetical(token) {
|
|
181
|
+
return token.startsWith("(") && parentheticalEnd(token) !== -1;
|
|
182
|
+
}
|
|
183
|
+
function parentheticalGroups(tokens, limit) {
|
|
184
|
+
const groups = [];
|
|
185
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
186
|
+
if (tokens[index].startsWith("(")) {
|
|
187
|
+
const end = parentheticalEnd(tokens.slice(index).join(" "));
|
|
188
|
+
let last = index;
|
|
189
|
+
let length = tokens[index].length;
|
|
190
|
+
while (length <= end && last + 1 < tokens.length) {
|
|
191
|
+
length += 1 + tokens[++last].length;
|
|
192
|
+
}
|
|
193
|
+
if (end !== -1 && length <= limit) {
|
|
194
|
+
groups.push({ start: index, end: last + 1 });
|
|
195
|
+
index = last;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return groups;
|
|
201
|
+
}
|
|
202
|
+
function bindParentheticals(tokens, limit) {
|
|
203
|
+
const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));
|
|
204
|
+
const units = [];
|
|
205
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
206
|
+
const end = groups.get(index) ?? index + 1;
|
|
207
|
+
units.push(tokens.slice(index, end).join(" "));
|
|
208
|
+
index = end - 1;
|
|
209
|
+
}
|
|
210
|
+
return units;
|
|
211
|
+
}
|
|
212
|
+
function endsClause(token) {
|
|
213
|
+
return CLAUSE_END.test(token) && !isAbbreviation(token);
|
|
214
|
+
}
|
|
215
|
+
function endsSentence(token) {
|
|
216
|
+
return SENTENCE_END.test(token) && !isAbbreviation(token);
|
|
217
|
+
}
|
|
218
|
+
function opensAside(token) {
|
|
219
|
+
return /^[([]/.test(token) && !/[)\]]["'`.,;:!?]*$/.test(token);
|
|
220
|
+
}
|
|
221
|
+
function isOrphan(previous, token) {
|
|
222
|
+
return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
|
|
223
|
+
}
|
|
224
|
+
function startsBlock(token) {
|
|
225
|
+
return /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
|
|
226
|
+
}
|
|
227
|
+
function bindLineStarts(tokens, startsLine2 = startsBlock) {
|
|
228
|
+
const units = [];
|
|
229
|
+
for (const [index, token] of tokens.entries()) {
|
|
230
|
+
if (units.length > 0 && startsLine2(token, tokens[index + 1])) {
|
|
231
|
+
units[units.length - 1] += ` ${token}`;
|
|
232
|
+
} else {
|
|
233
|
+
units.push(token);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return units;
|
|
237
|
+
}
|
|
238
|
+
function breaksBefore(previous, last, next, options) {
|
|
239
|
+
return options.avoidSentenceOrphans && isOrphan(previous, last) || startsBlock(next);
|
|
240
|
+
}
|
|
241
|
+
function lineLength(tokens) {
|
|
242
|
+
let length = 0;
|
|
243
|
+
for (const token of tokens) {
|
|
244
|
+
length += token.length;
|
|
245
|
+
}
|
|
246
|
+
return length + Math.max(0, tokens.length - 1);
|
|
247
|
+
}
|
|
248
|
+
function wrapTokens(tokens, firstLimit, restLimit, options) {
|
|
249
|
+
return wrapFrom(tokens, 0, firstLimit, restLimit, options, Infinity);
|
|
250
|
+
}
|
|
251
|
+
function wrapFrom(tokens, from, firstLimit, restLimit, options, maxLines) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
let line = [];
|
|
254
|
+
let length = 0;
|
|
255
|
+
for (let index = from; index < tokens.length; index += 1) {
|
|
256
|
+
const token = tokens[index];
|
|
257
|
+
while (line.length > 0) {
|
|
258
|
+
const limit = lines.length === 0 ? firstLimit : restLimit;
|
|
259
|
+
const nextLength = length + 1 + token.length;
|
|
260
|
+
const extra = isParenthetical(token) ? options.parentheticalOverflow ?? 0 : 0;
|
|
261
|
+
if (nextLength <= limit + extra) {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
const carry = [];
|
|
265
|
+
while (line.length >= 2 && breaksBefore(line.at(-2) ?? "", line.at(-1) ?? "", carry[0] ?? token, options)) {
|
|
266
|
+
carry.unshift(line.pop() ?? "");
|
|
267
|
+
}
|
|
268
|
+
lines.push(line);
|
|
269
|
+
if (lines.length >= maxLines) {
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
line = carry;
|
|
273
|
+
length = lineLength(carry);
|
|
274
|
+
}
|
|
275
|
+
line.push(token);
|
|
276
|
+
length = lineLength(line);
|
|
277
|
+
}
|
|
278
|
+
if (line.length > 0) {
|
|
279
|
+
lines.push(line);
|
|
280
|
+
}
|
|
281
|
+
return lines;
|
|
282
|
+
}
|
|
283
|
+
var MIN_LAST_LINE_WORDS = 3;
|
|
284
|
+
function hasWidow(lines) {
|
|
285
|
+
const last = lines.at(-1) ?? [];
|
|
286
|
+
const words = last.reduce((count, token) => count + (isParenthetical(token) ? tokenize(token).length : 1), 0);
|
|
287
|
+
return lines.length >= 2 && words < MIN_LAST_LINE_WORDS;
|
|
288
|
+
}
|
|
289
|
+
var FULLNESS_WEIGHT = 0.07;
|
|
290
|
+
var PARENTHETICAL_SPLIT_WEIGHT = 2;
|
|
291
|
+
var MAX_STEP = 15;
|
|
292
|
+
var EXTRA_LINES = 2;
|
|
293
|
+
function fewestLines(starts, target) {
|
|
294
|
+
const total = starts.length - 1;
|
|
295
|
+
const fewest = new Int32Array(total + 1);
|
|
296
|
+
let end = total;
|
|
297
|
+
for (let index = total - 1; index >= 0; index -= 1) {
|
|
298
|
+
while (starts[end] - starts[index] - 1 > target) {
|
|
299
|
+
end -= 1;
|
|
300
|
+
}
|
|
301
|
+
fewest[index] = fewest[end] + 1;
|
|
302
|
+
}
|
|
303
|
+
return fewest;
|
|
304
|
+
}
|
|
305
|
+
function firstAtMost(values, limit, from) {
|
|
306
|
+
let low = from;
|
|
307
|
+
let high = values.length - 1;
|
|
308
|
+
while (low < high) {
|
|
309
|
+
const middle = Math.floor((low + high) / 2);
|
|
310
|
+
if (values[middle] <= limit) {
|
|
311
|
+
high = middle;
|
|
312
|
+
} else {
|
|
313
|
+
low = middle + 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return low;
|
|
317
|
+
}
|
|
318
|
+
function costsOfLine(input, target, line, previous, fewest) {
|
|
319
|
+
const { starts, afterOrphan } = input;
|
|
320
|
+
const offset = line === 1 ? input.offset : 0;
|
|
321
|
+
const linesLeft = input.count - line;
|
|
322
|
+
const last = starts.length - 1 - linesLeft;
|
|
323
|
+
const base = firstAtMost(fewest, linesLeft, Math.max(line, previous.low + 1));
|
|
324
|
+
const costs = [];
|
|
325
|
+
const origins = [];
|
|
326
|
+
let low = -1;
|
|
327
|
+
let high = -1;
|
|
328
|
+
for (let end = base; end <= last; end += 1) {
|
|
329
|
+
const nearest = Math.min(end - 1, previous.high);
|
|
330
|
+
if (end > previous.high && starts[end] - starts[nearest] - 1 + offset > target) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
let cost = Infinity;
|
|
334
|
+
let origin = -1;
|
|
335
|
+
if (!input.blockStarts[end]) {
|
|
336
|
+
const splitCost = input.splitCosts.get(end) ?? 0;
|
|
337
|
+
for (let start = nearest; start >= previous.low; start -= 1) {
|
|
338
|
+
const length = starts[end] - starts[start] - 1 + offset;
|
|
339
|
+
if (length > target) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
const before = previous.costs[start - previous.base];
|
|
343
|
+
if (before === Infinity || afterOrphan[end] && end - start >= 2) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const candidate = before + (target - length) ** 2 + splitCost;
|
|
347
|
+
if (candidate <= cost) {
|
|
348
|
+
cost = candidate;
|
|
349
|
+
origin = start;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
costs.push(cost);
|
|
354
|
+
origins.push(origin);
|
|
355
|
+
if (cost !== Infinity) {
|
|
356
|
+
low = low === -1 ? end : low;
|
|
357
|
+
high = end;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return low === -1 ? void 0 : { base, costs, origins, low, high };
|
|
361
|
+
}
|
|
362
|
+
function balanceTokens(tokens, firstLimit, restLimit, count, options, preferredLimit = restLimit, splitCosts = /* @__PURE__ */ new Map()) {
|
|
363
|
+
const total = tokens.length;
|
|
364
|
+
const starts = [0];
|
|
365
|
+
let longest = -Infinity;
|
|
366
|
+
for (const token of tokens) {
|
|
367
|
+
starts.push((starts.at(-1) ?? 0) + token.length + 1);
|
|
368
|
+
longest = Math.max(longest, token.length);
|
|
369
|
+
}
|
|
370
|
+
const input = {
|
|
371
|
+
starts,
|
|
372
|
+
count,
|
|
373
|
+
offset: restLimit - firstLimit,
|
|
374
|
+
blockStarts: tokens.map((token) => startsBlock(token)),
|
|
375
|
+
afterOrphan: tokens.map((_, index) => {
|
|
376
|
+
return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);
|
|
377
|
+
}),
|
|
378
|
+
splitCosts
|
|
379
|
+
};
|
|
380
|
+
const targets = [];
|
|
381
|
+
for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {
|
|
382
|
+
targets.push(target);
|
|
383
|
+
}
|
|
384
|
+
const most = starts[total] - count + input.offset;
|
|
385
|
+
let best;
|
|
386
|
+
for (const target of targets.toReversed()) {
|
|
387
|
+
const fullness = FULLNESS_WEIGHT * (count - 1) * (preferredLimit - target) ** 2;
|
|
388
|
+
const spare = (count - 1) * target - most;
|
|
389
|
+
const least = count > 1 && spare > 0 ? Math.max(spare ** 2 / (count - 1) - 1, 0) : 0;
|
|
390
|
+
if (best !== void 0 && least + fullness > best.cost) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const fewest = fewestLines(starts, target);
|
|
394
|
+
const rows = [{ base: 0, costs: [0], origins: [-1], low: 0, high: 0 }];
|
|
395
|
+
for (let line = 1; line < count; line += 1) {
|
|
396
|
+
const row2 = costsOfLine(input, target, line, rows[line - 1], fewest);
|
|
397
|
+
if (!row2) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
rows.push(row2);
|
|
401
|
+
}
|
|
402
|
+
if (rows.length < count) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const row = rows[count - 1];
|
|
406
|
+
for (let start = row.low; start <= row.high; start += 1) {
|
|
407
|
+
const score = row.costs[start - row.base] + fullness;
|
|
408
|
+
const improves = score < (best?.cost ?? Infinity) || score === best?.cost && target > best.target;
|
|
409
|
+
if (!improves || starts[total] - starts[start] - 1 > target || hasWidow([[], tokens.slice(start)])) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const breaks = [start];
|
|
413
|
+
for (let line = count - 1; line > 1; line -= 1) {
|
|
414
|
+
breaks.unshift(rows[line].origins[breaks[0] - rows[line].base]);
|
|
415
|
+
}
|
|
416
|
+
best = { cost: score, target, breaks };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!best) {
|
|
420
|
+
return void 0;
|
|
421
|
+
}
|
|
422
|
+
const bounds = [0, ...best.breaks, total];
|
|
423
|
+
return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));
|
|
424
|
+
}
|
|
425
|
+
function hasStep(lines, offset = 0) {
|
|
426
|
+
return lines.some((line, index) => {
|
|
427
|
+
if (index === 0) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const before = lineLength(lines[index - 1]) + (index === 1 ? offset : 0);
|
|
431
|
+
return lineLength(line) - before > MAX_STEP;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function splitCostsOf(tokens, limit) {
|
|
435
|
+
const splitCosts = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const group of parentheticalGroups(tokens, limit)) {
|
|
437
|
+
const length = lineLength(tokens.slice(group.start, group.end));
|
|
438
|
+
for (let at = group.start + 1; at < group.end; at += 1) {
|
|
439
|
+
splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return splitCosts;
|
|
443
|
+
}
|
|
444
|
+
function hasParentheticals(tokens, restLimit, options) {
|
|
445
|
+
if (options.parentheticalOverflow === void 0) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return bindParentheticals(tokens, restLimit + options.parentheticalOverflow).some((token) => isParenthetical(token));
|
|
449
|
+
}
|
|
450
|
+
function layoutTokens(tokens, firstLimit, restLimit, options) {
|
|
451
|
+
const lines = layoutLines(tokens, firstLimit, restLimit, options);
|
|
452
|
+
const offset = restLimit - firstLimit;
|
|
453
|
+
if (options.keepFirstLine || options.keepSteps || !hasStep(lines, offset)) {
|
|
454
|
+
return lines;
|
|
455
|
+
}
|
|
456
|
+
const overflow = hasParentheticals(tokens, restLimit, options) ? options.parentheticalOverflow ?? 0 : 0;
|
|
457
|
+
const splitCosts = overflow > 0 ? splitCostsOf(tokens, restLimit + overflow) : /* @__PURE__ */ new Map();
|
|
458
|
+
for (let count = lines.length + 1; count <= lines.length + EXTRA_LINES; count += 1) {
|
|
459
|
+
const even = balanceTokens(
|
|
460
|
+
tokens,
|
|
461
|
+
firstLimit + overflow,
|
|
462
|
+
restLimit + overflow,
|
|
463
|
+
count,
|
|
464
|
+
options,
|
|
465
|
+
restLimit,
|
|
466
|
+
splitCosts
|
|
467
|
+
);
|
|
468
|
+
if (even && !hasStep(even, offset)) {
|
|
469
|
+
return even;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines;
|
|
473
|
+
}
|
|
474
|
+
function layoutLines(tokens, firstLimit, restLimit, options) {
|
|
475
|
+
if (options.parentheticalOverflow !== void 0) {
|
|
476
|
+
const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);
|
|
477
|
+
if (grouped.some((token) => isParenthetical(token))) {
|
|
478
|
+
const minimumLines = Math.max(
|
|
479
|
+
2,
|
|
480
|
+
Math.ceil((lineLength(tokens) + 1 + restLimit - firstLimit) / Math.max(1, restLimit + 1))
|
|
481
|
+
);
|
|
482
|
+
let preserved = wrapTokens(grouped, firstLimit, restLimit, options);
|
|
483
|
+
if (minimumLines === 2 && preserved.length > 2) {
|
|
484
|
+
const wide = wrapTokens(
|
|
485
|
+
grouped,
|
|
486
|
+
firstLimit + options.parentheticalOverflow,
|
|
487
|
+
restLimit + options.parentheticalOverflow,
|
|
488
|
+
{ ...options, parentheticalOverflow: void 0 }
|
|
489
|
+
);
|
|
490
|
+
if (wide.length === 2 && !hasWidow(wide)) {
|
|
491
|
+
preserved = wide;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (preserved.length <= 2 && !hasWidow(preserved)) {
|
|
495
|
+
let cursor = 0;
|
|
496
|
+
return preserved.map((line) => {
|
|
497
|
+
const expanded = [];
|
|
498
|
+
const length = lineLength(line);
|
|
499
|
+
let used = -1;
|
|
500
|
+
while (cursor < tokens.length && used < length) {
|
|
501
|
+
const token = tokens[cursor++];
|
|
502
|
+
expanded.push(token);
|
|
503
|
+
used += token.length + 1;
|
|
504
|
+
}
|
|
505
|
+
return expanded;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const splitCosts = splitCostsOf(tokens, restLimit + options.parentheticalOverflow);
|
|
509
|
+
const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: void 0 });
|
|
510
|
+
for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {
|
|
511
|
+
const balanced2 = balanceTokens(
|
|
512
|
+
tokens,
|
|
513
|
+
firstLimit + options.parentheticalOverflow,
|
|
514
|
+
restLimit + options.parentheticalOverflow,
|
|
515
|
+
count,
|
|
516
|
+
options,
|
|
517
|
+
restLimit,
|
|
518
|
+
splitCosts
|
|
519
|
+
);
|
|
520
|
+
if (balanced2) {
|
|
521
|
+
return balanced2;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const greedy = wrapTokens(tokens, firstLimit, restLimit, options);
|
|
527
|
+
if (greedy.length < 2) {
|
|
528
|
+
return greedy;
|
|
529
|
+
}
|
|
530
|
+
if (options.keepFirstLine) {
|
|
531
|
+
const [head] = greedy;
|
|
532
|
+
const rest = layoutTokens(tokens.slice(head.length), restLimit, restLimit, { ...options, keepFirstLine: false });
|
|
533
|
+
const lines = [head, ...rest];
|
|
534
|
+
if (!hasWidow(lines)) {
|
|
535
|
+
return lines;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);
|
|
539
|
+
if (balanced) {
|
|
540
|
+
return balanced;
|
|
541
|
+
}
|
|
542
|
+
const stretched = wrapTokens(tokens, firstLimit + options.tolerance, restLimit + options.tolerance, options);
|
|
543
|
+
return hasWidow(stretched) ? greedy : stretched;
|
|
544
|
+
}
|
|
545
|
+
function breakAfterSentence(lines, firstLimit, restLimit, options) {
|
|
546
|
+
if (lines.length !== 2 || !options.avoidSentenceOrphans) {
|
|
547
|
+
return lines;
|
|
548
|
+
}
|
|
549
|
+
const tokens = lines.flat();
|
|
550
|
+
const offset = restLimit - firstLimit;
|
|
551
|
+
for (let end = tokens.length - 1; end > 0; end -= 1) {
|
|
552
|
+
const first = tokens.slice(0, end);
|
|
553
|
+
const second = tokens.slice(end);
|
|
554
|
+
if (endsSentence(tokens[end - 1]) && !hasWidow([first, second]) && !breaksBefore(tokens[end - 2] ?? "", tokens[end - 1], tokens[end], options) && lineLength(first) <= firstLimit && lineLength(second) <= lineLength(first) + offset) {
|
|
555
|
+
return [first, second];
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return lines;
|
|
559
|
+
}
|
|
560
|
+
function findLineIssues(lines, firstLimit, restLimit, options) {
|
|
561
|
+
const issues = [];
|
|
562
|
+
const tokens = lines.flat();
|
|
563
|
+
let next = 0;
|
|
564
|
+
for (const [index, line] of lines.entries()) {
|
|
565
|
+
const limit = index === 0 ? firstLimit : restLimit;
|
|
566
|
+
const length = lineLength(line);
|
|
567
|
+
const start = next;
|
|
568
|
+
next += line.length;
|
|
569
|
+
if (line.length > 1 && length > limit + options.tolerance) {
|
|
570
|
+
issues.push({ type: "overflow", line: index });
|
|
571
|
+
}
|
|
572
|
+
if (index === lines.length - 1 || line.length === 0) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (options.avoidSentenceOrphans && line.length >= 2 && isOrphan(line.at(-2) ?? "", line.at(-1) ?? "")) {
|
|
576
|
+
issues.push({ type: "orphan", line: index });
|
|
577
|
+
}
|
|
578
|
+
const [expected] = wrapFrom(tokens, start, limit, restLimit, options, 1);
|
|
579
|
+
if (expected.length > line.length) {
|
|
580
|
+
issues.push({ type: "underfill", line: index });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return issues;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/comments/analyze.ts
|
|
587
|
+
var ISSUE_TIERS = {
|
|
588
|
+
overflow: "verdict",
|
|
589
|
+
underfill: "verdict",
|
|
590
|
+
widow: "verdict",
|
|
591
|
+
unbalanced: "verdict",
|
|
592
|
+
orphan: "hint",
|
|
593
|
+
banner: "hint",
|
|
594
|
+
"em-dash": "hint",
|
|
595
|
+
semicolon: "hint",
|
|
596
|
+
"label-colon": "hint",
|
|
597
|
+
emphasis: "hint",
|
|
598
|
+
dash: "hint"
|
|
599
|
+
};
|
|
600
|
+
var IGNORE_DIRECTIVE = "comments-ignore";
|
|
601
|
+
var FENCE = /^(?:`{3,}(?!.*`)|~{3,})/;
|
|
602
|
+
var JSDOC_TAG = /^@([A-Za-z]+)/;
|
|
603
|
+
var NESTED_LIST_MARKER = /^\s+(?:[-*+]|\d+[.)])\s+/;
|
|
604
|
+
var PROSE_TAGS = /* @__PURE__ */ new Set([
|
|
605
|
+
"arg",
|
|
606
|
+
"argument",
|
|
607
|
+
"deprecated",
|
|
608
|
+
"description",
|
|
609
|
+
"exception",
|
|
610
|
+
"note",
|
|
611
|
+
"param",
|
|
612
|
+
"prop",
|
|
613
|
+
"property",
|
|
614
|
+
"remarks",
|
|
615
|
+
"return",
|
|
616
|
+
"returns",
|
|
617
|
+
"see",
|
|
618
|
+
"since",
|
|
619
|
+
"summary",
|
|
620
|
+
"template",
|
|
621
|
+
"throws",
|
|
622
|
+
"todo",
|
|
623
|
+
"typeParam",
|
|
624
|
+
"yields"
|
|
625
|
+
]);
|
|
626
|
+
var CODE_TAGS = /* @__PURE__ */ new Set(["default", "defaultValue", "example"]);
|
|
627
|
+
var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "property", "template", "typeParam"]);
|
|
628
|
+
var LINE_DIRECTIVE = /^\s*(?:(?:eslint|oxlint|stylelint)-(?:disable(?:-next-line|-line)?|enable)\b|eslint-env\b|@ts-(?:ignore|expect-error|nocheck|check)\b|@jsx\w*\b|@vite-ignore\b|[@#]__[A-Z_]+__|@(?:vitest|jest)-environment\b|(?:istanbul|c8|v8)\s+ignore\b|prettier-ignore\b|biome-ignore\b|deno-(?:lint-ignore|fmt-ignore)(?:-file)?\b|tslint:|(?:c[sS]pell|spell-?checker)\s*:|(?:webpack|turbopack)[A-Z][a-zA-Z]*\s*:|vim?:\s|#(?:region|endregion)\b|<reference\s)/;
|
|
629
|
+
var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
|
|
630
|
+
var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
|
|
631
|
+
var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
|
|
632
|
+
var BOX_DRAWING = /[\u2500-\u257F]/;
|
|
633
|
+
var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
|
|
634
|
+
var ARROWS_OR_RULES = /[▲▼◀▶→←↑↓]|-->|<--|\+-{2,}|-{3,}|={4,}/;
|
|
635
|
+
var HARD_BREAK = /\S {2,}$|(?<!\\)\\(?:\\\\)*$/;
|
|
636
|
+
function isProseLike(statement, source) {
|
|
637
|
+
if (statement.type === "LabeledStatement") {
|
|
638
|
+
return source[statement.end - 1] !== ";";
|
|
639
|
+
}
|
|
640
|
+
if (statement.type === "EmptyStatement") {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (statement.type !== "ExpressionStatement") {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
let { expression } = statement;
|
|
647
|
+
while (expression.type === "ParenthesizedExpression") {
|
|
648
|
+
expression = expression.expression;
|
|
649
|
+
}
|
|
650
|
+
if (expression.type === "TaggedTemplateExpression") {
|
|
651
|
+
return expression.tag.type === "Identifier";
|
|
652
|
+
}
|
|
653
|
+
return expression.type === "Identifier" || expression.type === "Literal" || expression.type === "TemplateLiteral";
|
|
654
|
+
}
|
|
655
|
+
function parsesAsStatements(text) {
|
|
656
|
+
if (!/[()[\]{}=;<>]/.test(text)) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
const source = `async function* comment() {
|
|
660
|
+
${text}
|
|
661
|
+
}`;
|
|
662
|
+
const { program, errors } = parseSync("comment.tsx", source);
|
|
663
|
+
const wrapper = program.body.at(0);
|
|
664
|
+
if (errors.length > 0 || wrapper?.type !== "FunctionDeclaration" || !wrapper.body) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const statements = wrapper.body.body;
|
|
668
|
+
return statements.length > 0 && !statements.every((statement) => isProseLike(statement, source));
|
|
669
|
+
}
|
|
670
|
+
var CODE_VALUES = /* @__PURE__ */ new Set([
|
|
671
|
+
"Literal",
|
|
672
|
+
"ArrayExpression",
|
|
673
|
+
"ObjectExpression",
|
|
674
|
+
"CallExpression",
|
|
675
|
+
"NewExpression",
|
|
676
|
+
"ArrowFunctionExpression",
|
|
677
|
+
"FunctionExpression",
|
|
678
|
+
"UnaryExpression",
|
|
679
|
+
"MemberExpression"
|
|
680
|
+
]);
|
|
681
|
+
function parsesAsProperties(text) {
|
|
682
|
+
if (!text.includes(":")) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
const { program, errors } = parseSync("comment.ts", `({
|
|
686
|
+
${text}
|
|
687
|
+
})`);
|
|
688
|
+
const statement = program.body.at(0);
|
|
689
|
+
if (errors.length > 0 || program.body.length !== 1 || statement?.type !== "ExpressionStatement") {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
let { expression } = statement;
|
|
693
|
+
while (expression.type === "ParenthesizedExpression") {
|
|
694
|
+
expression = expression.expression;
|
|
695
|
+
}
|
|
696
|
+
if (expression.type !== "ObjectExpression" || expression.properties.length === 0) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
const { properties } = expression;
|
|
700
|
+
if (!properties.every((property) => property.type === "Property" && !property.shorthand && !property.method)) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
|
|
704
|
+
}
|
|
705
|
+
function isCode(texts) {
|
|
706
|
+
const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
|
|
707
|
+
return parsesAsStatements(text) || parsesAsProperties(text);
|
|
708
|
+
}
|
|
709
|
+
function isCodeLine(text) {
|
|
710
|
+
return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
|
|
711
|
+
}
|
|
712
|
+
function layoutReason(prose, raw) {
|
|
713
|
+
if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
|
|
714
|
+
return "table row";
|
|
715
|
+
}
|
|
716
|
+
if (BOX_DRAWING.test(prose)) {
|
|
717
|
+
return "box drawing";
|
|
718
|
+
}
|
|
719
|
+
if (isBanner(raw)) {
|
|
720
|
+
return "repeated characters";
|
|
721
|
+
}
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
function isMarkup(prose) {
|
|
725
|
+
return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
|
|
726
|
+
}
|
|
727
|
+
function mask(text) {
|
|
728
|
+
return text.replaceAll(/[^\n]/g, "x");
|
|
729
|
+
}
|
|
730
|
+
function maskCode(text) {
|
|
731
|
+
let masked = text.replaceAll(CODE_SPAN, mask);
|
|
732
|
+
for (let previous = ""; previous !== masked; ) {
|
|
733
|
+
previous = masked;
|
|
734
|
+
masked = masked.replaceAll(/\{[^{}]*\}/g, mask);
|
|
735
|
+
}
|
|
736
|
+
return masked;
|
|
737
|
+
}
|
|
738
|
+
function hasAlignedColumns(lines) {
|
|
739
|
+
const masked = maskCode(lines.map((body) => body.text).join("\n")).split("\n");
|
|
740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const line of masked) {
|
|
742
|
+
const columns = new Set([...line.matchAll(/[^\s.!?][^\S\n]{2,}(?=\S)/g)].map((gap) => gap.index + gap[0].length));
|
|
743
|
+
if ([...columns].some((column) => seen.has(column))) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
for (const column of columns) {
|
|
747
|
+
seen.add(column);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
function structureOf(lines) {
|
|
753
|
+
const trimmed = lines.map((line) => line.text.trim()).join("\n");
|
|
754
|
+
const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
|
|
755
|
+
const proseLines = stripNonProse(trimmed).split("\n");
|
|
756
|
+
if (isCode(lines.map((line) => line.text.trim()))) {
|
|
757
|
+
return { tier: "code", reason: "code" };
|
|
758
|
+
}
|
|
759
|
+
let layout;
|
|
760
|
+
let codeLike = false;
|
|
761
|
+
let manual = lines.slice(0, -1).some((line) => line.hardBreak) ? "hard line break" : void 0;
|
|
762
|
+
for (const [position, line] of texts.entries()) {
|
|
763
|
+
const prose = proseLines[position].trim();
|
|
764
|
+
layout ??= layoutReason(prose, lines[position]?.text ?? "");
|
|
765
|
+
if (isCodeLine(line.trim())) {
|
|
766
|
+
codeLike = true;
|
|
767
|
+
} else if (isMarkup(prose)) {
|
|
768
|
+
manual ??= "markup";
|
|
769
|
+
} else if ((prose.match(/\|/g)?.length ?? 0) >= 2) {
|
|
770
|
+
manual ??= "pipes";
|
|
771
|
+
} else if (ARROWS_OR_RULES.test(prose)) {
|
|
772
|
+
manual ??= "arrows or rules";
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
layout ??= hasAlignedColumns(lines) ? "aligned columns" : void 0;
|
|
776
|
+
if (layout) {
|
|
777
|
+
return { tier: "layout", reason: layout };
|
|
778
|
+
}
|
|
779
|
+
if (codeLike) {
|
|
780
|
+
return { tier: "code-like", reason: "code-like line" };
|
|
781
|
+
}
|
|
782
|
+
if (texts.filter((text) => DEFINITION.test(text.trim())).length >= 2) {
|
|
783
|
+
manual ??= "definition-like lines";
|
|
784
|
+
}
|
|
785
|
+
return manual ? { tier: "manual", reason: manual } : void 0;
|
|
786
|
+
}
|
|
787
|
+
var FenceTracker = class {
|
|
788
|
+
_open;
|
|
789
|
+
isInside() {
|
|
790
|
+
return this._open !== void 0;
|
|
791
|
+
}
|
|
792
|
+
next(text) {
|
|
793
|
+
const trimmed = text.trim();
|
|
794
|
+
if (this._open === void 0) {
|
|
795
|
+
this._open = FENCE.exec(trimmed)?.[0];
|
|
796
|
+
return this._open !== void 0;
|
|
797
|
+
}
|
|
798
|
+
if (trimmed.startsWith(this._open) && new RegExp(`^\\${this._open[0]}+$`).test(trimmed)) {
|
|
799
|
+
this._open = void 0;
|
|
800
|
+
}
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
function segment(lines) {
|
|
805
|
+
const paragraphs = [];
|
|
806
|
+
const fence = new FenceTracker();
|
|
807
|
+
let current;
|
|
808
|
+
let inCodeTag = false;
|
|
809
|
+
const flush = () => {
|
|
810
|
+
if (current) {
|
|
811
|
+
while (current.lines.length > 1 && !current.lines.at(-1)?.text.trim()) {
|
|
812
|
+
current.lines.pop();
|
|
813
|
+
}
|
|
814
|
+
paragraphs.push(current);
|
|
815
|
+
current = void 0;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
const start = (kind, line, marker = "", reason) => {
|
|
819
|
+
flush();
|
|
820
|
+
current = { kind, lines: [line], marker, reason };
|
|
821
|
+
};
|
|
822
|
+
for (const line of lines) {
|
|
823
|
+
const { text } = line;
|
|
824
|
+
if (inCodeTag) {
|
|
825
|
+
if (fence.isInside() || !JSDOC_TAG.test(text)) {
|
|
826
|
+
fence.next(text);
|
|
827
|
+
current?.lines.push(line);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
inCodeTag = false;
|
|
831
|
+
}
|
|
832
|
+
if (fence.isInside()) {
|
|
833
|
+
current?.lines.push(line);
|
|
834
|
+
fence.next(text);
|
|
835
|
+
if (!fence.isInside()) {
|
|
836
|
+
flush();
|
|
837
|
+
}
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (line.verbatim) {
|
|
841
|
+
start("code", line, "", "directive");
|
|
842
|
+
flush();
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (!text.trim()) {
|
|
846
|
+
flush();
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (fence.next(text)) {
|
|
850
|
+
start("code", line, "", "code block");
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (/^\s/.test(text)) {
|
|
854
|
+
const holdsProse = current?.kind === "list" || current?.kind === "tag" && !current.reason;
|
|
855
|
+
const nested = holdsProse ? NESTED_LIST_MARKER.exec(text) : null;
|
|
856
|
+
if (nested) {
|
|
857
|
+
start("list", line, nested[0]);
|
|
858
|
+
} else if (current && (current.kind === "list" || current.kind === "tag" || current.reason === "indented")) {
|
|
859
|
+
current.lines.push(line);
|
|
860
|
+
} else {
|
|
861
|
+
start("code", line, "", "indented");
|
|
862
|
+
}
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const tag = JSDOC_TAG.exec(text);
|
|
866
|
+
if (tag) {
|
|
867
|
+
start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
|
|
868
|
+
inCodeTag = CODE_TAGS.has(tag[1]);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const marker = LIST_MARKER.exec(text);
|
|
872
|
+
if (marker) {
|
|
873
|
+
start("list", line, marker[0]);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (current && (current.kind === "prose" || current.kind === "tag" || current.kind === "list")) {
|
|
877
|
+
current.lines.push(line);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
start("prose", line);
|
|
881
|
+
}
|
|
882
|
+
flush();
|
|
883
|
+
return paragraphs;
|
|
884
|
+
}
|
|
885
|
+
function startsLine(token, next) {
|
|
886
|
+
return startsBlock(token) || LINE_DIRECTIVE.test(next === void 0 ? token : `${token} ${next}`);
|
|
887
|
+
}
|
|
888
|
+
function visualLength(text, tabWidth) {
|
|
889
|
+
return text.replaceAll(" ", " ".repeat(tabWidth)).length;
|
|
890
|
+
}
|
|
891
|
+
function sourceLineLength(line, tabWidth) {
|
|
892
|
+
return visualLength(`${line.prefix}${line.text.trimEnd()}`, tabWidth);
|
|
893
|
+
}
|
|
894
|
+
function overflowIssues(lines, options) {
|
|
895
|
+
const issues = [];
|
|
896
|
+
const limit = options.maxWidth ?? options.width + options.tolerance;
|
|
897
|
+
for (const line of lines) {
|
|
898
|
+
const length = sourceLineLength(line, options.tabWidth);
|
|
899
|
+
if (length <= limit) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (options.maxWidth !== void 0) {
|
|
903
|
+
issues.push(lineIssue("overflow", line, options));
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const prefixLength = visualLength(`${line.prefix}${/^\s*/.exec(line.text)?.[0] ?? ""}`, options.tabWidth);
|
|
907
|
+
const tokens = bindLineStarts(tokenize(line.text), startsLine);
|
|
908
|
+
if (tokens.length > 1 && tokens.every((token) => prefixLength + token.length <= limit)) {
|
|
909
|
+
issues.push(lineIssue("overflow", line, options));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return issues;
|
|
913
|
+
}
|
|
914
|
+
function lineIssue(type, line, options) {
|
|
915
|
+
return { type, tier: ISSUE_TIERS[type], line: line.index + 1, length: sourceLineLength(line, options.tabWidth) };
|
|
916
|
+
}
|
|
917
|
+
function stripBlockSyntax(text) {
|
|
918
|
+
return text.replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, "").replace(/^\s*\*(?=\s|$)/, "");
|
|
919
|
+
}
|
|
920
|
+
function bannerIssues(paragraph, options) {
|
|
921
|
+
const verbatim = ["code block", "indented", "legal comment"];
|
|
922
|
+
if (!options.style || paragraph.reason !== void 0 && verbatim.includes(paragraph.reason)) {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
const banners = paragraph.lines.filter((line) => {
|
|
926
|
+
const text = paragraph.kind === "other" ? stripBlockSyntax(line.text) : line.text;
|
|
927
|
+
return isBanner(text) || paragraph.kind === "other" && /^\s*\/\*{3,}|\*{3,}\/\s*$/.test(line.text);
|
|
928
|
+
});
|
|
929
|
+
return banners.map((line) => lineIssue("banner", line, options));
|
|
930
|
+
}
|
|
931
|
+
function styleIssues(paragraph, options) {
|
|
932
|
+
if (!options.style) {
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
const byIndex = new Map(paragraph.lines.map((line) => [line.index, line]));
|
|
936
|
+
const lines = paragraph.lines.map((line) => {
|
|
937
|
+
return { index: line.index, text: paragraph.kind === "other" ? stripBlockSyntax(line.text) : line.text };
|
|
938
|
+
});
|
|
939
|
+
return findStyleIssues(lines).flatMap((issue) => {
|
|
940
|
+
const line = byIndex.get(issue.index);
|
|
941
|
+
return line ? [lineIssue(issue.type, line, options)] : [];
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function bindTagHead(tokens) {
|
|
945
|
+
let end = 1;
|
|
946
|
+
if (tokens[end]?.startsWith("{")) {
|
|
947
|
+
end += 1;
|
|
948
|
+
}
|
|
949
|
+
if (NAMED_TAGS.has(tokens[0].slice(1)) && end < tokens.length) {
|
|
950
|
+
end += 1;
|
|
951
|
+
}
|
|
952
|
+
if (tokens[end] === "-") {
|
|
953
|
+
end += 1;
|
|
954
|
+
}
|
|
955
|
+
return [tokens.slice(0, end).join(" "), ...tokens.slice(end)];
|
|
956
|
+
}
|
|
957
|
+
function keptIssues(paragraph, structure, options) {
|
|
958
|
+
const tier = structure?.tier ?? "manual";
|
|
959
|
+
return byLine([
|
|
960
|
+
...tier === "manual" || tier === "code-like" ? overflowIssues(paragraph.lines, options) : [],
|
|
961
|
+
...tier === "code" || tier === "code-like" ? [] : styleIssues(paragraph, options),
|
|
962
|
+
...bannerIssues(paragraph, options)
|
|
963
|
+
]);
|
|
964
|
+
}
|
|
965
|
+
function byLine(issues) {
|
|
966
|
+
const unique = new Map(issues.map((issue) => [`${issue.line}:${issue.type}`, issue]));
|
|
967
|
+
return [...unique.values()].toSorted((a, b) => a.line - b.line);
|
|
968
|
+
}
|
|
969
|
+
function sameLayout(a, b) {
|
|
970
|
+
return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
|
|
971
|
+
}
|
|
972
|
+
function analyzeParagraph(paragraph, options) {
|
|
973
|
+
const first = paragraph.lines[0];
|
|
974
|
+
const last = paragraph.lines.at(-1) ?? first;
|
|
975
|
+
const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
|
|
976
|
+
if (paragraph.kind === "code") {
|
|
977
|
+
return {
|
|
978
|
+
...base,
|
|
979
|
+
reflowable: false,
|
|
980
|
+
reason: paragraph.reason,
|
|
981
|
+
issues: bannerIssues(paragraph, options),
|
|
982
|
+
suggestion: null
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (paragraph.kind === "tag" && paragraph.reason) {
|
|
986
|
+
const codeTag = CODE_TAGS.has(JSDOC_TAG.exec(first.text)?.[1] ?? "");
|
|
987
|
+
const issues2 = codeTag ? [] : overflowIssues(paragraph.lines, options);
|
|
988
|
+
return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
|
|
989
|
+
}
|
|
990
|
+
if (paragraph.kind === "other") {
|
|
991
|
+
const fence = new FenceTracker();
|
|
992
|
+
const prose = {
|
|
993
|
+
...paragraph,
|
|
994
|
+
lines: paragraph.lines.filter((line) => !fence.next(stripBlockSyntax(line.text)))
|
|
995
|
+
};
|
|
996
|
+
const structure2 = structureOf(
|
|
997
|
+
prose.lines.map((line) => {
|
|
998
|
+
return { ...line, text: stripBlockSyntax(line.text) };
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
1001
|
+
const issues2 = keptIssues(prose, structure2, options);
|
|
1002
|
+
return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
|
|
1003
|
+
}
|
|
1004
|
+
const keep = (structure2) => {
|
|
1005
|
+
const issues2 = keptIssues(paragraph, structure2, options);
|
|
1006
|
+
return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
|
|
1007
|
+
};
|
|
1008
|
+
const structure = structureOf(paragraph.lines);
|
|
1009
|
+
if (structure) {
|
|
1010
|
+
return keep(structure);
|
|
1011
|
+
}
|
|
1012
|
+
const prefix = first.prefix;
|
|
1013
|
+
const prefixLength = visualLength(prefix, options.tabWidth);
|
|
1014
|
+
const hang = " ".repeat(paragraph.marker.length);
|
|
1015
|
+
const firstLimit = options.width - prefixLength - paragraph.marker.length;
|
|
1016
|
+
const restLimit = options.width - prefixLength - hang.length;
|
|
1017
|
+
const currentLines = paragraph.lines.map(
|
|
1018
|
+
(line, position) => bindLineStarts(tokenize(position === 0 ? line.text.slice(paragraph.marker.length) : line.text), startsLine)
|
|
1019
|
+
);
|
|
1020
|
+
const tokens = tokenize(currentLines.flat().join(" "));
|
|
1021
|
+
const layoutOptions = {
|
|
1022
|
+
...options,
|
|
1023
|
+
tolerance: options.maxWidth === void 0 ? options.tolerance : Math.min(options.tolerance, options.maxWidth - options.width),
|
|
1024
|
+
parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
|
|
1025
|
+
keepFirstLine: paragraph.kind === "tag"
|
|
1026
|
+
};
|
|
1027
|
+
const layOut = (from, settings) => {
|
|
1028
|
+
const even2 = layoutTokens(from, firstLimit, restLimit, settings);
|
|
1029
|
+
return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
|
|
1030
|
+
};
|
|
1031
|
+
const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
|
|
1032
|
+
const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
|
|
1033
|
+
const written = currentLines.map((words) => ` ${words.join(" ")} `);
|
|
1034
|
+
const staysInLimit = (unit) => {
|
|
1035
|
+
return unit.length <= hardLimit || written.some((line) => line.includes(` ${unit} `));
|
|
1036
|
+
};
|
|
1037
|
+
if (!units.every((unit) => staysInLimit(unit))) {
|
|
1038
|
+
return keep({ tier: "manual", reason: "unbreakable text across lines" });
|
|
1039
|
+
}
|
|
1040
|
+
const bodyOf = (lines) => {
|
|
1041
|
+
return lines.map((words, position) => {
|
|
1042
|
+
const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
|
|
1043
|
+
return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
|
|
1044
|
+
});
|
|
1045
|
+
};
|
|
1046
|
+
const settle = (settings) => {
|
|
1047
|
+
let current = units;
|
|
1048
|
+
let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
|
|
1049
|
+
for (; ; ) {
|
|
1050
|
+
const body = bodyOf(wrapped2);
|
|
1051
|
+
const rewrap = structureOf(body);
|
|
1052
|
+
if (!rewrap) {
|
|
1053
|
+
return { even: even2, wrapped: wrapped2 };
|
|
1054
|
+
}
|
|
1055
|
+
const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
|
|
1056
|
+
const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
|
|
1057
|
+
const pair = `${current[start - 1]} ${current[start]}`;
|
|
1058
|
+
if (body.length < 2 || !staysInLimit(pair)) {
|
|
1059
|
+
const reading = rewrap.tier === "code-like" ? "code" : rewrap.reason;
|
|
1060
|
+
return { tier: "manual", reason: `rewrap would read as ${reading}` };
|
|
1061
|
+
}
|
|
1062
|
+
current = current.toSpliced(start - 1, 2, pair);
|
|
1063
|
+
({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
const settled = settle(layoutOptions);
|
|
1067
|
+
const layout = "wrapped" in settled ? settled : settle({ ...layoutOptions, keepSteps: true });
|
|
1068
|
+
if (!("wrapped" in layout)) {
|
|
1069
|
+
return keep(layout);
|
|
1070
|
+
}
|
|
1071
|
+
const { even, wrapped } = layout;
|
|
1072
|
+
const matchesLayout = sameLayout(wrapped, currentLines);
|
|
1073
|
+
const lineIssues = [];
|
|
1074
|
+
const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
|
|
1075
|
+
if (!matchesLayout) {
|
|
1076
|
+
const hasExtraLines = currentLines.length > wrapped.length;
|
|
1077
|
+
if (options.maxWidth === void 0) {
|
|
1078
|
+
overflow.push(...overflowIssues(paragraph.lines, options));
|
|
1079
|
+
}
|
|
1080
|
+
lineIssues.push(
|
|
1081
|
+
...findLineIssues(currentLines, firstLimit, restLimit, layoutOptions).filter(
|
|
1082
|
+
(issue) => issue.type !== "overflow" && (issue.type !== "underfill" || hasExtraLines)
|
|
1083
|
+
)
|
|
1084
|
+
);
|
|
1085
|
+
if (hasWidow(currentLines) && !hasWidow(wrapped)) {
|
|
1086
|
+
lineIssues.push({ type: "widow", line: currentLines.length - 1 });
|
|
1087
|
+
}
|
|
1088
|
+
if (lineIssues.length === 0 && overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== void 0)) {
|
|
1089
|
+
const line = wrapped.findIndex((words, position) => words.join(" ") !== currentLines[position]?.join(" "));
|
|
1090
|
+
lineIssues.push({ type: sameLayout(even, currentLines) ? "orphan" : "unbalanced", line: Math.max(line, 0) });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const suggestion = wrapped.map(
|
|
1094
|
+
(words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
|
|
1095
|
+
);
|
|
1096
|
+
const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
|
|
1097
|
+
const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
|
|
1098
|
+
return { ...base, reflowable: true, issues, suggestion };
|
|
1099
|
+
}
|
|
1100
|
+
function computeLineStarts(source) {
|
|
1101
|
+
const starts = [0];
|
|
1102
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1103
|
+
if (source.codePointAt(index) === 10) {
|
|
1104
|
+
starts.push(index + 1);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return starts;
|
|
1108
|
+
}
|
|
1109
|
+
function lineOf(lineStarts, offset) {
|
|
1110
|
+
let low = 0;
|
|
1111
|
+
let high = lineStarts.length - 1;
|
|
1112
|
+
while (low < high) {
|
|
1113
|
+
const middle = Math.ceil((low + high) / 2);
|
|
1114
|
+
if (lineStarts[middle] <= offset) {
|
|
1115
|
+
low = middle;
|
|
1116
|
+
} else {
|
|
1117
|
+
high = middle - 1;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return low;
|
|
1121
|
+
}
|
|
1122
|
+
function stripCarriageReturn(line) {
|
|
1123
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1124
|
+
}
|
|
1125
|
+
function shapeBlock(comment, sourceLines, lineStarts) {
|
|
1126
|
+
const startLine = lineOf(lineStarts, comment.start);
|
|
1127
|
+
const endLine = lineOf(lineStarts, comment.end - 1);
|
|
1128
|
+
const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
|
|
1129
|
+
const lines = sourceLines.slice(startLine, endLine + 1);
|
|
1130
|
+
const wholeComment = (paragraphKind, reason) => {
|
|
1131
|
+
const otherLines = lines.map((text, offset) => {
|
|
1132
|
+
return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
|
|
1133
|
+
});
|
|
1134
|
+
return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
|
|
1135
|
+
};
|
|
1136
|
+
const value = comment.value.replace(/^\*/, "");
|
|
1137
|
+
if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
|
|
1138
|
+
return wholeComment("code", "legal comment");
|
|
1139
|
+
}
|
|
1140
|
+
if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
|
|
1141
|
+
return wholeComment("code", "directive");
|
|
1142
|
+
}
|
|
1143
|
+
const other = () => {
|
|
1144
|
+
return wholeComment(
|
|
1145
|
+
"other",
|
|
1146
|
+
startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
|
|
1147
|
+
);
|
|
1148
|
+
};
|
|
1149
|
+
if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
|
|
1150
|
+
return other();
|
|
1151
|
+
}
|
|
1152
|
+
const body = [];
|
|
1153
|
+
const inner = lines.slice(1, -1);
|
|
1154
|
+
for (const [offset, line] of inner.entries()) {
|
|
1155
|
+
const match = /^(\s*\*)( ?)(.*)$/.exec(line);
|
|
1156
|
+
if (!match) {
|
|
1157
|
+
return other();
|
|
1158
|
+
}
|
|
1159
|
+
const [, star, space, text] = match;
|
|
1160
|
+
body.push({
|
|
1161
|
+
index: startLine + 1 + offset,
|
|
1162
|
+
prefix: `${star} `,
|
|
1163
|
+
text: text.trimEnd(),
|
|
1164
|
+
verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
|
|
1165
|
+
hardBreak: HARD_BREAK.test(text)
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
return { kind, startLine, endLine, paragraphs: segment(body) };
|
|
1169
|
+
}
|
|
1170
|
+
function ownsLines(comment, source, lineStarts) {
|
|
1171
|
+
const startLine = lineOf(lineStarts, comment.start);
|
|
1172
|
+
const before = source.slice(lineStarts[startLine], comment.start);
|
|
1173
|
+
const endLine = lineOf(lineStarts, comment.end - 1);
|
|
1174
|
+
const lineEnd = lineStarts[endLine + 1] ?? source.length;
|
|
1175
|
+
const after = source.slice(comment.end, lineEnd);
|
|
1176
|
+
return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
|
|
1177
|
+
}
|
|
1178
|
+
function singleLineLength(paragraph, options) {
|
|
1179
|
+
const [first] = paragraph.lines;
|
|
1180
|
+
if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
|
|
1181
|
+
return sourceLineLength(first, options.tabWidth);
|
|
1182
|
+
}
|
|
1183
|
+
if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
|
|
1184
|
+
return Infinity;
|
|
1185
|
+
}
|
|
1186
|
+
const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
|
|
1187
|
+
return visualLength(first.prefix, options.tabWidth) + text.length;
|
|
1188
|
+
}
|
|
1189
|
+
var BYTE_ORDER_MARK = "\uFEFF";
|
|
1190
|
+
function parseSource(filename, source) {
|
|
1191
|
+
return parseSync(filename, source, filename.endsWith(".js") ? { lang: "jsx" } : void 0);
|
|
1192
|
+
}
|
|
1193
|
+
function describeFailure(errors, lineStarts) {
|
|
1194
|
+
const [first] = errors;
|
|
1195
|
+
const offset = first.labels.at(0)?.start;
|
|
1196
|
+
return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
|
|
1197
|
+
}
|
|
1198
|
+
function analyzeSource(source, filename, options) {
|
|
1199
|
+
return inspectSource(source, filename, options).comments;
|
|
1200
|
+
}
|
|
1201
|
+
function inspectSource(source, filename, options) {
|
|
1202
|
+
if (options.maxWidth !== void 0 && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {
|
|
1203
|
+
throw new Error("comments: maxWidth must be an integer at least as large as width");
|
|
1204
|
+
}
|
|
1205
|
+
const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
|
|
1206
|
+
const { program, comments, errors } = parseSource(filename, text);
|
|
1207
|
+
const lineStarts = computeLineStarts(text);
|
|
1208
|
+
if (errors.length > 0 && program.body.length === 0) {
|
|
1209
|
+
return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
|
|
1210
|
+
}
|
|
1211
|
+
const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
|
|
1212
|
+
const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
|
|
1213
|
+
const reports = [];
|
|
1214
|
+
const blocks = parsed.filter((comment) => {
|
|
1215
|
+
return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
|
|
1216
|
+
});
|
|
1217
|
+
for (const block of blocks) {
|
|
1218
|
+
const shape = shapeBlock(block, sourceLines, lineStarts);
|
|
1219
|
+
const singleLineWidth = Math.max(
|
|
1220
|
+
options.width,
|
|
1221
|
+
Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
|
|
1222
|
+
);
|
|
1223
|
+
const paragraphOptions = shape.paragraphs.length === 1 && singleLineLength(shape.paragraphs[0], options) <= singleLineWidth ? { ...options, width: singleLineWidth } : options;
|
|
1224
|
+
const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
|
|
1225
|
+
reports.push({
|
|
1226
|
+
file: filename,
|
|
1227
|
+
kind: shape.kind,
|
|
1228
|
+
startLine: shape.startLine + 1,
|
|
1229
|
+
endLine: shape.endLine + 1,
|
|
1230
|
+
indent: /^[\t ]*/.exec(lines[0])?.[0] ?? "",
|
|
1231
|
+
width: options.width,
|
|
1232
|
+
maxWidth: options.maxWidth,
|
|
1233
|
+
lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
|
|
1234
|
+
text: lines.join("\n"),
|
|
1235
|
+
paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return { comments: reports };
|
|
1239
|
+
}
|
|
1240
|
+
function isSuspicious(report) {
|
|
1241
|
+
return report.paragraphs.some((paragraph) => paragraph.issues.length > 0);
|
|
1242
|
+
}
|
|
1243
|
+
function hasVerdictIssues(report) {
|
|
1244
|
+
return report.paragraphs.some((paragraph) => paragraph.issues.some((issue) => issue.tier === "verdict"));
|
|
1245
|
+
}
|
|
1246
|
+
function rewrapComment(report) {
|
|
1247
|
+
const lines = report.text.split("\n");
|
|
1248
|
+
const paragraphs = report.paragraphs.filter((paragraph) => paragraph.suggestion !== null).toSorted((a, b) => b.startLine - a.startLine);
|
|
1249
|
+
for (const paragraph of paragraphs) {
|
|
1250
|
+
const start = paragraph.startLine - report.startLine;
|
|
1251
|
+
lines.splice(start, paragraph.endLine - paragraph.startLine + 1, ...paragraph.suggestion ?? []);
|
|
1252
|
+
}
|
|
1253
|
+
return lines.join("\n");
|
|
1254
|
+
}
|
|
1255
|
+
var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
|
|
1256
|
+
function codeFingerprint(source, filename) {
|
|
1257
|
+
const { program, errors } = parseSource(filename, source);
|
|
1258
|
+
if (errors.length > 0) {
|
|
1259
|
+
return JSON.stringify({ source, errors: errors.map((error) => error.message) });
|
|
1260
|
+
}
|
|
1261
|
+
return JSON.stringify(
|
|
1262
|
+
program,
|
|
1263
|
+
(key, value) => POSITION_KEYS.has(key) || typeof value === "bigint" ? void 0 : value
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/comments/agent.ts
|
|
1268
|
+
import { spawn } from "child_process";
|
|
1269
|
+
import { readFileSync } from "fs";
|
|
1270
|
+
import path from "path";
|
|
1271
|
+
var DEFAULT_RULES = `
|
|
1272
|
+
# Comment rules
|
|
1273
|
+
|
|
1274
|
+
## Priorities
|
|
1275
|
+
|
|
1276
|
+
When rules pull in different directions, the earlier one wins.
|
|
1277
|
+
|
|
1278
|
+
1. Keep code and exact text byte for byte, including every punctuation character in it. That covers code blocks, \`@example\`, code spans, identifiers, string literals like error messages, regexes, globs and other patterns like \`*test*\`, URLs, quoted text, titles, and notation like \`host:port\`.
|
|
1279
|
+
2. Keep the meaning. An edit may make a comment clearer, never different.
|
|
1280
|
+
3. Follow the style preferences below.
|
|
1281
|
+
4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
|
|
1282
|
+
|
|
1283
|
+
## Layout
|
|
1284
|
+
|
|
1285
|
+
- After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.
|
|
1286
|
+
- Don't end a line with the first word of a new sentence, clause, or parenthetical, like \`This\` after a period, \`the\` after a comma, or \`(the\`. Move it to the next line instead.
|
|
1287
|
+
- In a paragraph of two lines, the first line ends with a sentence whenever the rest fits on the second line and is no longer than the first. Don't end it with the start of the next sentence, like \`A wrap\` in \`...lay out differently. A wrap\`. Move those words to the second line instead.
|
|
1288
|
+
- A comment whose whole text is one paragraph stays on a single line when that line fits a separate, larger single-line width, even past the width. Only a longer comment wraps to the width.
|
|
1289
|
+
- Separate paragraphs with an empty comment line.
|
|
1290
|
+
- Never split a backtick code span across lines.
|
|
1291
|
+
- Keep code blocks, \`@example\`, lists, tables, box drawings, aligned columns, and Markdown hard line breaks (a line that ends with two spaces or a backslash) laid out as they are. List items wrap with their continuation lines indented to the item text, and a JSDoc tag description that doesn't fit continues on the next line without indentation.
|
|
1292
|
+
- The tool measures every line, taking the tolerance and the parts it keeps as written into account, and the notes on each comment list what is still wrong. Act on those notes. Don't count columns, and don't apply a stricter limit than the notes show.
|
|
1293
|
+
- The maximum is a hard limit for measured prose, including long tokens. If a URL, code span, or other exact text exceeds it, preserve that text and explain the unresolved overflow instead of changing its contents.
|
|
1294
|
+
- The tool never rewraps the parts it keeps as written, like a single-line block comment, a paragraph with hard line breaks, or lines that may be laid out by hand. When a note says one of their prose lines is too long, break or shorten it yourself. Exact text and intentional layout win over the width, so keep code, code spans, and the layout in them intact, and if a note can only be fixed by changing them, leave it and say why in your reply.
|
|
1295
|
+
|
|
1296
|
+
## Style
|
|
1297
|
+
|
|
1298
|
+
These are preferences for comment prose. They never apply to code or exact text (see the priorities above), and a punctuation character on its own is never a reason to edit prose that already reads clearly.
|
|
1299
|
+
|
|
1300
|
+
- Write clear sentences that flow. Descriptions read best as complete sentences with a subject and a verb, so prefer "Returns the bytes the guest sent" over a fragment like \`Bytes from the guest.\` or \`Optional.\` Imperative or third-person verb phrases like "Resolve a hostname..." or "Aborts every channel..." are fine, since the preference is against verb-less fragments, not against concise sentences.
|
|
1301
|
+
- Prefer plain sentence punctuation. Where a semicolon, an em-dash (\u2014), or a spaced hyphen, en-dash, or double hyphen standing in for one only joins two thoughts, a period, a comma, parentheses, or a word like "and", "so", or "because" usually reads better. Keep the punctuation where it carries meaning or reads more clearly than the alternatives.
|
|
1302
|
+
- Prefer a sentence over a "Label: explanation" fragment, like \`...the socket. Optional: without it...\` or \`Default: refuse\` in the middle of prose. A colon is fine where it introduces a list, code block, or example, in a term's definition, in notation, in a quotation, and in a \`Note:\` or \`Optional:\` that starts a comment. A JSDoc tag like \`@note\` or \`@todo\` can replace a label, and \`@default\` fits a default value, not a description.
|
|
1303
|
+
- Avoid asterisk emphasis like \`*this*\`, and use plain wording instead. A name or pattern in asterisks, like \`*test*\` or \`*.spec.ts\`, is a pattern, so keep its asterisks and wrap it in a code span, like \`\` \`*test*\` \`\`.
|
|
1304
|
+
- Avoid section banner comments, i.e. dividers or headings made of dashes, equals signs, asterisks, or other repeated characters that split a file into visual sections. Let the code structure carry the grouping, or write one short, plain sentence comment (e.g. \`// handlers for control mode\`). Rules that belong to a diagram, a table, a box drawing, a Markdown document, an example, or a legal header aren't banners.
|
|
1305
|
+
- Block and JSDoc comments are capitalized and end with punctuation.
|
|
1306
|
+
- Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
|
|
1307
|
+
|
|
1308
|
+
## Editing
|
|
1309
|
+
|
|
1310
|
+
- Keep the wording unless it goes against a rule above. Then reword as little as needed and keep the meaning.
|
|
1311
|
+
- Don't reword anything else unless the instructions below allow it.
|
|
1312
|
+
`.trim();
|
|
1313
|
+
|
|
1314
|
+
// src/comments/config.ts
|
|
1315
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
1316
|
+
import { createRequire } from "module";
|
|
1317
|
+
import path2 from "path";
|
|
1318
|
+
import { pathToFileURL } from "url";
|
|
1319
|
+
var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
|
|
1320
|
+
var CONFIG_FILES = [
|
|
1321
|
+
"comments.config.ts",
|
|
1322
|
+
"comments.config.mts",
|
|
1323
|
+
"comments.config.js",
|
|
1324
|
+
"comments.config.mjs",
|
|
1325
|
+
"comments.config.cjs",
|
|
1326
|
+
".commentsrc",
|
|
1327
|
+
".commentsrc.json"
|
|
1328
|
+
];
|
|
1329
|
+
var PACKAGE_JSON_KEY = "comments";
|
|
1330
|
+
function defineConfig(config) {
|
|
1331
|
+
return config;
|
|
1332
|
+
}
|
|
1333
|
+
function readJson(filepath) {
|
|
1334
|
+
try {
|
|
1335
|
+
return JSON.parse(readFileSync2(filepath, "utf8"));
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
throw new Error(`comments: failed to parse ${filepath}: ${error.message}`, { cause: error });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async function readConfigFile(filepath) {
|
|
1341
|
+
if (filepath.endsWith("rc") || filepath.endsWith(".json")) {
|
|
1342
|
+
return readJson(filepath);
|
|
1343
|
+
}
|
|
1344
|
+
const module = await import(pathToFileURL(filepath).href);
|
|
1345
|
+
return module.default ?? module;
|
|
1346
|
+
}
|
|
1347
|
+
function assertType(value, type, key, filepath) {
|
|
1348
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
1349
|
+
if (value !== void 0 && actual !== type) {
|
|
1350
|
+
throw new Error(`comments: \`${key}\` in ${filepath} must be a ${type}, got ${actual}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function assertStrings(value, key, filepath) {
|
|
1354
|
+
if (Array.isArray(value) && !value.every((item) => typeof item === "string")) {
|
|
1355
|
+
throw new Error(`comments: \`${key}\` in ${filepath} must only hold strings`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function validateConfig(raw, filepath) {
|
|
1359
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1360
|
+
throw new Error(`comments: ${filepath} must export an object`);
|
|
1361
|
+
}
|
|
1362
|
+
const config = raw;
|
|
1363
|
+
assertType(config.width, "number", "width", filepath);
|
|
1364
|
+
assertType(config.maxWidth, "number", "maxWidth", filepath);
|
|
1365
|
+
assertType(config.singleLineWidth, "number", "singleLineWidth", filepath);
|
|
1366
|
+
assertType(config.tolerance, "number", "tolerance", filepath);
|
|
1367
|
+
assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
|
|
1368
|
+
assertType(config.style, "boolean", "style", filepath);
|
|
1369
|
+
assertType(config.extensions, "array", "extensions", filepath);
|
|
1370
|
+
assertType(config.ignore, "array", "ignore", filepath);
|
|
1371
|
+
assertType(config.agent, "object", "agent", filepath);
|
|
1372
|
+
assertType(config.hook, "object", "hook", filepath);
|
|
1373
|
+
assertStrings(config.extensions, "extensions", filepath);
|
|
1374
|
+
assertStrings(config.ignore, "ignore", filepath);
|
|
1375
|
+
for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
|
|
1376
|
+
const value = config[key];
|
|
1377
|
+
const minimum = key === "tolerance" ? 0 : 1;
|
|
1378
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < minimum)) {
|
|
1379
|
+
throw new Error(`comments: \`${key}\` in ${filepath} must be an integer of at least ${minimum}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (typeof config.width === "number" && typeof config.maxWidth === "number" && config.maxWidth < config.width) {
|
|
1383
|
+
throw new Error(`comments: \`maxWidth\` in ${filepath} must be at least \`width\``);
|
|
1384
|
+
}
|
|
1385
|
+
const agent = config.agent ?? {};
|
|
1386
|
+
if (agent.runner !== void 0 && !["claude", "codex", "cursor"].includes(agent.runner)) {
|
|
1387
|
+
throw new Error(`comments: \`agent.runner\` in ${filepath} must be one of claude, codex, cursor`);
|
|
1388
|
+
}
|
|
1389
|
+
assertType(agent.model, "string", "agent.model", filepath);
|
|
1390
|
+
assertType(agent.args, "array", "agent.args", filepath);
|
|
1391
|
+
assertType(agent.command, "array", "agent.command", filepath);
|
|
1392
|
+
assertType(agent.timeout, "number", "agent.timeout", filepath);
|
|
1393
|
+
assertStrings(agent.args, "agent.args", filepath);
|
|
1394
|
+
assertStrings(agent.command, "agent.command", filepath);
|
|
1395
|
+
if (agent.timeout !== void 0 && !(agent.timeout > 0)) {
|
|
1396
|
+
throw new Error(`comments: \`agent.timeout\` in ${filepath} must be a positive number`);
|
|
1397
|
+
}
|
|
1398
|
+
for (const key of ["batchSize", "maxFiles", "maxAgents"]) {
|
|
1399
|
+
const value = agent[key];
|
|
1400
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
|
|
1401
|
+
throw new Error(`comments: \`agent.${key}\` in ${filepath} must be a positive integer`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
assertType(agent.rules, "string", "agent.rules", filepath);
|
|
1405
|
+
assertType(agent.instructions, "string", "agent.instructions", filepath);
|
|
1406
|
+
if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
|
|
1407
|
+
throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
|
|
1408
|
+
}
|
|
1409
|
+
const hook = config.hook ?? {};
|
|
1410
|
+
if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
|
|
1411
|
+
throw new Error(`comments: \`hook.mode\` in ${filepath} must be one of agent, report, off`);
|
|
1412
|
+
}
|
|
1413
|
+
return config;
|
|
1414
|
+
}
|
|
1415
|
+
async function loadConfig(cwd, options = {}) {
|
|
1416
|
+
if (options.explicitPath) {
|
|
1417
|
+
const filepath = path2.resolve(cwd, options.explicitPath);
|
|
1418
|
+
return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
|
|
1419
|
+
}
|
|
1420
|
+
let directory = path2.resolve(cwd);
|
|
1421
|
+
for (; ; ) {
|
|
1422
|
+
for (const name of CONFIG_FILES) {
|
|
1423
|
+
const filepath = path2.join(directory, name);
|
|
1424
|
+
if (existsSync(filepath)) {
|
|
1425
|
+
return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const packageJsonPath = path2.join(directory, "package.json");
|
|
1429
|
+
if (existsSync(packageJsonPath)) {
|
|
1430
|
+
const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
|
|
1431
|
+
if (value !== void 0) {
|
|
1432
|
+
return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const parent = path2.dirname(directory);
|
|
1436
|
+
if (directory === options.stopDir || parent === directory) {
|
|
1437
|
+
return { config: {} };
|
|
1438
|
+
}
|
|
1439
|
+
directory = parent;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
export {
|
|
1443
|
+
DEFAULT_RULES,
|
|
1444
|
+
analyzeSource,
|
|
1445
|
+
codeFingerprint,
|
|
1446
|
+
defineConfig,
|
|
1447
|
+
hasVerdictIssues,
|
|
1448
|
+
isSuspicious,
|
|
1449
|
+
loadConfig,
|
|
1450
|
+
rewrapComment,
|
|
1451
|
+
tokenize,
|
|
1452
|
+
wrapTokens
|
|
1453
|
+
};
|
|
1454
|
+
/**
|
|
1455
|
+
* Legal comments are kept as they are. A comment is legal when it uses the same markers
|
|
1456
|
+
* as bundlers like esbuild (`/*!`, `//!`, `@license`, and `@preserve`), or when it is a
|
|
1457
|
+
* copyright notice like `Copyright (c) 2020 Foo`.
|
|
1458
|
+
*/
|
|
1459
|
+
// a code span only names a marker, like the `@license` in a comment that explains legal comments
|
|
1460
|
+
//# sourceMappingURL=comments.js.map
|