@d3lm/lint-preset 1.5.6 → 1.6.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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/comments/cli.ts
4
- import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "fs";
5
- import path5 from "path";
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, and notation like \`host:port\`.
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 JSDoc comments are capitalized and end with punctuation.
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, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)",
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 /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
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
- const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
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) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.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
- return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
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(lines.map((line) => line.text.trim()))) {
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 analyzeParagraph(paragraph, options) {
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
  }
@@ -1364,10 +1567,14 @@ 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 text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1370
- return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
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
1580
  const settle = (settings, extra = 0) => {
@@ -1375,11 +1582,11 @@ function analyzeParagraph(paragraph, options) {
1375
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)) {
@@ -1405,6 +1612,7 @@ function analyzeParagraph(paragraph, options) {
1405
1612
  }
1406
1613
  const { even, wrapped } = layout;
1407
1614
  const matchesLayout = sameLayout(wrapped, currentLines);
1615
+ const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1408
1616
  const lineIssues = [];
1409
1617
  const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
1410
1618
  if (!matchesLayout) {
@@ -1463,8 +1671,11 @@ function analyzeParagraph(paragraph, options) {
1463
1671
  lineIssues.push({ type: "unbalanced", line });
1464
1672
  }
1465
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
+ }
1466
1678
  }
1467
- const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1468
1679
  const matchesWiderLayout = () => {
1469
1680
  for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {
1470
1681
  const wider = layoutAt(extra);
@@ -1474,7 +1685,20 @@ function analyzeParagraph(paragraph, options) {
1474
1685
  }
1475
1686
  return false;
1476
1687
  };
1477
- const accepted = lineIssues.some((issue) => isVerdict(issue)) && matchesWiderLayout();
1688
+ const staysNearLayout = () => {
1689
+ if (currentLines.length !== wrapped.length || splitTokenLine(currentLines, units) !== -1) {
1690
+ return false;
1691
+ }
1692
+ return currentLines.every((words, position) => {
1693
+ const length = words.join(" ").length;
1694
+ const suggestedLength = wrapped[position].join(" ").length;
1695
+ const limit = (position === 0 ? firstLimit : restLimit) + layoutOptions.tolerance;
1696
+ return Math.abs(length - suggestedLength) <= layoutOptions.tolerance && length <= Math.max(suggestedLength, limit);
1697
+ });
1698
+ };
1699
+ const hasVerdict = lineIssues.some((issue) => isVerdict(issue));
1700
+ const onlyUnbalanced = lineIssues.every((issue) => issue.type === "unbalanced");
1701
+ const accepted = hasVerdict && (matchesWiderLayout() || onlyUnbalanced && staysNearLayout());
1478
1702
  const suggested = accepted ? currentLines : wrapped;
1479
1703
  const suggestion = suggested.map(
1480
1704
  (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
@@ -1483,6 +1707,18 @@ function analyzeParagraph(paragraph, options) {
1483
1707
  const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
1484
1708
  return { ...base, reflowable: true, issues, suggestion };
1485
1709
  }
1710
+ var LINE_COMMENT = "line comment";
1711
+ function analyzeLineParagraph(paragraph, options, language) {
1712
+ const first = paragraph.lines[0];
1713
+ const last = paragraph.lines.at(-1) ?? first;
1714
+ const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
1715
+ if (paragraph.kind === "code") {
1716
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };
1717
+ }
1718
+ const tier = structureOf(paragraph.lines, language)?.tier;
1719
+ const issues = tier === "code" || tier === "layout" ? [] : overflowIssues(paragraph.lines, options);
1720
+ return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };
1721
+ }
1486
1722
  function computeLineStarts(source) {
1487
1723
  const starts = [0];
1488
1724
  for (let index = 0; index < source.length; index += 1) {
@@ -1508,31 +1744,42 @@ function lineOf(lineStarts, offset) {
1508
1744
  function stripCarriageReturn(line) {
1509
1745
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1510
1746
  }
1511
- function shapeBlock(comment, sourceLines, lineStarts) {
1747
+ function blockKind(comment, language) {
1748
+ if (language.name === "rust") {
1749
+ return comment.doc ? `${comment.doc}-block-doc` : "block";
1750
+ }
1751
+ return comment.value.startsWith("*") ? "jsdoc" : "block";
1752
+ }
1753
+ function wholeComment(shape, lines, paragraphKind, reason) {
1754
+ const otherLines = lines.map((text, offset) => {
1755
+ return { index: shape.startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1756
+ });
1757
+ return { ...shape, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1758
+ }
1759
+ function isLegal(value, language) {
1760
+ return language.legal.test(replaceKeepingLines(value, CODE_SPAN, "``"));
1761
+ }
1762
+ function shapeBlock(comment, sourceLines, lineStarts, language) {
1512
1763
  const startLine = lineOf(lineStarts, comment.start);
1513
1764
  const endLine = lineOf(lineStarts, comment.end - 1);
1514
- const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
1765
+ const shape = { kind: blockKind(comment, language), startLine, endLine };
1515
1766
  const lines = sourceLines.slice(startLine, endLine + 1);
1516
- const wholeComment = (paragraphKind, reason) => {
1517
- const otherLines = lines.map((text, offset) => {
1518
- return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1519
- });
1520
- return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1521
- };
1522
- const value = comment.value.replace(/^\*/, "");
1523
- if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
1524
- return wholeComment("code", "legal comment");
1767
+ const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\*/, "");
1768
+ if (isLegal(comment.value, language)) {
1769
+ return wholeComment(shape, lines, "code", "legal comment");
1525
1770
  }
1526
- if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
1527
- return wholeComment("code", "directive");
1771
+ if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {
1772
+ return wholeComment(shape, lines, "code", "directive");
1528
1773
  }
1529
1774
  const other = () => {
1530
1775
  return wholeComment(
1776
+ shape,
1777
+ lines,
1531
1778
  "other",
1532
1779
  startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
1533
1780
  );
1534
1781
  };
1535
- if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1782
+ if (startLine === endLine || !/^\s*\/\*[*!]?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1536
1783
  return other();
1537
1784
  }
1538
1785
  const body = [];
@@ -1551,7 +1798,52 @@ function shapeBlock(comment, sourceLines, lineStarts) {
1551
1798
  hardBreak: HARD_BREAK.test(text)
1552
1799
  });
1553
1800
  }
1554
- return { kind, startLine, endLine, paragraphs: segment(body) };
1801
+ return { ...shape, paragraphs: segment(body, language) };
1802
+ }
1803
+ function shapeLineRun(group, sourceLines, lineStarts, language) {
1804
+ const startLine = lineOf(lineStarts, group[0].start);
1805
+ const endLine = startLine + group.length - 1;
1806
+ const kind = group[0].doc === "inner" ? "inner-line-doc" : "outer-line-doc";
1807
+ const shape = { kind, startLine, endLine };
1808
+ const lines = sourceLines.slice(startLine, endLine + 1);
1809
+ const values = group.map((comment) => comment.value.slice(1));
1810
+ if (isLegal(values.join("\n"), language)) {
1811
+ return wholeComment(shape, lines, "code", "legal comment");
1812
+ }
1813
+ if (LINE_DIRECTIVE.test(values[0])) {
1814
+ return wholeComment(shape, lines, "code", "directive");
1815
+ }
1816
+ const body = lines.map((line, offset) => {
1817
+ const [, marker, space, text] = /^(\s*\/\/[/!])( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1818
+ return {
1819
+ index: startLine + offset,
1820
+ prefix: `${marker} `,
1821
+ text: text.trimEnd(),
1822
+ verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
1823
+ hardBreak: HARD_BREAK.test(text)
1824
+ };
1825
+ });
1826
+ return { ...shape, paragraphs: segment(body, language) };
1827
+ }
1828
+ function shapeLineComment(group, sourceLines, lineStarts, language) {
1829
+ const startLine = lineOf(lineStarts, group[0].start);
1830
+ const endLine = startLine + group.length - 1;
1831
+ const shape = { kind: "line", startLine, endLine };
1832
+ const lines = sourceLines.slice(startLine, endLine + 1);
1833
+ if (isLegal(group.map((comment) => comment.value).join("\n"), language)) {
1834
+ return wholeComment(shape, lines, "code", "legal comment");
1835
+ }
1836
+ const body = lines.map((line, offset) => {
1837
+ const [, marker, space, text] = /^(\s*\/\/[/!]*)( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1838
+ return {
1839
+ index: startLine + offset,
1840
+ prefix: `${marker}${space}`,
1841
+ text: text.trimEnd(),
1842
+ verbatim: LINE_DIRECTIVE.test(text),
1843
+ hardBreak: HARD_BREAK.test(text)
1844
+ };
1845
+ });
1846
+ return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };
1555
1847
  }
1556
1848
  function ownsLines(comment, source, lineStarts) {
1557
1849
  const startLine = lineOf(lineStarts, comment.start);
@@ -1561,12 +1853,12 @@ function ownsLines(comment, source, lineStarts) {
1561
1853
  const after = source.slice(comment.end, lineEnd);
1562
1854
  return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
1563
1855
  }
1564
- function singleLineLength(paragraph, options) {
1856
+ function singleLineLength(paragraph, options, language) {
1565
1857
  const [first] = paragraph.lines;
1566
1858
  if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
1567
1859
  return sourceLineLength(first, options.tabWidth);
1568
1860
  }
1569
- if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
1861
+ if (paragraph.kind !== "prose" || structureOf(paragraph.lines, language)) {
1570
1862
  return Infinity;
1571
1863
  }
1572
1864
  const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
@@ -1581,33 +1873,73 @@ function describeFailure(errors, lineStarts) {
1581
1873
  const offset = first.labels.at(0)?.start;
1582
1874
  return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
1583
1875
  }
1876
+ function scanJavaScript(filename, text, lineStarts) {
1877
+ const { program, comments, errors } = parseSource(filename, text);
1878
+ if (errors.length > 0 && program.body.length === 0) {
1879
+ return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1880
+ }
1881
+ return {
1882
+ comments: comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")))
1883
+ };
1884
+ }
1885
+ function scanRustSource(text, lineStarts) {
1886
+ const { comments, error } = scanRust(text);
1887
+ if (error) {
1888
+ return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };
1889
+ }
1890
+ return {
1891
+ comments: comments.map((comment) => {
1892
+ return {
1893
+ type: comment.block ? "Block" : "Line",
1894
+ value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),
1895
+ start: comment.start,
1896
+ end: comment.end,
1897
+ doc: comment.doc
1898
+ };
1899
+ })
1900
+ };
1901
+ }
1902
+ function groupLineRuns(comments, text, lineStarts, markerOf) {
1903
+ const groups = [];
1904
+ let current;
1905
+ let previousLine = -1;
1906
+ let indent = "";
1907
+ let marker;
1908
+ for (const comment of comments) {
1909
+ const next = comment.type === "Line" ? markerOf(comment) : void 0;
1910
+ if (next === void 0 || !ownsLines(comment, text, lineStarts)) {
1911
+ current = void 0;
1912
+ continue;
1913
+ }
1914
+ const line = lineOf(lineStarts, comment.start);
1915
+ const before = text.slice(lineStarts[line], comment.start);
1916
+ if (current && line === previousLine + 1 && next === marker && before === indent) {
1917
+ current.push(comment);
1918
+ } else {
1919
+ current = [comment];
1920
+ indent = before;
1921
+ marker = next;
1922
+ groups.push(current);
1923
+ }
1924
+ previousLine = line;
1925
+ }
1926
+ return groups;
1927
+ }
1584
1928
  function inspectSource(source, filename, options) {
1585
1929
  if (options.maxWidth !== void 0 && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {
1586
1930
  throw new Error("comments: maxWidth must be an integer at least as large as width");
1587
1931
  }
1588
1932
  const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
1589
- const { program, comments, errors } = parseSource(filename, text);
1933
+ const language = languageOf(filename);
1590
1934
  const lineStarts = computeLineStarts(text);
1591
- if (errors.length > 0 && program.body.length === 0) {
1592
- return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1935
+ const scanned = language.name === "rust" ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);
1936
+ if (scanned.syntaxError) {
1937
+ return { comments: [], syntaxError: scanned.syntaxError };
1593
1938
  }
1594
1939
  const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
1595
- const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
1596
1940
  const reports = [];
1597
- const blocks = parsed.filter((comment) => {
1598
- return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1599
- });
1600
- for (const block of blocks) {
1601
- const shape = shapeBlock(block, sourceLines, lineStarts);
1602
- const singleLineWidth = Math.max(
1603
- options.width,
1604
- Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1605
- );
1606
- const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1607
- const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
1608
- const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1609
- const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1610
- reports.push({
1941
+ const report = (shape, lines, paragraphs) => {
1942
+ return {
1611
1943
  file: filename,
1612
1944
  kind: shape.kind,
1613
1945
  startLine: shape.startLine + 1,
@@ -1617,8 +1949,47 @@ function inspectSource(source, filename, options) {
1617
1949
  maxWidth: options.maxWidth,
1618
1950
  lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
1619
1951
  text: lines.join("\n"),
1620
- paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
1952
+ paragraphs
1953
+ };
1954
+ };
1955
+ const blocks = scanned.comments.filter((comment) => {
1956
+ return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1957
+ });
1958
+ const runs = (markerOf) => {
1959
+ return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {
1960
+ return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));
1621
1961
  });
1962
+ };
1963
+ const shapes = [
1964
+ ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),
1965
+ ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),
1966
+ ...runs((comment) => comment.doc ? void 0 : "//").map(
1967
+ (group) => shapeLineComment(group, sourceLines, lineStarts, language)
1968
+ )
1969
+ ].toSorted((a, b) => a.startLine - b.startLine);
1970
+ for (const shape of shapes) {
1971
+ const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1972
+ if (shape.kind === "line") {
1973
+ const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));
1974
+ if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {
1975
+ reports.push(report(shape, lines, paragraphs));
1976
+ }
1977
+ continue;
1978
+ }
1979
+ const singleLineWidth = Math.max(
1980
+ options.width,
1981
+ Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1982
+ );
1983
+ const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1984
+ const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;
1985
+ const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1986
+ reports.push(
1987
+ report(
1988
+ shape,
1989
+ lines,
1990
+ shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language))
1991
+ )
1992
+ );
1622
1993
  }
1623
1994
  return { comments: reports };
1624
1995
  }
@@ -1639,6 +2010,9 @@ function rewrapComment(report) {
1639
2010
  }
1640
2011
  var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
1641
2012
  function codeFingerprint(source, filename) {
2013
+ if (languageOf(filename).name === "rust") {
2014
+ return rustFingerprint(source);
2015
+ }
1642
2016
  const { program, errors } = parseSource(filename, source);
1643
2017
  if (errors.length > 0) {
1644
2018
  return JSON.stringify({ source, errors: errors.map((error) => error.message) });
@@ -1650,11 +2024,11 @@ function codeFingerprint(source, filename) {
1650
2024
  }
1651
2025
 
1652
2026
  // src/comments/config.ts
1653
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1654
- import { createRequire } from "module";
1655
- import path2 from "path";
2027
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
2028
+ import { createRequire as createRequire2 } from "module";
2029
+ import path3 from "path";
1656
2030
  import { pathToFileURL } from "url";
1657
- var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
2031
+ var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".rs"]);
1658
2032
  var CONFIG_FILES = [
1659
2033
  "comments.config.ts",
1660
2034
  "comments.config.mts",
@@ -1702,10 +2076,12 @@ function validateConfig(raw, filepath) {
1702
2076
  assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
1703
2077
  assertType(config.style, "boolean", "style", filepath);
1704
2078
  assertType(config.extensions, "array", "extensions", filepath);
2079
+ assertType(config.include, "array", "include", filepath);
1705
2080
  assertType(config.ignore, "array", "ignore", filepath);
1706
2081
  assertType(config.agent, "object", "agent", filepath);
1707
2082
  assertType(config.hook, "object", "hook", filepath);
1708
2083
  assertStrings(config.extensions, "extensions", filepath);
2084
+ assertStrings(config.include, "include", filepath);
1709
2085
  assertStrings(config.ignore, "ignore", filepath);
1710
2086
  for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
1711
2087
  const value = config[key];
@@ -1738,8 +2114,8 @@ function validateConfig(raw, filepath) {
1738
2114
  }
1739
2115
  assertType(agent.rules, "string", "agent.rules", filepath);
1740
2116
  assertType(agent.instructions, "string", "agent.instructions", filepath);
1741
- if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
1742
- throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
2117
+ if (agent.scope !== void 0 && !["suspicious", "failing", "all"].includes(agent.scope)) {
2118
+ throw new Error(`comments: \`agent.scope\` in ${filepath} must be one of suspicious, failing, all`);
1743
2119
  }
