@d3lm/lint-preset 1.5.5 → 1.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/README.md +42 -7
- package/dist/bin/comments.js +559 -123
- package/dist/bin/comments.js.map +1 -1
- package/dist/comments.cjs +462 -77
- package/dist/comments.cjs.map +1 -1
- package/dist/comments.d.cts +46 -20
- package/dist/comments.d.ts +46 -20
- package/dist/comments.js +464 -81
- package/dist/comments.js.map +1 -1
- package/native/comments.darwin-arm64.node +0 -0
- package/native/comments.darwin-x64.node +0 -0
- package/native/comments.linux-arm64-gnu.node +0 -0
- package/native/comments.linux-x64-gnu.node +0 -0
- package/package.json +9 -6
package/dist/bin/comments.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/comments/cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import
|
|
4
|
+
import { existsSync as existsSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
5
|
+
import path6 from "path";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
8
|
// src/comments/agent.ts
|
|
@@ -17,7 +17,7 @@ var DEFAULT_RULES = `
|
|
|
17
17
|
|
|
18
18
|
When rules pull in different directions, the earlier one wins.
|
|
19
19
|
|
|
20
|
-
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,
|
|
20
|
+
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, notation like \`host:port\`, and Rust intra-doc links (see below).
|
|
21
21
|
2. Keep the meaning. An edit may make a comment clearer, never different.
|
|
22
22
|
3. Follow the style preferences below.
|
|
23
23
|
4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
|
|
@@ -30,10 +30,12 @@ When rules pull in different directions, the earlier one wins.
|
|
|
30
30
|
- 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.
|
|
31
31
|
- Separate paragraphs with an empty comment line.
|
|
32
32
|
- Never split a backtick code span across lines.
|
|
33
|
+
- Never split a Markdown link across lines. A link is the text in brackets and the target right after it, like \`[the docs](https://example.com)\`, \`[the docs][label]\`, or \`[the docs]\`, and all of it stays on one line, from its first bracket to its last bracket or parenthesis. An editor can't follow a link that continues on the next line. When a line is too long, break it before or after the link.
|
|
33
34
|
- 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.
|
|
34
35
|
- 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.
|
|
35
36
|
- 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.
|
|
36
37
|
- 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.
|
|
38
|
+
- The tool never rewraps a line comment (\`//\`) either. When a note says one of its lines is too long, shorten the wording so the comment fits on its line and still says the same thing. When it can't be shortened without losing meaning, turn it into a multi-line block comment instead: \`/**\` in JavaScript and TypeScript, or \`/*\` in Rust, on its own line, then the text on lines that start with \` * \`, then \` */\` on its own line, all at the indentation of the line comment. Like other block comments, it starts with a capital letter and ends with punctuation. Leave its line breaks to the tool, which rewraps it like any other block comment. Never split a line comment into more \`//\` lines.
|
|
37
39
|
|
|
38
40
|
## Style
|
|
39
41
|
|
|
@@ -44,8 +46,16 @@ These are preferences for comment prose. They never apply to code or exact text
|
|
|
44
46
|
- 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.
|
|
45
47
|
- 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*\` \`\`.
|
|
46
48
|
- 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.
|
|
47
|
-
- Block and
|
|
48
|
-
- Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
|
|
49
|
+
- Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.
|
|
50
|
+
- Never change the kind of a comment, except to turn a line comment that is too long into a block comment (see Layout). Don't turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines. In Rust, a run of \`///\` or \`//!\` lines is one doc comment, so keep its markers, and never turn a doc comment (\`///\`, \`//!\`, \`/**\`, \`/*!\`) into another kind, since that changes or removes documentation.
|
|
51
|
+
|
|
52
|
+
## Rust doc comments
|
|
53
|
+
|
|
54
|
+
Rust doc comments are Markdown that rustdoc renders.
|
|
55
|
+
|
|
56
|
+
- Intra-doc links are exact text. Keep their brackets, backticks, and targets, like \`\` [\`count_provider_reads\`] \`\`, \`\` [\`permissions\`](crate::permissions) \`\`, \`\` [the parser][\`Parser\`] \`\`, or \`[Vec<T>]\`, since rustdoc resolves the text in brackets, or the target after it, as a path. Each link stays on one line (see Layout), including a link with spaces in its text, like \`[the provider module](crate::provider)\`.
|
|
57
|
+
- A link reference definition, like \`\` [\`Foo\`]: crate::Foo \`\`, stays on its own line as it is. Its colon is syntax, not a label.
|
|
58
|
+
- Headings like \`# Examples\`, \`# Panics\`, \`# Errors\`, and \`# Safety\` are sections that rustdoc and readers look for, so keep them. Code blocks are doctests, so keep their lines, including hidden lines that start with \`#\`, exactly as they are.
|
|
49
59
|
|
|
50
60
|
## Editing
|
|
51
61
|
|
|
@@ -64,13 +74,13 @@ var RUNNER_COMMANDS = {
|
|
|
64
74
|
var ISSUE_DESCRIPTIONS = {
|
|
65
75
|
underfill: "ends early, and words from the next line fit on it",
|
|
66
76
|
widow: "leaves fewer than three words on the last line of its paragraph",
|
|
67
|
-
unbalanced: "is in a paragraph whose lines are less even than the suggested rewrap",
|
|
77
|
+
unbalanced: "is in a paragraph whose lines are less even than the suggested rewrap, or that splits a link or code span that the rewrap keeps on one line",
|
|
68
78
|
orphan: "may end with the start of a new sentence, clause, or parenthetical, which belongs on the next line (leave it if the period or comma before it is part of an abbreviation, a name, or notation like `No. 3`, or if it is a long word like an identifier and moving it would leave the line far shorter than the lines around it)",
|
|
69
79
|
"split-parenthetical": "may split a parenthetical that the suggested rewrap keeps on one line, past the width but within the maximum (leave it if the split reads as well, like at a comma inside a list, and the line stays closer to the width)",
|
|
70
80
|
"em-dash": "may use an em-dash to join two thoughts (leave it if it's in a quotation, a title, or a string literal, or reads more clearly than the alternatives)",
|
|
71
81
|
banner: "may be a section banner (leave it if it's part of a diagram, a table, a box, a Markdown rule, a separator in an example, or legal text)",
|
|
72
82
|
semicolon: "may join clauses with a semicolon (leave it if it's code, a literal, not a clause join, or reads more clearly than a period or a joining word)",
|
|
73
|
-
"label-colon": "may use a colon after a label (leave it if it introduces a list or code, defines a term
|
|
83
|
+
"label-colon": "may use a colon after a label (leave it if it introduces a list or code, defines a term or a link like `` [`Foo`]: crate::Foo ``, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)",
|
|
74
84
|
emphasis: "may use asterisk emphasis (leave the asterisks of a name or pattern like `*test*`, which belongs in a code span with its asterisks)",
|
|
75
85
|
dash: "may use a spaced hyphen, en-dash, or double hyphen as an em-dash (leave it if it's a range, a CLI argument, arithmetic, code, a term's definition, or reads more clearly than the alternatives)"
|
|
76
86
|
};
|
|
@@ -105,7 +115,7 @@ function describeComment(comment) {
|
|
|
105
115
|
"",
|
|
106
116
|
...notes.length > 0 ? notes.map((note2) => `- ${note2}`) : ["- no measured issues; review the wording and flow"],
|
|
107
117
|
"",
|
|
108
|
-
`${fence}ts`,
|
|
118
|
+
`${fence}${comment.file.endsWith(".rs") ? "rust" : "ts"}`,
|
|
109
119
|
comment.text,
|
|
110
120
|
fence
|
|
111
121
|
].join("\n");
|
|
@@ -293,6 +303,71 @@ function runAgent(invocation, options) {
|
|
|
293
303
|
// src/comments/analyze.ts
|
|
294
304
|
import { parseSync } from "oxc-parser";
|
|
295
305
|
|
|
306
|
+
// src/comments/rust.ts
|
|
307
|
+
import { existsSync } from "fs";
|
|
308
|
+
import { createRequire } from "module";
|
|
309
|
+
import path2 from "path";
|
|
310
|
+
import { fileURLToPath } from "url";
|
|
311
|
+
var BINARY_NAME = "comments";
|
|
312
|
+
function platformSuffixes() {
|
|
313
|
+
const { platform, arch } = process;
|
|
314
|
+
switch (platform) {
|
|
315
|
+
case "darwin": {
|
|
316
|
+
return [`darwin-${arch}`, "darwin-universal"];
|
|
317
|
+
}
|
|
318
|
+
case "linux": {
|
|
319
|
+
return arch === "arm" ? ["linux-arm-gnueabihf", "linux-arm-musleabihf"] : [`linux-${arch}-gnu`, `linux-${arch}-musl`];
|
|
320
|
+
}
|
|
321
|
+
case "win32": {
|
|
322
|
+
return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];
|
|
323
|
+
}
|
|
324
|
+
default: {
|
|
325
|
+
return [`${platform}-${arch}`];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function findBinaries() {
|
|
330
|
+
const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);
|
|
331
|
+
for (let directory = path2.dirname(fileURLToPath(import.meta.url)); ; directory = path2.dirname(directory)) {
|
|
332
|
+
const found = names.map((name) => path2.join(directory, "native", name)).filter((file) => existsSync(file));
|
|
333
|
+
if (found.length > 0) {
|
|
334
|
+
return found;
|
|
335
|
+
}
|
|
336
|
+
if (path2.dirname(directory) === directory) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
var binding;
|
|
342
|
+
function loadBinding() {
|
|
343
|
+
if (binding) {
|
|
344
|
+
return binding;
|
|
345
|
+
}
|
|
346
|
+
const require2 = createRequire(import.meta.url);
|
|
347
|
+
const errors = [];
|
|
348
|
+
for (const file of findBinaries()) {
|
|
349
|
+
try {
|
|
350
|
+
binding = require2(file);
|
|
351
|
+
return binding;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
errors.push(`${file}: ${error.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const reason = errors.length > 0 ? `, and loading it failed (${errors.join("; ")})` : "";
|
|
357
|
+
throw new Error(
|
|
358
|
+
`comments: Rust files need the native addon for ${process.platform}-${process.arch}${reason}. Build it with \`pnpm run build:native\`, or leave \`.rs\` out of \`extensions\`.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
function scanRust(source) {
|
|
362
|
+
return loadBinding().scanRust(source);
|
|
363
|
+
}
|
|
364
|
+
function rustFingerprint(source) {
|
|
365
|
+
return loadBinding().rustFingerprint(source);
|
|
366
|
+
}
|
|
367
|
+
function isRustCode(text) {
|
|
368
|
+
return loadBinding().isRustCode(text);
|
|
369
|
+
}
|
|
370
|
+
|
|
296
371
|
// src/comments/style.ts
|
|
297
372
|
var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
|
|
298
373
|
var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
|
|
@@ -416,12 +491,94 @@ function findCodeSpanEnd(text, start) {
|
|
|
416
491
|
}
|
|
417
492
|
return -1;
|
|
418
493
|
}
|
|
494
|
+
function isEscaped(text, index) {
|
|
495
|
+
let backslashes = 0;
|
|
496
|
+
while (text[index - 1 - backslashes] === "\\") {
|
|
497
|
+
backslashes += 1;
|
|
498
|
+
}
|
|
499
|
+
return backslashes % 2 === 1;
|
|
500
|
+
}
|
|
501
|
+
function findUnescaped(text, char, start) {
|
|
502
|
+
for (let index = start; index < text.length; index += 1) {
|
|
503
|
+
if (text[index] === "\\") {
|
|
504
|
+
index += 1;
|
|
505
|
+
} else if (text[index] === char) {
|
|
506
|
+
return index;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return -1;
|
|
510
|
+
}
|
|
511
|
+
function findClosingBracket(text, start) {
|
|
512
|
+
let depth = 0;
|
|
513
|
+
for (let index = start; index < text.length; index += 1) {
|
|
514
|
+
switch (text[index]) {
|
|
515
|
+
case "\\": {
|
|
516
|
+
index += 1;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "`": {
|
|
520
|
+
const close = findCodeSpanEnd(text, index);
|
|
521
|
+
index = close === -1 ? index + backtickRun(text, index) - 1 : close;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case "[": {
|
|
525
|
+
depth += 1;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
case "]": {
|
|
529
|
+
depth -= 1;
|
|
530
|
+
if (depth === 0) {
|
|
531
|
+
return index;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return -1;
|
|
538
|
+
}
|
|
539
|
+
function findDestinationEnd(text, start) {
|
|
540
|
+
let depth = 0;
|
|
541
|
+
for (let index = start; index < text.length; index += 1) {
|
|
542
|
+
const char = text[index];
|
|
543
|
+
let close = -1;
|
|
544
|
+
if (char === "\\") {
|
|
545
|
+
index += 1;
|
|
546
|
+
} else if (char === "<" && text.slice(start + 1, index).trim() === "") {
|
|
547
|
+
close = findUnescaped(text, ">", index + 1);
|
|
548
|
+
} else if ((char === '"' || char === "'") && /\s/.test(text[index - 1])) {
|
|
549
|
+
close = findUnescaped(text, char, index + 1);
|
|
550
|
+
} else if (char === "(") {
|
|
551
|
+
depth += 1;
|
|
552
|
+
} else if (char === ")" && --depth === 0) {
|
|
553
|
+
return index;
|
|
554
|
+
}
|
|
555
|
+
index = close === -1 ? index : close;
|
|
556
|
+
}
|
|
557
|
+
return -1;
|
|
558
|
+
}
|
|
559
|
+
function findLinkEnd(text, start) {
|
|
560
|
+
const close = findClosingBracket(text, start);
|
|
561
|
+
if (close === -1 || text[close + 1] !== "(") {
|
|
562
|
+
return close;
|
|
563
|
+
}
|
|
564
|
+
const end = findDestinationEnd(text, close + 1);
|
|
565
|
+
return end === -1 ? close : end;
|
|
566
|
+
}
|
|
419
567
|
function tokenize(text) {
|
|
568
|
+
return splitWords(text, true);
|
|
569
|
+
}
|
|
570
|
+
function splitWords(text, keepLinks) {
|
|
420
571
|
const tokens = [];
|
|
421
572
|
let current = "";
|
|
422
573
|
let index = 0;
|
|
423
574
|
while (index < text.length) {
|
|
424
575
|
const char = text[index];
|
|
576
|
+
const linkEnd = keepLinks && char === "[" && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;
|
|
577
|
+
if (linkEnd !== -1) {
|
|
578
|
+
current += splitWords(text.slice(index, linkEnd + 1), false).join(" ");
|
|
579
|
+
index = linkEnd + 1;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
425
582
|
const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
|
|
426
583
|
if (char === "`" && close === -1) {
|
|
427
584
|
const run = backtickRun(text, index);
|
|
@@ -517,7 +674,7 @@ function isOrphan(previous, token) {
|
|
|
517
674
|
return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
|
|
518
675
|
}
|
|
519
676
|
function startsBlock(token) {
|
|
520
|
-
return /^(
|
|
677
|
+
return /^(?:\+|-+|=+|\*+|_{3,}|\d+[.)]|#{1,6})$/.test(token) || /^(?:>|@[A-Za-z]|`{3}|~{3}|\||<(?:\/?[A-Za-z]|[!?]))/.test(token);
|
|
521
678
|
}
|
|
522
679
|
function bindLineStarts(tokens, startsLine2 = startsBlock) {
|
|
523
680
|
const units = [];
|
|
@@ -955,6 +1112,7 @@ var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "p
|
|
|
955
1112
|
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)/;
|
|
956
1113
|
var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
|
|
957
1114
|
var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
|
|
1115
|
+
var RUST_LEGAL_COMMENT = /@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
|
|
958
1116
|
var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
|
|
959
1117
|
var BOX_DRAWING = /[\u2500-\u257F]/;
|
|
960
1118
|
var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
|
|
@@ -1029,12 +1187,35 @@ ${text}
|
|
|
1029
1187
|
}
|
|
1030
1188
|
return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
|
|
1031
1189
|
}
|
|
1032
|
-
function isCode(texts) {
|
|
1033
|
-
|
|
1034
|
-
return parsesAsStatements(text) || parsesAsProperties(text);
|
|
1190
|
+
function isCode(texts, language) {
|
|
1191
|
+
return language.isCode(texts.map((line) => line.replace(LIST_MARKER, "")).join("\n"));
|
|
1035
1192
|
}
|
|
1036
1193
|
function isCodeLine(text) {
|
|
1037
|
-
return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text)
|
|
1194
|
+
return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text);
|
|
1195
|
+
}
|
|
1196
|
+
var JAVASCRIPT = {
|
|
1197
|
+
name: "javascript",
|
|
1198
|
+
isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),
|
|
1199
|
+
isCodeLine: (text) => {
|
|
1200
|
+
return isCodeLine(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
|
|
1201
|
+
},
|
|
1202
|
+
legal: LEGAL_COMMENT,
|
|
1203
|
+
blockDirective: BLOCK_DIRECTIVE,
|
|
1204
|
+
tags: true,
|
|
1205
|
+
joinsCodeSpans: true
|
|
1206
|
+
};
|
|
1207
|
+
var RUST = {
|
|
1208
|
+
name: "rust",
|
|
1209
|
+
isCode: (text) => isRustCode(text),
|
|
1210
|
+
isCodeLine: (text) => {
|
|
1211
|
+
return isCodeLine(text) || /^let\s+(?:mut\s+)?\w+\s*[:=]/.test(text) || /^(?:pub(?:\([\w:\s]+\))?\s+)?(?:const|static(?:\s+mut)?)\s+[A-Z_\d]+\s*:/.test(text) || /^(?:pub(?:\([\w:\s]+\))?\s+)?use\s+[\w:]*::/.test(text) || /^#!?\[/.test(text);
|
|
1212
|
+
},
|
|
1213
|
+
legal: RUST_LEGAL_COMMENT,
|
|
1214
|
+
tags: false,
|
|
1215
|
+
joinsCodeSpans: false
|
|
1216
|
+
};
|
|
1217
|
+
function languageOf(filename) {
|
|
1218
|
+
return filename.endsWith(".rs") ? RUST : JAVASCRIPT;
|
|
1038
1219
|
}
|
|
1039
1220
|
function layoutReason(prose, raw) {
|
|
1040
1221
|
if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
|
|
@@ -1048,8 +1229,10 @@ function layoutReason(prose, raw) {
|
|
|
1048
1229
|
}
|
|
1049
1230
|
return void 0;
|
|
1050
1231
|
}
|
|
1232
|
+
var THEMATIC_BREAK = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
|
|
1051
1233
|
function isMarkup(prose) {
|
|
1052
|
-
|
|
1234
|
+
const text = prose.replace(LIST_MARKER, "");
|
|
1235
|
+
return /^#{1,6}(?:\s|$)/.test(text) || /^\[[^\]]+\]:/.test(text) || /^<(?:\/?[a-z]|[!?])/i.test(text) || text.startsWith(">") || /^(?:=+|-+)$/.test(prose) || THEMATIC_BREAK.test(prose);
|
|
1053
1236
|
}
|
|
1054
1237
|
function mask(text) {
|
|
1055
1238
|
return text.replaceAll(/[^\n]/g, "x");
|
|
@@ -1076,11 +1259,14 @@ function hasAlignedColumns(lines) {
|
|
|
1076
1259
|
}
|
|
1077
1260
|
return false;
|
|
1078
1261
|
}
|
|
1079
|
-
function structureOf(lines) {
|
|
1262
|
+
function structureOf(lines, language) {
|
|
1080
1263
|
const trimmed = lines.map((line) => line.text.trim()).join("\n");
|
|
1081
1264
|
const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
|
|
1082
1265
|
const proseLines = stripNonProse(trimmed).split("\n");
|
|
1083
|
-
if (isCode(
|
|
1266
|
+
if (isCode(
|
|
1267
|
+
lines.map((line) => line.text.trim()),
|
|
1268
|
+
language
|
|
1269
|
+
)) {
|
|
1084
1270
|
return { tier: "code", reason: "code" };
|
|
1085
1271
|
}
|
|
1086
1272
|
let layout;
|
|
@@ -1089,7 +1275,7 @@ function structureOf(lines) {
|
|
|
1089
1275
|
for (const [position, line] of texts.entries()) {
|
|
1090
1276
|
const prose = proseLines[position].trim();
|
|
1091
1277
|
layout ??= layoutReason(prose, lines[position]?.text ?? "");
|
|
1092
|
-
if (isCodeLine(line.trim())) {
|
|
1278
|
+
if (language.isCodeLine(line.trim())) {
|
|
1093
1279
|
codeLike = true;
|
|
1094
1280
|
} else if (isMarkup(prose)) {
|
|
1095
1281
|
manual ??= "markup";
|
|
@@ -1128,7 +1314,7 @@ var FenceTracker = class {
|
|
|
1128
1314
|
return true;
|
|
1129
1315
|
}
|
|
1130
1316
|
};
|
|
1131
|
-
function segment(lines) {
|
|
1317
|
+
function segment(lines, language) {
|
|
1132
1318
|
const paragraphs = [];
|
|
1133
1319
|
const fence = new FenceTracker();
|
|
1134
1320
|
let current;
|
|
@@ -1165,6 +1351,10 @@ function segment(lines) {
|
|
|
1165
1351
|
continue;
|
|
1166
1352
|
}
|
|
1167
1353
|
if (line.verbatim) {
|
|
1354
|
+
if (fence.next(text)) {
|
|
1355
|
+
start("code", line, "", "code block");
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1168
1358
|
start("code", line, "", "directive");
|
|
1169
1359
|
flush();
|
|
1170
1360
|
continue;
|
|
@@ -1189,7 +1379,7 @@ function segment(lines) {
|
|
|
1189
1379
|
}
|
|
1190
1380
|
continue;
|
|
1191
1381
|
}
|
|
1192
|
-
const tag = JSDOC_TAG.exec(text);
|
|
1382
|
+
const tag = language.tags ? JSDOC_TAG.exec(text) : null;
|
|
1193
1383
|
if (tag) {
|
|
1194
1384
|
start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
|
|
1195
1385
|
inCodeTag = CODE_TAGS.has(tag[1]);
|
|
@@ -1296,7 +1486,19 @@ function byLine(issues) {
|
|
|
1296
1486
|
function sameLayout(a, b) {
|
|
1297
1487
|
return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
|
|
1298
1488
|
}
|
|
1299
|
-
function
|
|
1489
|
+
function splitTokenLine(lines, tokens) {
|
|
1490
|
+
let next = 0;
|
|
1491
|
+
return lines.findIndex((words) => {
|
|
1492
|
+
const line = words.join(" ");
|
|
1493
|
+
let text = "";
|
|
1494
|
+
while (text.length < line.length && next < tokens.length) {
|
|
1495
|
+
text += `${text ? " " : ""}${tokens[next]}`;
|
|
1496
|
+
next += 1;
|
|
1497
|
+
}
|
|
1498
|
+
return text !== line;
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
function analyzeParagraph(paragraph, options, language) {
|
|
1300
1502
|
const first = paragraph.lines[0];
|
|
1301
1503
|
const last = paragraph.lines.at(-1) ?? first;
|
|
1302
1504
|
const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
|
|
@@ -1323,7 +1525,8 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1323
1525
|
const structure2 = structureOf(
|
|
1324
1526
|
prose.lines.map((line) => {
|
|
1325
1527
|
return { ...line, text: stripBlockSyntax(line.text) };
|
|
1326
|
-
})
|
|
1528
|
+
}),
|
|
1529
|
+
language
|
|
1327
1530
|
);
|
|
1328
1531
|
const issues2 = keptIssues(prose, structure2, options);
|
|
1329
1532
|
return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
|
|
@@ -1332,7 +1535,7 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1332
1535
|
const issues2 = keptIssues(paragraph, structure2, options);
|
|
1333
1536
|
return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
|
|
1334
1537
|
};
|
|
1335
|
-
const structure = structureOf(paragraph.lines);
|
|
1538
|
+
const structure = structureOf(paragraph.lines, language);
|
|
1336
1539
|
if (structure) {
|
|
1337
1540
|
return keep(structure);
|
|
1338
1541
|
}
|
|
@@ -1351,9 +1554,9 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1351
1554
|
parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
|
|
1352
1555
|
keepFirstLine: paragraph.kind === "tag"
|
|
1353
1556
|
};
|
|
1354
|
-
const layOut = (from, settings) => {
|
|
1355
|
-
const even2 = layoutTokens(from, firstLimit, restLimit, settings);
|
|
1356
|
-
return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
|
|
1557
|
+
const layOut = (from, settings, extra) => {
|
|
1558
|
+
const even2 = layoutTokens(from, firstLimit + extra, restLimit + extra, settings);
|
|
1559
|
+
return { even: even2, wrapped: breakAfterSentence(even2, firstLimit + extra, restLimit + extra, settings) };
|
|
1357
1560
|
};
|
|
1358
1561
|
const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
|
|
1359
1562
|
const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
|
|
@@ -1364,22 +1567,26 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1364
1567
|
if (!units.every((unit) => staysInLimit(unit))) {
|
|
1365
1568
|
return keep({ tier: "manual", reason: "unbreakable text across lines" });
|
|
1366
1569
|
}
|
|
1570
|
+
const text = currentLines.map((words) => words.join(" ")).join("\n");
|
|
1571
|
+
if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes("\n"))) {
|
|
1572
|
+
return keep({ tier: "manual", reason: "code span across lines" });
|
|
1573
|
+
}
|
|
1367
1574
|
const bodyOf = (lines) => {
|
|
1368
1575
|
return lines.map((words, position) => {
|
|
1369
|
-
const
|
|
1370
|
-
return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(
|
|
1576
|
+
const text2 = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
|
|
1577
|
+
return { ...first, text: text2, verbatim: false, hardBreak: HARD_BREAK.test(text2) };
|
|
1371
1578
|
});
|
|
1372
1579
|
};
|
|
1373
|
-
const settle = (settings) => {
|
|
1580
|
+
const settle = (settings, extra = 0) => {
|
|
1374
1581
|
let current = units;
|
|
1375
|
-
let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
|
|
1582
|
+
let { even: even2, wrapped: wrapped2 } = layOut(current, settings, extra);
|
|
1376
1583
|
for (; ; ) {
|
|
1377
1584
|
const body = bodyOf(wrapped2);
|
|
1378
|
-
const rewrap = structureOf(body);
|
|
1585
|
+
const rewrap = structureOf(body, language);
|
|
1379
1586
|
if (!rewrap) {
|
|
1380
1587
|
return { even: even2, wrapped: wrapped2 };
|
|
1381
1588
|
}
|
|
1382
|
-
const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
|
|
1589
|
+
const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== void 0);
|
|
1383
1590
|
const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
|
|
1384
1591
|
const pair = `${current[start - 1]} ${current[start]}`;
|
|
1385
1592
|
if (body.length < 2 || !staysInLimit(pair)) {
|
|
@@ -1387,16 +1594,25 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1387
1594
|
return { tier: "manual", reason: `rewrap would read as ${reading}` };
|
|
1388
1595
|
}
|
|
1389
1596
|
current = current.toSpliced(start - 1, 2, pair);
|
|
1390
|
-
({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
|
|
1597
|
+
({ even: even2, wrapped: wrapped2 } = layOut(current, settings, extra));
|
|
1391
1598
|
}
|
|
1392
1599
|
};
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1600
|
+
const layoutAt = (extra) => {
|
|
1601
|
+
const settings = {
|
|
1602
|
+
...layoutOptions,
|
|
1603
|
+
tolerance: layoutOptions.tolerance - extra,
|
|
1604
|
+
parentheticalOverflow: layoutOptions.parentheticalOverflow === void 0 ? void 0 : layoutOptions.parentheticalOverflow - extra
|
|
1605
|
+
};
|
|
1606
|
+
const settled = settle(settings, extra);
|
|
1607
|
+
return "wrapped" in settled ? settled : settle({ ...settings, keepSteps: true }, extra);
|
|
1608
|
+
};
|
|
1609
|
+
const layout = layoutAt(0);
|
|
1395
1610
|
if (!("wrapped" in layout)) {
|
|
1396
1611
|
return keep(layout);
|
|
1397
1612
|
}
|
|
1398
1613
|
const { even, wrapped } = layout;
|
|
1399
1614
|
const matchesLayout = sameLayout(wrapped, currentLines);
|
|
1615
|
+
const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
|
|
1400
1616
|
const lineIssues = [];
|
|
1401
1617
|
const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
|
|
1402
1618
|
if (!matchesLayout) {
|
|
@@ -1455,14 +1671,41 @@ function analyzeParagraph(paragraph, options) {
|
|
|
1455
1671
|
lineIssues.push({ type: "unbalanced", line });
|
|
1456
1672
|
}
|
|
1457
1673
|
}
|
|
1674
|
+
const splitLine = splitTokenLine(currentLines, tokens);
|
|
1675
|
+
if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {
|
|
1676
|
+
lineIssues.push({ type: "unbalanced", line: splitLine });
|
|
1677
|
+
}
|
|
1458
1678
|
}
|
|
1459
|
-
const
|
|
1679
|
+
const matchesWiderLayout = () => {
|
|
1680
|
+
for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {
|
|
1681
|
+
const wider = layoutAt(extra);
|
|
1682
|
+
if ("wrapped" in wider && sameLayout(wider.wrapped, currentLines)) {
|
|
1683
|
+
return true;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return false;
|
|
1687
|
+
};
|
|
1688
|
+
const accepted = lineIssues.some((issue) => isVerdict(issue)) && matchesWiderLayout();
|
|
1689
|
+
const suggested = accepted ? currentLines : wrapped;
|
|
1690
|
+
const suggestion = suggested.map(
|
|
1460
1691
|
(words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
|
|
1461
1692
|
);
|
|
1462
|
-
const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
|
|
1693
|
+
const wrapIssues = lineIssues.filter((issue) => !accepted || !isVerdict(issue)).map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
|
|
1463
1694
|
const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
|
|
1464
1695
|
return { ...base, reflowable: true, issues, suggestion };
|
|
1465
1696
|
}
|
|
1697
|
+
var LINE_COMMENT = "line comment";
|
|
1698
|
+
function analyzeLineParagraph(paragraph, options, language) {
|
|
1699
|
+
const first = paragraph.lines[0];
|
|
1700
|
+
const last = paragraph.lines.at(-1) ?? first;
|
|
1701
|
+
const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
|
|
1702
|
+
if (paragraph.kind === "code") {
|
|
1703
|
+
return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };
|
|
1704
|
+
}
|
|
1705
|
+
const tier = structureOf(paragraph.lines, language)?.tier;
|
|
1706
|
+
const issues = tier === "code" || tier === "layout" ? [] : overflowIssues(paragraph.lines, options);
|
|
1707
|
+
return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };
|
|
1708
|
+
}
|
|
1466
1709
|
function computeLineStarts(source) {
|
|
1467
1710
|
const starts = [0];
|
|
1468
1711
|
for (let index = 0; index < source.length; index += 1) {
|
|
@@ -1488,31 +1731,42 @@ function lineOf(lineStarts, offset) {
|
|
|
1488
1731
|
function stripCarriageReturn(line) {
|
|
1489
1732
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1490
1733
|
}
|
|
1491
|
-
function
|
|
1734
|
+
function blockKind(comment, language) {
|
|
1735
|
+
if (language.name === "rust") {
|
|
1736
|
+
return comment.doc ? `${comment.doc}-block-doc` : "block";
|
|
1737
|
+
}
|
|
1738
|
+
return comment.value.startsWith("*") ? "jsdoc" : "block";
|
|
1739
|
+
}
|
|
1740
|
+
function wholeComment(shape, lines, paragraphKind, reason) {
|
|
1741
|
+
const otherLines = lines.map((text, offset) => {
|
|
1742
|
+
return { index: shape.startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
|
|
1743
|
+
});
|
|
1744
|
+
return { ...shape, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
|
|
1745
|
+
}
|
|
1746
|
+
function isLegal(value, language) {
|
|
1747
|
+
return language.legal.test(replaceKeepingLines(value, CODE_SPAN, "``"));
|
|
1748
|
+
}
|
|
1749
|
+
function shapeBlock(comment, sourceLines, lineStarts, language) {
|
|
1492
1750
|
const startLine = lineOf(lineStarts, comment.start);
|
|
1493
1751
|
const endLine = lineOf(lineStarts, comment.end - 1);
|
|
1494
|
-
const
|
|
1752
|
+
const shape = { kind: blockKind(comment, language), startLine, endLine };
|
|
1495
1753
|
const lines = sourceLines.slice(startLine, endLine + 1);
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
});
|
|
1500
|
-
return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
|
|
1501
|
-
};
|
|
1502
|
-
const value = comment.value.replace(/^\*/, "");
|
|
1503
|
-
if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
|
|
1504
|
-
return wholeComment("code", "legal comment");
|
|
1754
|
+
const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\*/, "");
|
|
1755
|
+
if (isLegal(comment.value, language)) {
|
|
1756
|
+
return wholeComment(shape, lines, "code", "legal comment");
|
|
1505
1757
|
}
|
|
1506
|
-
if (LINE_DIRECTIVE.test(value) ||
|
|
1507
|
-
return wholeComment("code", "directive");
|
|
1758
|
+
if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {
|
|
1759
|
+
return wholeComment(shape, lines, "code", "directive");
|
|
1508
1760
|
}
|
|
1509
1761
|
const other = () => {
|
|
1510
1762
|
return wholeComment(
|
|
1763
|
+
shape,
|
|
1764
|
+
lines,
|
|
1511
1765
|
"other",
|
|
1512
1766
|
startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
|
|
1513
1767
|
);
|
|
1514
1768
|
};
|
|
1515
|
-
if (startLine === endLine || !/^\s
|
|
1769
|
+
if (startLine === endLine || !/^\s*\/\*[*!]?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
|
|
1516
1770
|
return other();
|
|
1517
1771
|
}
|
|
1518
1772
|
const body = [];
|
|
@@ -1531,7 +1785,52 @@ function shapeBlock(comment, sourceLines, lineStarts) {
|
|
|
1531
1785
|
hardBreak: HARD_BREAK.test(text)
|
|
1532
1786
|
});
|
|
1533
1787
|
}
|
|
1534
|
-
return {
|
|
1788
|
+
return { ...shape, paragraphs: segment(body, language) };
|
|
1789
|
+
}
|
|
1790
|
+
function shapeLineRun(group, sourceLines, lineStarts, language) {
|
|
1791
|
+
const startLine = lineOf(lineStarts, group[0].start);
|
|
1792
|
+
const endLine = startLine + group.length - 1;
|
|
1793
|
+
const kind = group[0].doc === "inner" ? "inner-line-doc" : "outer-line-doc";
|
|
1794
|
+
const shape = { kind, startLine, endLine };
|
|
1795
|
+
const lines = sourceLines.slice(startLine, endLine + 1);
|
|
1796
|
+
const values = group.map((comment) => comment.value.slice(1));
|
|
1797
|
+
if (isLegal(values.join("\n"), language)) {
|
|
1798
|
+
return wholeComment(shape, lines, "code", "legal comment");
|
|
1799
|
+
}
|
|
1800
|
+
if (LINE_DIRECTIVE.test(values[0])) {
|
|
1801
|
+
return wholeComment(shape, lines, "code", "directive");
|
|
1802
|
+
}
|
|
1803
|
+
const body = lines.map((line, offset) => {
|
|
1804
|
+
const [, marker, space, text] = /^(\s*\/\/[/!])( ?)(.*)$/.exec(line) ?? ["", "", "", line];
|
|
1805
|
+
return {
|
|
1806
|
+
index: startLine + offset,
|
|
1807
|
+
prefix: `${marker} `,
|
|
1808
|
+
text: text.trimEnd(),
|
|
1809
|
+
verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
|
|
1810
|
+
hardBreak: HARD_BREAK.test(text)
|
|
1811
|
+
};
|
|
1812
|
+
});
|
|
1813
|
+
return { ...shape, paragraphs: segment(body, language) };
|
|
1814
|
+
}
|
|
1815
|
+
function shapeLineComment(group, sourceLines, lineStarts, language) {
|
|
1816
|
+
const startLine = lineOf(lineStarts, group[0].start);
|
|
1817
|
+
const endLine = startLine + group.length - 1;
|
|
1818
|
+
const shape = { kind: "line", startLine, endLine };
|
|
1819
|
+
const lines = sourceLines.slice(startLine, endLine + 1);
|
|
1820
|
+
if (isLegal(group.map((comment) => comment.value).join("\n"), language)) {
|
|
1821
|
+
return wholeComment(shape, lines, "code", "legal comment");
|
|
1822
|
+
}
|
|
1823
|
+
const body = lines.map((line, offset) => {
|
|
1824
|
+
const [, marker, space, text] = /^(\s*\/\/[/!]*)( ?)(.*)$/.exec(line) ?? ["", "", "", line];
|
|
1825
|
+
return {
|
|
1826
|
+
index: startLine + offset,
|
|
1827
|
+
prefix: `${marker}${space}`,
|
|
1828
|
+
text: text.trimEnd(),
|
|
1829
|
+
verbatim: LINE_DIRECTIVE.test(text),
|
|
1830
|
+
hardBreak: HARD_BREAK.test(text)
|
|
1831
|
+
};
|
|
1832
|
+
});
|
|
1833
|
+
return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };
|
|
1535
1834
|
}
|
|
1536
1835
|
function ownsLines(comment, source, lineStarts) {
|
|
1537
1836
|
const startLine = lineOf(lineStarts, comment.start);
|
|
@@ -1541,12 +1840,12 @@ function ownsLines(comment, source, lineStarts) {
|
|
|
1541
1840
|
const after = source.slice(comment.end, lineEnd);
|
|
1542
1841
|
return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
|
|
1543
1842
|
}
|
|
1544
|
-
function singleLineLength(paragraph, options) {
|
|
1843
|
+
function singleLineLength(paragraph, options, language) {
|
|
1545
1844
|
const [first] = paragraph.lines;
|
|
1546
1845
|
if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
|
|
1547
1846
|
return sourceLineLength(first, options.tabWidth);
|
|
1548
1847
|
}
|
|
1549
|
-
if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
|
|
1848
|
+
if (paragraph.kind !== "prose" || structureOf(paragraph.lines, language)) {
|
|
1550
1849
|
return Infinity;
|
|
1551
1850
|
}
|
|
1552
1851
|
const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
|
|
@@ -1561,33 +1860,73 @@ function describeFailure(errors, lineStarts) {
|
|
|
1561
1860
|
const offset = first.labels.at(0)?.start;
|
|
1562
1861
|
return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
|
|
1563
1862
|
}
|
|
1863
|
+
function scanJavaScript(filename, text, lineStarts) {
|
|
1864
|
+
const { program, comments, errors } = parseSource(filename, text);
|
|
1865
|
+
if (errors.length > 0 && program.body.length === 0) {
|
|
1866
|
+
return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
comments: comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")))
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function scanRustSource(text, lineStarts) {
|
|
1873
|
+
const { comments, error } = scanRust(text);
|
|
1874
|
+
if (error) {
|
|
1875
|
+
return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
comments: comments.map((comment) => {
|
|
1879
|
+
return {
|
|
1880
|
+
type: comment.block ? "Block" : "Line",
|
|
1881
|
+
value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),
|
|
1882
|
+
start: comment.start,
|
|
1883
|
+
end: comment.end,
|
|
1884
|
+
doc: comment.doc
|
|
1885
|
+
};
|
|
1886
|
+
})
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function groupLineRuns(comments, text, lineStarts, markerOf) {
|
|
1890
|
+
const groups = [];
|
|
1891
|
+
let current;
|
|
1892
|
+
let previousLine = -1;
|
|
1893
|
+
let indent = "";
|
|
1894
|
+
let marker;
|
|
1895
|
+
for (const comment of comments) {
|
|
1896
|
+
const next = comment.type === "Line" ? markerOf(comment) : void 0;
|
|
1897
|
+
if (next === void 0 || !ownsLines(comment, text, lineStarts)) {
|
|
1898
|
+
current = void 0;
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
const line = lineOf(lineStarts, comment.start);
|
|
1902
|
+
const before = text.slice(lineStarts[line], comment.start);
|
|
1903
|
+
if (current && line === previousLine + 1 && next === marker && before === indent) {
|
|
1904
|
+
current.push(comment);
|
|
1905
|
+
} else {
|
|
1906
|
+
current = [comment];
|
|
1907
|
+
indent = before;
|
|
1908
|
+
marker = next;
|
|
1909
|
+
groups.push(current);
|
|
1910
|
+
}
|
|
1911
|
+
previousLine = line;
|
|
1912
|
+
}
|
|
1913
|
+
return groups;
|
|
1914
|
+
}
|
|
1564
1915
|
function inspectSource(source, filename, options) {
|
|
1565
1916
|
if (options.maxWidth !== void 0 && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {
|
|
1566
1917
|
throw new Error("comments: maxWidth must be an integer at least as large as width");
|
|
1567
1918
|
}
|
|
1568
1919
|
const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
|
|
1569
|
-
const
|
|
1920
|
+
const language = languageOf(filename);
|
|
1570
1921
|
const lineStarts = computeLineStarts(text);
|
|
1571
|
-
|
|
1572
|
-
|
|
1922
|
+
const scanned = language.name === "rust" ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);
|
|
1923
|
+
if (scanned.syntaxError) {
|
|
1924
|
+
return { comments: [], syntaxError: scanned.syntaxError };
|
|
1573
1925
|
}
|
|
1574
1926
|
const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
|
|
1575
|
-
const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
|
|
1576
1927
|
const reports = [];
|
|
1577
|
-
const
|
|
1578
|
-
return
|
|
1579
|
-
});
|
|
1580
|
-
for (const block of blocks) {
|
|
1581
|
-
const shape = shapeBlock(block, sourceLines, lineStarts);
|
|
1582
|
-
const singleLineWidth = Math.max(
|
|
1583
|
-
options.width,
|
|
1584
|
-
Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
|
|
1585
|
-
);
|
|
1586
|
-
const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
|
|
1587
|
-
const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
|
|
1588
|
-
const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
|
|
1589
|
-
const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
|
|
1590
|
-
reports.push({
|
|
1928
|
+
const report = (shape, lines, paragraphs) => {
|
|
1929
|
+
return {
|
|
1591
1930
|
file: filename,
|
|
1592
1931
|
kind: shape.kind,
|
|
1593
1932
|
startLine: shape.startLine + 1,
|
|
@@ -1597,8 +1936,47 @@ function inspectSource(source, filename, options) {
|
|
|
1597
1936
|
maxWidth: options.maxWidth,
|
|
1598
1937
|
lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
|
|
1599
1938
|
text: lines.join("\n"),
|
|
1600
|
-
paragraphs
|
|
1939
|
+
paragraphs
|
|
1940
|
+
};
|
|
1941
|
+
};
|
|
1942
|
+
const blocks = scanned.comments.filter((comment) => {
|
|
1943
|
+
return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
|
|
1944
|
+
});
|
|
1945
|
+
const runs = (markerOf) => {
|
|
1946
|
+
return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {
|
|
1947
|
+
return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));
|
|
1601
1948
|
});
|
|
1949
|
+
};
|
|
1950
|
+
const shapes = [
|
|
1951
|
+
...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),
|
|
1952
|
+
...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),
|
|
1953
|
+
...runs((comment) => comment.doc ? void 0 : "//").map(
|
|
1954
|
+
(group) => shapeLineComment(group, sourceLines, lineStarts, language)
|
|
1955
|
+
)
|
|
1956
|
+
].toSorted((a, b) => a.startLine - b.startLine);
|
|
1957
|
+
for (const shape of shapes) {
|
|
1958
|
+
const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
|
|
1959
|
+
if (shape.kind === "line") {
|
|
1960
|
+
const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));
|
|
1961
|
+
if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {
|
|
1962
|
+
reports.push(report(shape, lines, paragraphs));
|
|
1963
|
+
}
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const singleLineWidth = Math.max(
|
|
1967
|
+
options.width,
|
|
1968
|
+
Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
|
|
1969
|
+
);
|
|
1970
|
+
const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
|
|
1971
|
+
const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;
|
|
1972
|
+
const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
|
|
1973
|
+
reports.push(
|
|
1974
|
+
report(
|
|
1975
|
+
shape,
|
|
1976
|
+
lines,
|
|
1977
|
+
shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language))
|
|
1978
|
+
)
|
|
1979
|
+
);
|
|
1602
1980
|
}
|
|
1603
1981
|
return { comments: reports };
|
|
1604
1982
|
}
|
|
@@ -1619,6 +1997,9 @@ function rewrapComment(report) {
|
|
|
1619
1997
|
}
|
|
1620
1998
|
var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
|
|
1621
1999
|
function codeFingerprint(source, filename) {
|
|
2000
|
+
if (languageOf(filename).name === "rust") {
|
|
2001
|
+
return rustFingerprint(source);
|
|
2002
|
+
}
|
|
1622
2003
|
const { program, errors } = parseSource(filename, source);
|
|
1623
2004
|
if (errors.length > 0) {
|
|
1624
2005
|
return JSON.stringify({ source, errors: errors.map((error) => error.message) });
|
|
@@ -1630,11 +2011,11 @@ function codeFingerprint(source, filename) {
|
|
|
1630
2011
|
}
|
|
1631
2012
|
|
|
1632
2013
|
// src/comments/config.ts
|
|
1633
|
-
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
1634
|
-
import { createRequire } from "module";
|
|
1635
|
-
import
|
|
2014
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2015
|
+
import { createRequire as createRequire2 } from "module";
|
|
2016
|
+
import path3 from "path";
|
|
1636
2017
|
import { pathToFileURL } from "url";
|
|
1637
|
-
var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
|
|
2018
|
+
var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".rs"]);
|
|
1638
2019
|
var CONFIG_FILES = [
|
|
1639
2020
|
"comments.config.ts",
|
|
1640
2021
|
"comments.config.mts",
|
|
@@ -1682,10 +2063,12 @@ function validateConfig(raw, filepath) {
|
|
|
1682
2063
|
assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
|
|
1683
2064
|
assertType(config.style, "boolean", "style", filepath);
|
|
1684
2065
|
assertType(config.extensions, "array", "extensions", filepath);
|
|
2066
|
+
assertType(config.include, "array", "include", filepath);
|
|
1685
2067
|
assertType(config.ignore, "array", "ignore", filepath);
|
|
1686
2068
|
assertType(config.agent, "object", "agent", filepath);
|
|
1687
2069
|
assertType(config.hook, "object", "hook", filepath);
|
|
1688
2070
|
assertStrings(config.extensions, "extensions", filepath);
|
|
2071
|
+
assertStrings(config.include, "include", filepath);
|
|
1689
2072
|
assertStrings(config.ignore, "ignore", filepath);
|
|
1690
2073
|
for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
|
|
1691
2074
|
const value = config[key];
|
|
@@ -1718,8 +2101,8 @@ function validateConfig(raw, filepath) {
|
|
|
1718
2101
|
}
|
|
1719
2102
|
assertType(agent.rules, "string", "agent.rules", filepath);
|
|
1720
2103
|
assertType(agent.instructions, "string", "agent.instructions", filepath);
|
|
1721
|
-
if (agent.scope !== void 0 &&
|
|
1722
|
-
throw new Error(`comments: \`agent.scope\` in ${filepath} must be
|
|
2104
|
+
if (agent.scope !== void 0 && !["suspicious", "failing", "all"].includes(agent.scope)) {
|
|
2105
|
+
throw new Error(`comments: \`agent.scope\` in ${filepath} must be one of suspicious, failing, all`);
|
|
1723
2106
|
}
|
|
1724
2107
|
const hook = config.hook ?? {};
|
|
1725
2108
|
if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
|
|
@@ -1729,25 +2112,25 @@ function validateConfig(raw, filepath) {
|
|
|
1729
2112
|
}
|
|
1730
2113
|
async function loadConfig(cwd, options = {}) {
|
|
1731
2114
|
if (options.explicitPath) {
|
|
1732
|
-
const filepath =
|
|
2115
|
+
const filepath = path3.resolve(cwd, options.explicitPath);
|
|
1733
2116
|
return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
|
|
1734
2117
|
}
|
|
1735
|
-
let directory =
|
|
2118
|
+
let directory = path3.resolve(cwd);
|
|
1736
2119
|
for (; ; ) {
|
|
1737
2120
|
for (const name of CONFIG_FILES) {
|
|
1738
|
-
const filepath =
|
|
1739
|
-
if (
|
|
2121
|
+
const filepath = path3.join(directory, name);
|
|
2122
|
+
if (existsSync2(filepath)) {
|
|
1740
2123
|
return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
|
|
1741
2124
|
}
|
|
1742
2125
|
}
|
|
1743
|
-
const packageJsonPath =
|
|
1744
|
-
if (
|
|
2126
|
+
const packageJsonPath = path3.join(directory, "package.json");
|
|
2127
|
+
if (existsSync2(packageJsonPath)) {
|
|
1745
2128
|
const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
|
|
1746
2129
|
if (value !== void 0) {
|
|
1747
2130
|
return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
|
|
1748
2131
|
}
|
|
1749
2132
|
}
|
|
1750
|
-
const parent =
|
|
2133
|
+
const parent = path3.dirname(directory);
|
|
1751
2134
|
if (directory === options.stopDir || parent === directory) {
|
|
1752
2135
|
return { config: {} };
|
|
1753
2136
|
}
|
|
@@ -1759,8 +2142,8 @@ function readPrettierFile(directory, stopDir) {
|
|
|
1759
2142
|
let current = directory;
|
|
1760
2143
|
for (; ; ) {
|
|
1761
2144
|
for (const name of PRETTIER_FILES) {
|
|
1762
|
-
const filepath =
|
|
1763
|
-
if (!
|
|
2145
|
+
const filepath = path3.join(current, name);
|
|
2146
|
+
if (!existsSync2(filepath)) {
|
|
1764
2147
|
continue;
|
|
1765
2148
|
}
|
|
1766
2149
|
const text = readFileSync2(filepath, "utf8");
|
|
@@ -1771,14 +2154,14 @@ function readPrettierFile(directory, stopDir) {
|
|
|
1771
2154
|
tabWidth: tabWidth === void 0 ? void 0 : Number(tabWidth)
|
|
1772
2155
|
};
|
|
1773
2156
|
}
|
|
1774
|
-
const packageJsonPath =
|
|
1775
|
-
if (
|
|
2157
|
+
const packageJsonPath = path3.join(current, "package.json");
|
|
2158
|
+
if (existsSync2(packageJsonPath)) {
|
|
1776
2159
|
const prettier = readJson(packageJsonPath).prettier;
|
|
1777
2160
|
if (prettier && typeof prettier === "object") {
|
|
1778
2161
|
return prettier;
|
|
1779
2162
|
}
|
|
1780
2163
|
}
|
|
1781
|
-
const parent =
|
|
2164
|
+
const parent = path3.dirname(current);
|
|
1782
2165
|
if (current === stopDir || parent === current) {
|
|
1783
2166
|
return void 0;
|
|
1784
2167
|
}
|
|
@@ -1797,7 +2180,7 @@ var PrettierOptionsResolver = class {
|
|
|
1797
2180
|
return this._prettier;
|
|
1798
2181
|
}
|
|
1799
2182
|
try {
|
|
1800
|
-
const require2 =
|
|
2183
|
+
const require2 = createRequire2(path3.join(this._root, "package.json"));
|
|
1801
2184
|
const resolved = require2.resolve("prettier");
|
|
1802
2185
|
this._prettier = await import(pathToFileURL(resolved).href);
|
|
1803
2186
|
} catch {
|
|
@@ -1816,7 +2199,7 @@ var PrettierOptionsResolver = class {
|
|
|
1816
2199
|
} catch {
|
|
1817
2200
|
}
|
|
1818
2201
|
}
|
|
1819
|
-
const directory =
|
|
2202
|
+
const directory = path3.dirname(file);
|
|
1820
2203
|
const cached = this._cache.get(directory);
|
|
1821
2204
|
if (cached) {
|
|
1822
2205
|
return cached;
|
|
@@ -1826,9 +2209,45 @@ var PrettierOptionsResolver = class {
|
|
|
1826
2209
|
return options;
|
|
1827
2210
|
}
|
|
1828
2211
|
};
|
|
2212
|
+
var RUSTFMT_FILES = ["rustfmt.toml", ".rustfmt.toml"];
|
|
2213
|
+
var RUSTFMT_TAB_SPACES = 4;
|
|
2214
|
+
var RustfmtOptionsResolver = class {
|
|
2215
|
+
constructor(_root) {
|
|
2216
|
+
this._root = _root;
|
|
2217
|
+
}
|
|
2218
|
+
_root;
|
|
2219
|
+
_cache = /* @__PURE__ */ new Map();
|
|
2220
|
+
_read(directory) {
|
|
2221
|
+
const cached = this._cache.get(directory);
|
|
2222
|
+
if (cached) {
|
|
2223
|
+
return cached;
|
|
2224
|
+
}
|
|
2225
|
+
let options;
|
|
2226
|
+
for (const name of RUSTFMT_FILES) {
|
|
2227
|
+
const filepath = path3.join(directory, name);
|
|
2228
|
+
if (existsSync2(filepath)) {
|
|
2229
|
+
const text = readFileSync2(filepath, "utf8");
|
|
2230
|
+
const commentWidth = /^\s*comment_width\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
2231
|
+
const tabSpaces = /^\s*tab_spaces\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
2232
|
+
options = {
|
|
2233
|
+
printWidth: commentWidth === void 0 ? void 0 : Number(commentWidth),
|
|
2234
|
+
tabWidth: tabSpaces === void 0 ? RUSTFMT_TAB_SPACES : Number(tabSpaces)
|
|
2235
|
+
};
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const parent = path3.dirname(directory);
|
|
2240
|
+
options ??= directory === this._root || parent === directory ? { tabWidth: RUSTFMT_TAB_SPACES } : this._read(parent);
|
|
2241
|
+
this._cache.set(directory, options);
|
|
2242
|
+
return options;
|
|
2243
|
+
}
|
|
2244
|
+
resolve(file) {
|
|
2245
|
+
return this._read(path3.dirname(file));
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
1829
2248
|
|
|
1830
2249
|
// src/comments/flow.ts
|
|
1831
|
-
import { existsSync as
|
|
2250
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1832
2251
|
var DEFAULT_BATCH_SIZE = 20;
|
|
1833
2252
|
var DEFAULT_MAX_FILES = 10;
|
|
1834
2253
|
var DEFAULT_MAX_AGENTS = 6;
|
|
@@ -1856,6 +2275,11 @@ function needsAgent(comment) {
|
|
|
1856
2275
|
(paragraph) => paragraph.issues.some((issue) => issue.tier === "hint" || paragraph.suggestion === null)
|
|
1857
2276
|
);
|
|
1858
2277
|
}
|
|
2278
|
+
var SELECTORS = {
|
|
2279
|
+
suspicious: needsAgent,
|
|
2280
|
+
failing: hasVerdictIssues,
|
|
2281
|
+
all: () => true
|
|
2282
|
+
};
|
|
1859
2283
|
function seconds(milliseconds) {
|
|
1860
2284
|
return `${Math.round(milliseconds / 1e3)}s`;
|
|
1861
2285
|
}
|
|
@@ -2000,9 +2424,9 @@ async function runFlow(project, scope, options) {
|
|
|
2000
2424
|
skipped: skippedFiles(analyses)
|
|
2001
2425
|
};
|
|
2002
2426
|
}
|
|
2427
|
+
const select2 = SELECTORS[options.select];
|
|
2003
2428
|
const pending = analyses.map((analysis) => {
|
|
2004
|
-
|
|
2005
|
-
return { ...analysis, comments: selected };
|
|
2429
|
+
return { ...analysis, comments: analysis.comments.filter((comment) => select2(comment)) };
|
|
2006
2430
|
}).filter((analysis) => analysis.comments.length > 0);
|
|
2007
2431
|
const comments = pending.flatMap((analysis) => analysis.comments);
|
|
2008
2432
|
if (comments.length === 0) {
|
|
@@ -2070,7 +2494,7 @@ async function runFlow(project, scope, options) {
|
|
|
2070
2494
|
const handedOver = new Set(pending.map((analysis) => analysis.file));
|
|
2071
2495
|
const changedElsewhere = before && after ? changedBetween(before, after).filter((file) => !handedOver.has(file)) : [];
|
|
2072
2496
|
const codeChanged = pending.filter((analysis) => {
|
|
2073
|
-
if (!
|
|
2497
|
+
if (!existsSync3(analysis.absolute)) {
|
|
2074
2498
|
return true;
|
|
2075
2499
|
}
|
|
2076
2500
|
const current = readFileSync3(analysis.absolute, "utf8");
|
|
@@ -2127,7 +2551,7 @@ function describeSummary(summary) {
|
|
|
2127
2551
|
import { spawnSync } from "child_process";
|
|
2128
2552
|
import { createHash } from "crypto";
|
|
2129
2553
|
import { readFileSync as readFileSync4, statSync } from "fs";
|
|
2130
|
-
import
|
|
2554
|
+
import path4 from "path";
|
|
2131
2555
|
function git(cwd, args) {
|
|
2132
2556
|
const result = spawnSync("git", ["-c", "core.quotePath=false", ...args], {
|
|
2133
2557
|
cwd,
|
|
@@ -2237,7 +2661,7 @@ function snapshotChanges(root) {
|
|
|
2237
2661
|
try {
|
|
2238
2662
|
const tracked = hasHead(root) ? splitPaths(gitOrThrow(root, ["diff", "-z", "--name-only", "--no-renames", "HEAD"])) : splitPaths(gitOrThrow(root, ["ls-files", "-z", "--cached"]));
|
|
2239
2663
|
const files = /* @__PURE__ */ new Set([...tracked, ...listUntrackedFiles(root)]);
|
|
2240
|
-
return new Map([...files].map((file) => [file, fileState(
|
|
2664
|
+
return new Map([...files].map((file) => [file, fileState(path4.join(root, file))]));
|
|
2241
2665
|
} catch {
|
|
2242
2666
|
return void 0;
|
|
2243
2667
|
}
|
|
@@ -2256,15 +2680,15 @@ function getChangedLines(root, scope = {}) {
|
|
|
2256
2680
|
}
|
|
2257
2681
|
|
|
2258
2682
|
// src/comments/project.ts
|
|
2259
|
-
import { existsSync as
|
|
2260
|
-
import
|
|
2683
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
2684
|
+
import path5 from "path";
|
|
2261
2685
|
var DEFAULT_WIDTH = 80;
|
|
2262
2686
|
var DEFAULT_MAX_WIDTH = 120;
|
|
2263
2687
|
var DEFAULT_SINGLE_LINE_WIDTH = 120;
|
|
2264
2688
|
var DEFAULT_TAB_WIDTH = 2;
|
|
2265
2689
|
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
|
|
2266
2690
|
function toPosix(file) {
|
|
2267
|
-
return file.split(
|
|
2691
|
+
return file.split(path5.sep).join("/");
|
|
2268
2692
|
}
|
|
2269
2693
|
function mergeFilter(existing, next) {
|
|
2270
2694
|
if (existing === "all" || next === "all") {
|
|
@@ -2277,10 +2701,10 @@ function walkDirectory(directory, files = []) {
|
|
|
2277
2701
|
for (const entry of entries) {
|
|
2278
2702
|
if (entry.isDirectory()) {
|
|
2279
2703
|
if (!SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
2280
|
-
walkDirectory(
|
|
2704
|
+
walkDirectory(path5.join(directory, entry.name), files);
|
|
2281
2705
|
}
|
|
2282
2706
|
} else if (entry.isFile()) {
|
|
2283
|
-
files.push(
|
|
2707
|
+
files.push(path5.join(directory, entry.name));
|
|
2284
2708
|
}
|
|
2285
2709
|
}
|
|
2286
2710
|
return files;
|
|
@@ -2302,41 +2726,45 @@ var Project = class {
|
|
|
2302
2726
|
this.config = config;
|
|
2303
2727
|
this._options = _options;
|
|
2304
2728
|
this._prettier = new PrettierOptionsResolver(root);
|
|
2729
|
+
this._rustfmt = new RustfmtOptionsResolver(root);
|
|
2305
2730
|
this._extensions = new Set(config.extensions ?? DEFAULT_EXTENSIONS);
|
|
2306
2731
|
}
|
|
2307
2732
|
root;
|
|
2308
2733
|
config;
|
|
2309
2734
|
_options;
|
|
2310
2735
|
_prettier;
|
|
2736
|
+
_rustfmt;
|
|
2311
2737
|
_extensions;
|
|
2312
2738
|
relative(absolute) {
|
|
2313
|
-
return toPosix(
|
|
2739
|
+
return toPosix(path5.relative(this.root, absolute));
|
|
2314
2740
|
}
|
|
2315
2741
|
_isIncluded(absolute) {
|
|
2316
|
-
if (!this._extensions.has(
|
|
2742
|
+
if (!this._extensions.has(path5.extname(absolute))) {
|
|
2317
2743
|
return false;
|
|
2318
2744
|
}
|
|
2319
2745
|
const relative = this.relative(absolute);
|
|
2320
|
-
|
|
2746
|
+
const matches = (patterns) => patterns.some((pattern) => path5.matchesGlob(relative, pattern));
|
|
2747
|
+
const include = this.config.include ?? [];
|
|
2748
|
+
return (include.length === 0 || matches(include)) && !matches(this.config.ignore ?? []);
|
|
2321
2749
|
}
|
|
2322
2750
|
_filesIn(directory) {
|
|
2323
2751
|
if (!this._options.isGitRepo) {
|
|
2324
2752
|
return walkDirectory(directory);
|
|
2325
2753
|
}
|
|
2326
|
-
return listFiles2(this.root, [
|
|
2754
|
+
return listFiles2(this.root, [path5.relative(this.root, directory) || "."]).map((file) => path5.join(this.root, file));
|
|
2327
2755
|
}
|
|
2328
2756
|
collect(scope) {
|
|
2329
2757
|
const files = /* @__PURE__ */ new Map();
|
|
2330
2758
|
const add = (absolute, filter) => {
|
|
2331
|
-
if (this._isIncluded(absolute) &&
|
|
2759
|
+
if (this._isIncluded(absolute) && existsSync4(absolute)) {
|
|
2332
2760
|
files.set(absolute, mergeFilter(files.get(absolute), filter));
|
|
2333
2761
|
}
|
|
2334
2762
|
};
|
|
2335
2763
|
if (scope.targets && scope.targets.length > 0) {
|
|
2336
2764
|
for (const target of scope.targets) {
|
|
2337
2765
|
const range = /^(.+):(\d+)(?:-(\d+))?$/.exec(target);
|
|
2338
|
-
const absolute =
|
|
2339
|
-
if (!
|
|
2766
|
+
const absolute = path5.resolve(this._options.cwd, range ? range[1] : target);
|
|
2767
|
+
if (!existsSync4(absolute)) {
|
|
2340
2768
|
throw new Error(`comments: ${target} does not exist`);
|
|
2341
2769
|
}
|
|
2342
2770
|
if (statSync2(absolute).isDirectory()) {
|
|
@@ -2365,21 +2793,22 @@ var Project = class {
|
|
|
2365
2793
|
throw new Error("comments: not a git repository, so pass files or directories explicitly");
|
|
2366
2794
|
}
|
|
2367
2795
|
for (const [file, lines] of getChangedLines(this.root, scope)) {
|
|
2368
|
-
add(
|
|
2796
|
+
add(path5.join(this.root, file), scope.wholeFiles ? "all" : lines);
|
|
2369
2797
|
}
|
|
2370
2798
|
return files;
|
|
2371
2799
|
}
|
|
2372
2800
|
async analyzeFile(absolute, filter = "all") {
|
|
2373
2801
|
const source = readFileSync5(absolute, "utf8");
|
|
2374
|
-
const
|
|
2802
|
+
const isRust = absolute.endsWith(".rs");
|
|
2803
|
+
const formatter = isRust ? this._rustfmt.resolve(absolute) : await this._prettier.resolve(absolute);
|
|
2375
2804
|
const file = this.relative(absolute);
|
|
2376
|
-
const width = this._options.width ?? this.config.width ??
|
|
2805
|
+
const width = this._options.width ?? this.config.width ?? formatter.printWidth ?? DEFAULT_WIDTH;
|
|
2377
2806
|
const maxWidth = this.config.maxWidth ?? Math.max(width, this.config.tolerance === void 0 ? DEFAULT_MAX_WIDTH : width + this.config.tolerance);
|
|
2378
2807
|
const { comments, syntaxError } = inspectSource(source, file, {
|
|
2379
2808
|
width,
|
|
2380
2809
|
maxWidth,
|
|
2381
2810
|
singleLineWidth: this.config.singleLineWidth ?? DEFAULT_SINGLE_LINE_WIDTH,
|
|
2382
|
-
tabWidth:
|
|
2811
|
+
tabWidth: formatter.tabWidth ?? DEFAULT_TAB_WIDTH,
|
|
2383
2812
|
tolerance: Math.min(this.config.tolerance ?? 0, maxWidth - width),
|
|
2384
2813
|
avoidSentenceOrphans: this.config.avoidSentenceOrphans ?? true,
|
|
2385
2814
|
style: this.config.style ?? true
|
|
@@ -2862,7 +3291,7 @@ ${this._paint("gray", S_BAR_END)} ${message}
|
|
|
2862
3291
|
var HELP = `
|
|
2863
3292
|
Usage: comments <command> [targets...] [options]
|
|
2864
3293
|
|
|
2865
|
-
Finds comments in JavaScript and
|
|
3294
|
+
Finds comments in JavaScript, TypeScript, and Rust files and measures how their prose fits the configured width.
|
|
2866
3295
|
Only agent and hook change files, by applying the exact rewrap to comments that still have layout issues.
|
|
2867
3296
|
|
|
2868
3297
|
Commands:
|
|
@@ -2882,6 +3311,7 @@ Options:
|
|
|
2882
3311
|
--suspicious Only comments with layout issues (overflow, early line ends, last lines of one or two words)
|
|
2883
3312
|
or hints (possible orphans, em-dashes, banners, ...). For agent and hook (the default), only
|
|
2884
3313
|
comments with hints or with issues in parts the tool never rewraps, a triage, not a full review
|
|
3314
|
+
--failing agent, hook: hand the agent only the comments that fail check, and none that only have hints
|
|
2885
3315
|
--all agent, hook: hand every comment in scope to the agent for a full review of its wording
|
|
2886
3316
|
--verbose agent, hook: print every agent's reply (otherwise a terminal offers to open them)
|
|
2887
3317
|
--no-interactive agent: show the agents while they run, then exit without offering to open their replies
|
|
@@ -2897,12 +3327,14 @@ Config overrides (take precedence over the config file):
|
|
|
2897
3327
|
--width <n> Preferred comment line length
|
|
2898
3328
|
--max-width <n> Hard limit for prose (default: 120, never below width)
|
|
2899
3329
|
--single-line-width <n> Longest line a one-paragraph comment may stay on (default: 120)
|
|
2900
|
-
--tolerance <n> Columns prose may go past width to save a line, within max-width (default: 0)
|
|
3330
|
+
--tolerance <n> Columns prose may go past width to save a line, within max-width (default: 0).
|
|
3331
|
+
A paragraph that has the layout for a width up to that much wider also passes
|
|
2901
3332
|
--[no-]orphans Keep a sentence's first word off the end of a line, and end the first of two lines
|
|
2902
3333
|
with a sentence when the second is no longer than the first (default: on)
|
|
2903
3334
|
--[no-]style Look for possible semicolons, em-dashes, dashes, label colons, emphasis, and banners
|
|
2904
3335
|
as hints (default: on). The agent's rules apply either way
|
|
2905
|
-
--
|
|
3336
|
+
--include <glob> Only consider matching files, repeatable. Replaces include from the config file
|
|
3337
|
+
--ignore <glob> Skip matching files, repeatable. Adds to ignore from the config file
|
|
2906
3338
|
--model <name> agent, hook: model for the agent
|
|
2907
3339
|
--timeout <seconds> agent, hook: how long to wait for each agent (default: 300)
|
|
2908
3340
|
--batch-size <n> agent, hook: most comments per agent (default: 20)
|
|
@@ -2916,6 +3348,7 @@ var OPTIONS = {
|
|
|
2916
3348
|
tolerance: { type: "string" },
|
|
2917
3349
|
orphans: { type: "boolean" },
|
|
2918
3350
|
style: { type: "boolean" },
|
|
3351
|
+
include: { type: "string", multiple: true },
|
|
2919
3352
|
ignore: { type: "string", multiple: true },
|
|
2920
3353
|
model: { type: "string" },
|
|
2921
3354
|
timeout: { type: "string" },
|
|
@@ -2927,6 +3360,7 @@ var OPTIONS = {
|
|
|
2927
3360
|
"whole-files": { type: "boolean" },
|
|
2928
3361
|
repo: { type: "boolean" },
|
|
2929
3362
|
suspicious: { type: "boolean" },
|
|
3363
|
+
failing: { type: "boolean" },
|
|
2930
3364
|
all: { type: "boolean" },
|
|
2931
3365
|
verbose: { type: "boolean" },
|
|
2932
3366
|
interactive: { type: "boolean" },
|
|
@@ -2963,8 +3397,8 @@ function log2(message) {
|
|
|
2963
3397
|
`);
|
|
2964
3398
|
}
|
|
2965
3399
|
function resolveCwd(directory, source = "--cwd") {
|
|
2966
|
-
const resolved =
|
|
2967
|
-
if (!
|
|
3400
|
+
const resolved = path6.resolve(directory);
|
|
3401
|
+
if (!existsSync5(resolved) || !statSync3(resolved).isDirectory()) {
|
|
2968
3402
|
throw new Error(`comments: ${source} ${directory} is not a directory`);
|
|
2969
3403
|
}
|
|
2970
3404
|
return realpathSync(resolved);
|
|
@@ -2990,6 +3424,7 @@ function applyOverrides(config, values) {
|
|
|
2990
3424
|
tolerance: parseInteger("tolerance", values.tolerance, 0),
|
|
2991
3425
|
avoidSentenceOrphans: values.orphans,
|
|
2992
3426
|
style: values.style,
|
|
3427
|
+
include: values.include,
|
|
2993
3428
|
ignore: values.ignore ? [...config.ignore ?? [], ...values.ignore] : void 0
|
|
2994
3429
|
});
|
|
2995
3430
|
const agent = defined({
|
|
@@ -3014,10 +3449,11 @@ function scopeFrom(positionals, values) {
|
|
|
3014
3449
|
return { targets: positionals, base: values.base, wholeFiles: values["whole-files"], repo: values.repo };
|
|
3015
3450
|
}
|
|
3016
3451
|
function selectFrom(values, project) {
|
|
3017
|
-
|
|
3018
|
-
|
|
3452
|
+
const flags = ["suspicious", "failing", "all"].filter((scope) => values[scope]);
|
|
3453
|
+
if (flags.length > 1) {
|
|
3454
|
+
throw new Error("comments: pass only one of --suspicious, --failing, and --all");
|
|
3019
3455
|
}
|
|
3020
|
-
return
|
|
3456
|
+
return flags.at(0) ?? project.config.agent?.scope ?? "suspicious";
|
|
3021
3457
|
}
|
|
3022
3458
|
function formatIssue(issue) {
|
|
3023
3459
|
return issue.tier === "hint" ? `${issue.type} (hint)` : issue.type;
|
|
@@ -3073,7 +3509,7 @@ function hintEmptyScope(analyses, scope) {
|
|
|
3073
3509
|
const isDefaultScope = (scope.targets ?? []).length === 0 && !scope.repo && !scope.base;
|
|
3074
3510
|
if (analyses.length === 0 && isDefaultScope) {
|
|
3075
3511
|
log2(
|
|
3076
|
-
"comments: no uncommitted changes in JavaScript or
|
|
3512
|
+
"comments: no uncommitted changes in JavaScript, TypeScript, or Rust files. Pass --repo, --base <ref>, or files to widen the scope."
|
|
3077
3513
|
);
|
|
3078
3514
|
}
|
|
3079
3515
|
}
|