1744
2120
  const hook = config.hook ?? {};
1745
2121
  if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
@@ -1749,25 +2125,25 @@ function validateConfig(raw, filepath) {
1749
2125
  }
1750
2126
  async function loadConfig(cwd, options = {}) {
1751
2127
  if (options.explicitPath) {
1752
- const filepath = path2.resolve(cwd, options.explicitPath);
2128
+ const filepath = path3.resolve(cwd, options.explicitPath);
1753
2129
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1754
2130
  }
1755
- let directory = path2.resolve(cwd);
2131
+ let directory = path3.resolve(cwd);
1756
2132
  for (; ; ) {
1757
2133
  for (const name of CONFIG_FILES) {
1758
- const filepath = path2.join(directory, name);
1759
- if (existsSync(filepath)) {
2134
+ const filepath = path3.join(directory, name);
2135
+ if (existsSync2(filepath)) {
1760
2136
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1761
2137
  }
1762
2138
  }
1763
- const packageJsonPath = path2.join(directory, "package.json");
1764
- if (existsSync(packageJsonPath)) {
2139
+ const packageJsonPath = path3.join(directory, "package.json");
2140
+ if (existsSync2(packageJsonPath)) {
1765
2141
  const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
1766
2142
  if (value !== void 0) {
1767
2143
  return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
1768
2144
  }
1769
2145
  }
1770
- const parent = path2.dirname(directory);
2146
+ const parent = path3.dirname(directory);
1771
2147
  if (directory === options.stopDir || parent === directory) {
1772
2148
  return { config: {} };
1773
2149
  }
@@ -1779,8 +2155,8 @@ function readPrettierFile(directory, stopDir) {
1779
2155
  let current = directory;
1780
2156
  for (; ; ) {
1781
2157
  for (const name of PRETTIER_FILES) {
1782
- const filepath = path2.join(current, name);
1783
- if (!existsSync(filepath)) {
2158
+ const filepath = path3.join(current, name);
2159
+ if (!existsSync2(filepath)) {
1784
2160
  continue;
1785
2161
  }
1786
2162
  const text = readFileSync2(filepath, "utf8");
@@ -1791,14 +2167,14 @@ function readPrettierFile(directory, stopDir) {
1791
2167
  tabWidth: tabWidth === void 0 ? void 0 : Number(tabWidth)
1792
2168
  };
1793
2169
  }
1794
- const packageJsonPath = path2.join(current, "package.json");
1795
- if (existsSync(packageJsonPath)) {
2170
+ const packageJsonPath = path3.join(current, "package.json");
2171
+ if (existsSync2(packageJsonPath)) {
1796
2172
  const prettier = readJson(packageJsonPath).prettier;
1797
2173
  if (prettier && typeof prettier === "object") {
1798
2174
  return prettier;
1799
2175
  }
1800
2176
  }
1801
- const parent = path2.dirname(current);
2177
+ const parent = path3.dirname(current);
1802
2178
  if (current === stopDir || parent === current) {
1803
2179
  return void 0;
1804
2180
  }
@@ -1817,7 +2193,7 @@ var PrettierOptionsResolver = class {
1817
2193
  return this._prettier;
1818
2194
  }
1819
2195
  try {
1820
- const require2 = createRequire(path2.join(this._root, "package.json"));
2196
+ const require2 = createRequire2(path3.join(this._root, "package.json"));
1821
2197
  const resolved = require2.resolve("prettier");
1822
2198
  this._prettier = await import(pathToFileURL(resolved).href);
1823
2199
  } catch {
@@ -1836,7 +2212,7 @@ var PrettierOptionsResolver = class {
1836
2212
  } catch {
1837
2213
  }
1838
2214
  }
1839
- const directory = path2.dirname(file);
2215
+ const directory = path3.dirname(file);
1840
2216
  const cached = this._cache.get(directory);
1841
2217
  if (cached) {
1842
2218
  return cached;
@@ -1846,9 +2222,45 @@ var PrettierOptionsResolver = class {
1846
2222
  return options;
1847
2223
  }
1848
2224
  };
2225
+ var RUSTFMT_FILES = ["rustfmt.toml", ".rustfmt.toml"];
2226
+ var RUSTFMT_TAB_SPACES = 4;
2227
+ var RustfmtOptionsResolver = class {
2228
+ constructor(_root) {
2229
+ this._root = _root;
2230
+ }
2231
+ _root;
2232
+ _cache = /* @__PURE__ */ new Map();
2233
+ _read(directory) {
2234
+ const cached = this._cache.get(directory);
2235
+ if (cached) {
2236
+ return cached;
2237
+ }
2238
+ let options;
2239
+ for (const name of RUSTFMT_FILES) {
2240
+ const filepath = path3.join(directory, name);
2241
+ if (existsSync2(filepath)) {
2242
+ const text = readFileSync2(filepath, "utf8");
2243
+ const commentWidth = /^\s*comment_width\s*=\s*(\d+)/m.exec(text)?.[1];
2244
+ const tabSpaces = /^\s*tab_spaces\s*=\s*(\d+)/m.exec(text)?.[1];
2245
+ options = {
2246
+ printWidth: commentWidth === void 0 ? void 0 : Number(commentWidth),
2247
+ tabWidth: tabSpaces === void 0 ? RUSTFMT_TAB_SPACES : Number(tabSpaces)
2248
+ };
2249
+ break;
2250
+ }
2251
+ }
2252
+ const parent = path3.dirname(directory);
2253
+ options ??= directory === this._root || parent === directory ? { tabWidth: RUSTFMT_TAB_SPACES } : this._read(parent);
2254
+ this._cache.set(directory, options);
2255
+ return options;
2256
+ }
2257
+ resolve(file) {
2258
+ return this._read(path3.dirname(file));
2259
+ }
2260
+ };
1849
2261
 
1850
2262
  // src/comments/flow.ts
1851
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
2263
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
1852
2264
  var DEFAULT_BATCH_SIZE = 20;
1853
2265
  var DEFAULT_MAX_FILES = 10;
1854
2266
  var DEFAULT_MAX_AGENTS = 6;
@@ -1876,6 +2288,11 @@ function needsAgent(comment) {
1876
2288
  (paragraph) => paragraph.issues.some((issue) => issue.tier === "hint" || paragraph.suggestion === null)
1877
2289
  );
1878
2290
  }
2291
+ var SELECTORS = {
2292
+ suspicious: needsAgent,
2293
+ failing: hasVerdictIssues,
2294
+ all: () => true
2295
+ };
1879
2296
  function seconds(milliseconds) {
1880
2297
  return `${Math.round(milliseconds / 1e3)}s`;
1881
2298
  }
@@ -2020,9 +2437,9 @@ async function runFlow(project, scope, options) {
2020
2437
  skipped: skippedFiles(analyses)
2021
2438
  };
2022
2439
  }
2440
+ const select2 = SELECTORS[options.select];
2023
2441
  const pending = analyses.map((analysis) => {
2024
- const selected = options.select === "all" ? analysis.comments : analysis.comments.filter((comment) => needsAgent(comment));
2025
- return { ...analysis, comments: selected };
2442
+ return { ...analysis, comments: analysis.comments.filter((comment) => select2(comment)) };
2026
2443
  }).filter((analysis) => analysis.comments.length > 0);
2027
2444
  const comments = pending.flatMap((analysis) => analysis.comments);
2028
2445
  if (comments.length === 0) {
@@ -2090,7 +2507,7 @@ async function runFlow(project, scope, options) {
2090
2507
  const handedOver = new Set(pending.map((analysis) => analysis.file));
2091
2508
  const changedElsewhere = before && after ? changedBetween(before, after).filter((file) => !handedOver.has(file)) : [];
2092
2509
  const codeChanged = pending.filter((analysis) => {
2093
- if (!existsSync2(analysis.absolute)) {
2510
+ if (!existsSync3(analysis.absolute)) {
2094
2511
  return true;
2095
2512
  }
2096
2513
  const current = readFileSync3(analysis.absolute, "utf8");
@@ -2147,7 +2564,7 @@ function describeSummary(summary) {
2147
2564
  import { spawnSync } from "child_process";
2148
2565
  import { createHash } from "crypto";
2149
2566
  import { readFileSync as readFileSync4, statSync } from "fs";
2150
- import path3 from "path";
2567
+ import path4 from "path";
2151
2568
  function git(cwd, args) {
2152
2569
  const result = spawnSync("git", ["-c", "core.quotePath=false", ...args], {
2153
2570
  cwd,
@@ -2257,7 +2674,7 @@ function snapshotChanges(root) {
2257
2674
  try {
2258
2675
  const tracked = hasHead(root) ? splitPaths(gitOrThrow(root, ["diff", "-z", "--name-only", "--no-renames", "HEAD"])) : splitPaths(gitOrThrow(root, ["ls-files", "-z", "--cached"]));
2259
2676
  const files = /* @__PURE__ */ new Set([...tracked, ...listUntrackedFiles(root)]);
2260
- return new Map([...files].map((file) => [file, fileState(path3.join(root, file))]));
2677
+ return new Map([...files].map((file) => [file, fileState(path4.join(root, file))]));
2261
2678
  } catch {
2262
2679
  return void 0;
2263
2680
  }
@@ -2276,15 +2693,15 @@ function getChangedLines(root, scope = {}) {
2276
2693
  }
2277
2694
 
2278
2695
  // src/comments/project.ts
2279
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2280
- import path4 from "path";
2696
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2697
+ import path5 from "path";
2281
2698
  var DEFAULT_WIDTH = 80;
2282
2699
  var DEFAULT_MAX_WIDTH = 120;
2283
2700
  var DEFAULT_SINGLE_LINE_WIDTH = 120;
2284
2701
  var DEFAULT_TAB_WIDTH = 2;
2285
2702
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage"]);
2286
2703
  function toPosix(file) {
2287
- return file.split(path4.sep).join("/");
2704
+ return file.split(path5.sep).join("/");
2288
2705
  }
2289
2706
  function mergeFilter(existing, next) {
2290
2707
  if (existing === "all" || next === "all") {
@@ -2297,10 +2714,10 @@ function walkDirectory(directory, files = []) {
2297
2714
  for (const entry of entries) {
2298
2715
  if (entry.isDirectory()) {
2299
2716
  if (!SKIPPED_DIRECTORIES.has(entry.name)) {
2300
- walkDirectory(path4.join(directory, entry.name), files);
2717
+ walkDirectory(path5.join(directory, entry.name), files);
2301
2718
  }
2302
2719
  } else if (entry.isFile()) {
2303
- files.push(path4.join(directory, entry.name));
2720
+ files.push(path5.join(directory, entry.name));
2304
2721
  }
2305
2722
  }
2306
2723
  return files;
@@ -2322,41 +2739,45 @@ var Project = class {
2322
2739
  this.config = config;
2323
2740
  this._options = _options;
2324
2741
  this._prettier = new PrettierOptionsResolver(root);
2742
+ this._rustfmt = new RustfmtOptionsResolver(root);
2325
2743
  this._extensions = new Set(config.extensions ?? DEFAULT_EXTENSIONS);
2326
2744
  }
2327
2745
  root;
2328
2746
  config;
2329
2747
  _options;
2330
2748
  _prettier;
2749
+ _rustfmt;
2331
2750
  _extensions;
2332
2751
  relative(absolute) {
2333
- return toPosix(path4.relative(this.root, absolute));
2752
+ return toPosix(path5.relative(this.root, absolute));
2334
2753
  }
2335
2754
  _isIncluded(absolute) {
2336
- if (!this._extensions.has(path4.extname(absolute))) {
2755
+ if (!this._extensions.has(path5.extname(absolute))) {
2337
2756
  return false;
2338
2757
  }
2339
2758
  const relative = this.relative(absolute);
2340
- return !(this.config.ignore ?? []).some((pattern) => path4.matchesGlob(relative, pattern));
2759
+ const matches = (patterns) => patterns.some((pattern) => path5.matchesGlob(relative, pattern));
2760
+ const include = this.config.include ?? [];
2761
+ return (include.length === 0 || matches(include)) && !matches(this.config.ignore ?? []);
2341
2762
  }
2342
2763
  _filesIn(directory) {
2343
2764
  if (!this._options.isGitRepo) {
2344
2765
  return walkDirectory(directory);
2345
2766
  }
2346
- return listFiles2(this.root, [path4.relative(this.root, directory) || "."]).map((file) => path4.join(this.root, file));
2767
+ return listFiles2(this.root, [path5.relative(this.root, directory) || "."]).map((file) => path5.join(this.root, file));
2347
2768
  }
2348
2769
  collect(scope) {
2349
2770
  const files = /* @__PURE__ */ new Map();
2350
2771
  const add = (absolute, filter) => {
2351
- if (this._isIncluded(absolute) && existsSync3(absolute)) {
2772
+ if (this._isIncluded(absolute) && existsSync4(absolute)) {
2352
2773
  files.set(absolute, mergeFilter(files.get(absolute), filter));
2353
2774
  }
2354
2775
  };
2355
2776
  if (scope.targets && scope.targets.length > 0) {
2356
2777
  for (const target of scope.targets) {
2357
2778
  const range = /^(.+):(\d+)(?:-(\d+))?$/.exec(target);
2358
- const absolute = path4.resolve(this._options.cwd, range ? range[1] : target);
2359
- if (!existsSync3(absolute)) {
2779
+ const absolute = path5.resolve(this._options.cwd, range ? range[1] : target);
2780
+ if (!existsSync4(absolute)) {
2360
2781
  throw new Error(`comments: ${target} does not exist`);
2361
2782
  }
2362
2783
  if (statSync2(absolute).isDirectory()) {
@@ -2385,21 +2806,22 @@ var Project = class {
2385
2806
  throw new Error("comments: not a git repository, so pass files or directories explicitly");
2386
2807
  }
2387
2808
  for (const [file, lines] of getChangedLines(this.root, scope)) {
2388
- add(path4.join(this.root, file), scope.wholeFiles ? "all" : lines);
2809
+ add(path5.join(this.root, file), scope.wholeFiles ? "all" : lines);
2389
2810
  }
2390
2811
  return files;
2391
2812
  }
2392
2813
  async analyzeFile(absolute, filter = "all") {
2393
2814
  const source = readFileSync5(absolute, "utf8");
2394
- const prettier = await this._prettier.resolve(absolute);
2815
+ const isRust = absolute.endsWith(".rs");
2816
+ const formatter = isRust ? this._rustfmt.resolve(absolute) : await this._prettier.resolve(absolute);
2395
2817
  const file = this.relative(absolute);
2396
- const width = this._options.width ?? this.config.width ?? prettier.printWidth ?? DEFAULT_WIDTH;
2818
+ const width = this._options.width ?? this.config.width ?? formatter.printWidth ?? DEFAULT_WIDTH;
2397
2819
  const maxWidth = this.config.maxWidth ?? Math.max(width, this.config.tolerance === void 0 ? DEFAULT_MAX_WIDTH : width + this.config.tolerance);
2398
2820
  const { comments, syntaxError } = inspectSource(source, file, {
2399
2821
  width,
2400
2822
  maxWidth,
2401
2823
  singleLineWidth: this.config.singleLineWidth ?? DEFAULT_SINGLE_LINE_WIDTH,
2402
- tabWidth: prettier.tabWidth ?? DEFAULT_TAB_WIDTH,
2824
+ tabWidth: formatter.tabWidth ?? DEFAULT_TAB_WIDTH,
2403
2825
  tolerance: Math.min(this.config.tolerance ?? 0, maxWidth - width),
2404
2826
  avoidSentenceOrphans: this.config.avoidSentenceOrphans ?? true,
2405
2827
  style: this.config.style ?? true
@@ -2882,7 +3304,7 @@ ${this._paint("gray", S_BAR_END)} ${message}
2882
3304
  var HELP = `
2883
3305
  Usage: comments <command> [targets...] [options]
2884
3306
 
2885
- Finds comments in JavaScript and TypeScript files and measures how their prose fits the configured width.
3307
+ Finds comments in JavaScript, TypeScript, and Rust files and measures how their prose fits the configured width.
2886
3308
  Only agent and hook change files, by applying the exact rewrap to comments that still have layout issues.
2887
3309
 
2888
3310
  Commands:
@@ -2902,6 +3324,7 @@ Options:
2902
3324
  --suspicious Only comments with layout issues (overflow, early line ends, last lines of one or two words)
2903
3325
  or hints (possible orphans, em-dashes, banners, ...). For agent and hook (the default), only
2904
3326
  comments with hints or with issues in parts the tool never rewraps, a triage, not a full review
3327
+ --failing agent, hook: hand the agent only the comments that fail check, and none that only have hints
2905
3328
  --all agent, hook: hand every comment in scope to the agent for a full review of its wording
2906
3329
  --verbose agent, hook: print every agent's reply (otherwise a terminal offers to open them)
2907
3330
  --no-interactive agent: show the agents while they run, then exit without offering to open their replies
@@ -2923,7 +3346,8 @@ Config overrides (take precedence over the config file):
2923
3346
  with a sentence when the second is no longer than the first (default: on)
2924
3347
  --[no-]style Look for possible semicolons, em-dashes, dashes, label colons, emphasis, and banners
2925
3348
  as hints (default: on). The agent's rules apply either way
2926
- --ignore <glob> Skip matching files, repeatable
3349
+ --include <glob> Only consider matching files, repeatable. Replaces include from the config file
3350
+ --ignore <glob> Skip matching files, repeatable. Adds to ignore from the config file
2927
3351
  --model <name> agent, hook: model for the agent
2928
3352
  --timeout <seconds> agent, hook: how long to wait for each agent (default: 300)
2929
3353
  --batch-size <n> agent, hook: most comments per agent (default: 20)
@@ -2937,6 +3361,7 @@ var OPTIONS = {
2937
3361
  tolerance: { type: "string" },
2938
3362
  orphans: { type: "boolean" },
2939
3363
  style: { type: "boolean" },
3364
+ include: { type: "string", multiple: true },
2940
3365
  ignore: { type: "string", multiple: true },
2941
3366
  model: { type: "string" },
2942
3367
  timeout: { type: "string" },
@@ -2948,6 +3373,7 @@ var OPTIONS = {
2948
3373
  "whole-files": { type: "boolean" },
2949
3374
  repo: { type: "boolean" },
2950
3375
  suspicious: { type: "boolean" },
3376
+ failing: { type: "boolean" },
2951
3377
  all: { type: "boolean" },
2952
3378
  verbose: { type: "boolean" },
2953
3379
  interactive: { type: "boolean" },
@@ -2984,8 +3410,8 @@ function log2(message) {
2984
3410
  `);
2985
3411
  }
2986
3412
  function resolveCwd(directory, source = "--cwd") {
2987
- const resolved = path5.resolve(directory);
2988
- if (!existsSync4(resolved) || !statSync3(resolved).isDirectory()) {
3413
+ const resolved = path6.resolve(directory);
3414
+ if (!existsSync5(resolved) || !statSync3(resolved).isDirectory()) {
2989
3415
  throw new Error(`comments: ${source} ${directory} is not a directory`);
2990
3416
  }
2991
3417
  return realpathSync(resolved);
@@ -3011,6 +3437,7 @@ function applyOverrides(config, values) {
3011
3437
  tolerance: parseInteger("tolerance", values.tolerance, 0),
3012
3438
  avoidSentenceOrphans: values.orphans,
3013
3439
  style: values.style,
3440
+ include: values.include,
3014
3441
  ignore: values.ignore ? [...config.ignore ?? [], ...values.ignore] : void 0
3015
3442
  });
3016
3443
  const agent = defined({
@@ -3035,10 +3462,11 @@ function scopeFrom(positionals, values) {
3035
3462
  return { targets: positionals, base: values.base, wholeFiles: values["whole-files"], repo: values.repo };
3036
3463
  }
3037
3464
  function selectFrom(values, project) {
3038
- if (values.all) {
3039
- return "all";
3465
+ const flags = ["suspicious", "failing", "all"].filter((scope) => values[scope]);
3466
+ if (flags.length > 1) {
3467
+ throw new Error("comments: pass only one of --suspicious, --failing, and --all");
3040
3468
  }
3041
- return values.suspicious ? "suspicious" : project.config.agent?.scope ?? "suspicious";
3469
+ return flags.at(0) ?? project.config.agent?.scope ?? "suspicious";
3042
3470
  }
3043
3471
  function formatIssue(issue) {
3044
3472
  return issue.tier === "hint" ? `${issue.type} (hint)` : issue.type;
@@ -3094,7 +3522,7 @@ function hintEmptyScope(analyses, scope) {
3094
3522
  const isDefaultScope = (scope.targets ?? []).length === 0 && !scope.repo && !scope.base;
3095
3523
  if (analyses.length === 0 && isDefaultScope) {
3096
3524
  log2(
3097
- "comments: no uncommitted changes in JavaScript or TypeScript files. Pass --repo, --base <ref>, or files to widen the scope."
3525
+ "comments: no uncommitted changes in JavaScript, TypeScript, or Rust files. Pass --repo, --base <ref>, or files to widen the scope."
3098
3526
  );
3099
3527
  }
3100
3528
  }