@d3lm/lint-preset 1.5.6 → 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/dist/comments.js CHANGED
@@ -1,6 +1,71 @@
1
1
  // src/comments/analyze.ts
2
2
  import { parseSync } from "oxc-parser";
3
3
 
4
+ // src/comments/rust.ts
5
+ import { existsSync } from "fs";
6
+ import { createRequire } from "module";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ var BINARY_NAME = "comments";
10
+ function platformSuffixes() {
11
+ const { platform, arch } = process;
12
+ switch (platform) {
13
+ case "darwin": {
14
+ return [`darwin-${arch}`, "darwin-universal"];
15
+ }
16
+ case "linux": {
17
+ return arch === "arm" ? ["linux-arm-gnueabihf", "linux-arm-musleabihf"] : [`linux-${arch}-gnu`, `linux-${arch}-musl`];
18
+ }
19
+ case "win32": {
20
+ return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];
21
+ }
22
+ default: {
23
+ return [`${platform}-${arch}`];
24
+ }
25
+ }
26
+ }
27
+ function findBinaries() {
28
+ const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);
29
+ for (let directory = path.dirname(fileURLToPath(import.meta.url)); ; directory = path.dirname(directory)) {
30
+ const found = names.map((name) => path.join(directory, "native", name)).filter((file) => existsSync(file));
31
+ if (found.length > 0) {
32
+ return found;
33
+ }
34
+ if (path.dirname(directory) === directory) {
35
+ return [];
36
+ }
37
+ }
38
+ }
39
+ var binding;
40
+ function loadBinding() {
41
+ if (binding) {
42
+ return binding;
43
+ }
44
+ const require2 = createRequire(import.meta.url);
45
+ const errors = [];
46
+ for (const file of findBinaries()) {
47
+ try {
48
+ binding = require2(file);
49
+ return binding;
50
+ } catch (error) {
51
+ errors.push(`${file}: ${error.message}`);
52
+ }
53
+ }
54
+ const reason = errors.length > 0 ? `, and loading it failed (${errors.join("; ")})` : "";
55
+ throw new Error(
56
+ `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\`.`
57
+ );
58
+ }
59
+ function scanRust(source) {
60
+ return loadBinding().scanRust(source);
61
+ }
62
+ function rustFingerprint(source) {
63
+ return loadBinding().rustFingerprint(source);
64
+ }
65
+ function isRustCode(text) {
66
+ return loadBinding().isRustCode(text);
67
+ }
68
+
4
69
  // src/comments/style.ts
5
70
  var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
6
71
  var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
@@ -124,12 +189,94 @@ function findCodeSpanEnd(text, start) {
124
189
  }
125
190
  return -1;
126
191
  }
192
+ function isEscaped(text, index) {
193
+ let backslashes = 0;
194
+ while (text[index - 1 - backslashes] === "\\") {
195
+ backslashes += 1;
196
+ }
197
+ return backslashes % 2 === 1;
198
+ }
199
+ function findUnescaped(text, char, start) {
200
+ for (let index = start; index < text.length; index += 1) {
201
+ if (text[index] === "\\") {
202
+ index += 1;
203
+ } else if (text[index] === char) {
204
+ return index;
205
+ }
206
+ }
207
+ return -1;
208
+ }
209
+ function findClosingBracket(text, start) {
210
+ let depth = 0;
211
+ for (let index = start; index < text.length; index += 1) {
212
+ switch (text[index]) {
213
+ case "\\": {
214
+ index += 1;
215
+ break;
216
+ }
217
+ case "`": {
218
+ const close = findCodeSpanEnd(text, index);
219
+ index = close === -1 ? index + backtickRun(text, index) - 1 : close;
220
+ break;
221
+ }
222
+ case "[": {
223
+ depth += 1;
224
+ break;
225
+ }
226
+ case "]": {
227
+ depth -= 1;
228
+ if (depth === 0) {
229
+ return index;
230
+ }
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ return -1;
236
+ }
237
+ function findDestinationEnd(text, start) {
238
+ let depth = 0;
239
+ for (let index = start; index < text.length; index += 1) {
240
+ const char = text[index];
241
+ let close = -1;
242
+ if (char === "\\") {
243
+ index += 1;
244
+ } else if (char === "<" && text.slice(start + 1, index).trim() === "") {
245
+ close = findUnescaped(text, ">", index + 1);
246
+ } else if ((char === '"' || char === "'") && /\s/.test(text[index - 1])) {
247
+ close = findUnescaped(text, char, index + 1);
248
+ } else if (char === "(") {
249
+ depth += 1;
250
+ } else if (char === ")" && --depth === 0) {
251
+ return index;
252
+ }
253
+ index = close === -1 ? index : close;
254
+ }
255
+ return -1;
256
+ }
257
+ function findLinkEnd(text, start) {
258
+ const close = findClosingBracket(text, start);
259
+ if (close === -1 || text[close + 1] !== "(") {
260
+ return close;
261
+ }
262
+ const end = findDestinationEnd(text, close + 1);
263
+ return end === -1 ? close : end;
264
+ }
127
265
  function tokenize(text) {
266
+ return splitWords(text, true);
267
+ }
268
+ function splitWords(text, keepLinks) {
128
269
  const tokens = [];
129
270
  let current = "";
130
271
  let index = 0;
131
272
  while (index < text.length) {
132
273
  const char = text[index];
274
+ const linkEnd = keepLinks && char === "[" && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;
275
+ if (linkEnd !== -1) {
276
+ current += splitWords(text.slice(index, linkEnd + 1), false).join(" ");
277
+ index = linkEnd + 1;
278
+ continue;
279
+ }
133
280
  const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
134
281
  if (char === "`" && close === -1) {
135
282
  const run = backtickRun(text, index);
@@ -225,7 +372,7 @@ function isOrphan(previous, token) {
225
372
  return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
226
373
  }
227
374
  function startsBlock(token) {
228
- return /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
375
+ return /^(?:\+|-+|=+|\*+|_{3,}|\d+[.)]|#{1,6})$/.test(token) || /^(?:>|@[A-Za-z]|`{3}|~{3}|\||<(?:\/?[A-Za-z]|[!?]))/.test(token);
229
376
  }
230
377
  function bindLineStarts(tokens, startsLine2 = startsBlock) {
231
378
  const units = [];
@@ -663,6 +810,7 @@ var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "p
663
810
  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)/;
664
811
  var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
665
812
  var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
813
+ var RUST_LEGAL_COMMENT = /@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
666
814
  var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
667
815
  var BOX_DRAWING = /[\u2500-\u257F]/;
668
816
  var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
@@ -737,12 +885,35 @@ ${text}
737
885
  }
738
886
  return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
739
887
  }
740
- function isCode(texts) {
741
- const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
742
- return parsesAsStatements(text) || parsesAsProperties(text);
888
+ function isCode(texts, language) {
889
+ return language.isCode(texts.map((line) => line.replace(LIST_MARKER, "")).join("\n"));
743
890
  }
744
891
  function isCodeLine(text) {
745
- 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);
892
+ return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text);
893
+ }
894
+ var JAVASCRIPT = {
895
+ name: "javascript",
896
+ isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),
897
+ isCodeLine: (text) => {
898
+ return isCodeLine(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
899
+ },
900
+ legal: LEGAL_COMMENT,
901
+ blockDirective: BLOCK_DIRECTIVE,
902
+ tags: true,
903
+ joinsCodeSpans: true
904
+ };
905
+ var RUST = {
906
+ name: "rust",
907
+ isCode: (text) => isRustCode(text),
908
+ isCodeLine: (text) => {
909
+ 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);
910
+ },
911
+ legal: RUST_LEGAL_COMMENT,
912
+ tags: false,
913
+ joinsCodeSpans: false
914
+ };
915
+ function languageOf(filename) {
916
+ return filename.endsWith(".rs") ? RUST : JAVASCRIPT;
746
917
  }
747
918
  function layoutReason(prose, raw) {
748
919
  if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
@@ -756,8 +927,10 @@ function layoutReason(prose, raw) {
756
927
  }
757
928
  return void 0;
758
929
  }
930
+ var THEMATIC_BREAK = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
759
931
  function isMarkup(prose) {
760
- return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
932
+ const text = prose.replace(LIST_MARKER, "");
933
+ return /^#{1,6}(?:\s|$)/.test(text) || /^\[[^\]]+\]:/.test(text) || /^<(?:\/?[a-z]|[!?])/i.test(text) || text.startsWith(">") || /^(?:=+|-+)$/.test(prose) || THEMATIC_BREAK.test(prose);
761
934
  }
762
935
  function mask(text) {
763
936
  return text.replaceAll(/[^\n]/g, "x");
@@ -784,11 +957,14 @@ function hasAlignedColumns(lines) {
784
957
  }
785
958
  return false;
786
959
  }
787
- function structureOf(lines) {
960
+ function structureOf(lines, language) {
788
961
  const trimmed = lines.map((line) => line.text.trim()).join("\n");
789
962
  const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
790
963
  const proseLines = stripNonProse(trimmed).split("\n");
791
- if (isCode(lines.map((line) => line.text.trim()))) {
964
+ if (isCode(
965
+ lines.map((line) => line.text.trim()),
966
+ language
967
+ )) {
792
968
  return { tier: "code", reason: "code" };
793
969
  }
794
970
  let layout;
@@ -797,7 +973,7 @@ function structureOf(lines) {
797
973
  for (const [position, line] of texts.entries()) {
798
974
  const prose = proseLines[position].trim();
799
975
  layout ??= layoutReason(prose, lines[position]?.text ?? "");
800
- if (isCodeLine(line.trim())) {
976
+ if (language.isCodeLine(line.trim())) {
801
977
  codeLike = true;
802
978
  } else if (isMarkup(prose)) {
803
979
  manual ??= "markup";
@@ -836,7 +1012,7 @@ var FenceTracker = class {
836
1012
  return true;
837
1013
  }
838
1014
  };
839
- function segment(lines) {
1015
+ function segment(lines, language) {
840
1016
  const paragraphs = [];
841
1017
  const fence = new FenceTracker();
842
1018
  let current;
@@ -873,6 +1049,10 @@ function segment(lines) {
873
1049
  continue;
874
1050
  }
875
1051
  if (line.verbatim) {
1052
+ if (fence.next(text)) {
1053
+ start("code", line, "", "code block");
1054
+ continue;
1055
+ }
876
1056
  start("code", line, "", "directive");
877
1057
  flush();
878
1058
  continue;
@@ -897,7 +1077,7 @@ function segment(lines) {
897
1077
  }
898
1078
  continue;
899
1079
  }
900
- const tag = JSDOC_TAG.exec(text);
1080
+ const tag = language.tags ? JSDOC_TAG.exec(text) : null;
901
1081
  if (tag) {
902
1082
  start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
903
1083
  inCodeTag = CODE_TAGS.has(tag[1]);
@@ -1004,7 +1184,19 @@ function byLine(issues) {
1004
1184
  function sameLayout(a, b) {
1005
1185
  return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
1006
1186
  }
1007
- function analyzeParagraph(paragraph, options) {
1187
+ function splitTokenLine(lines, tokens) {
1188
+ let next = 0;
1189
+ return lines.findIndex((words) => {
1190
+ const line = words.join(" ");
1191
+ let text = "";
1192
+ while (text.length < line.length && next < tokens.length) {
1193
+ text += `${text ? " " : ""}${tokens[next]}`;
1194
+ next += 1;
1195
+ }
1196
+ return text !== line;
1197
+ });
1198
+ }
1199
+ function analyzeParagraph(paragraph, options, language) {
1008
1200
  const first = paragraph.lines[0];
1009
1201
  const last = paragraph.lines.at(-1) ?? first;
1010
1202
  const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
@@ -1031,7 +1223,8 @@ function analyzeParagraph(paragraph, options) {
1031
1223
  const structure2 = structureOf(
1032
1224
  prose.lines.map((line) => {
1033
1225
  return { ...line, text: stripBlockSyntax(line.text) };
1034
- })
1226
+ }),
1227
+ language
1035
1228
  );
1036
1229
  const issues2 = keptIssues(prose, structure2, options);
1037
1230
  return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
@@ -1040,7 +1233,7 @@ function analyzeParagraph(paragraph, options) {
1040
1233
  const issues2 = keptIssues(paragraph, structure2, options);
1041
1234
  return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
1042
1235
  };
1043
- const structure = structureOf(paragraph.lines);
1236
+ const structure = structureOf(paragraph.lines, language);
1044
1237
  if (structure) {
1045
1238
  return keep(structure);
1046
1239
  }
@@ -1072,10 +1265,14 @@ function analyzeParagraph(paragraph, options) {
1072
1265
  if (!units.every((unit) => staysInLimit(unit))) {
1073
1266
  return keep({ tier: "manual", reason: "unbreakable text across lines" });
1074
1267
  }
1268
+ const text = currentLines.map((words) => words.join(" ")).join("\n");
1269
+ if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes("\n"))) {
1270
+ return keep({ tier: "manual", reason: "code span across lines" });
1271
+ }
1075
1272
  const bodyOf = (lines) => {
1076
1273
  return lines.map((words, position) => {
1077
- const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1078
- return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
1274
+ const text2 = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1275
+ return { ...first, text: text2, verbatim: false, hardBreak: HARD_BREAK.test(text2) };
1079
1276
  });
1080
1277
  };
1081
1278
  const settle = (settings, extra = 0) => {
@@ -1083,11 +1280,11 @@ function analyzeParagraph(paragraph, options) {
1083
1280
  let { even: even2, wrapped: wrapped2 } = layOut(current, settings, extra);
1084
1281
  for (; ; ) {
1085
1282
  const body = bodyOf(wrapped2);
1086
- const rewrap = structureOf(body);
1283
+ const rewrap = structureOf(body, language);
1087
1284
  if (!rewrap) {
1088
1285
  return { even: even2, wrapped: wrapped2 };
1089
1286
  }
1090
- const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1287
+ const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== void 0);
1091
1288
  const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
1092
1289
  const pair = `${current[start - 1]} ${current[start]}`;
1093
1290
  if (body.length < 2 || !staysInLimit(pair)) {
@@ -1113,6 +1310,7 @@ function analyzeParagraph(paragraph, options) {
1113
1310
  }
1114
1311
  const { even, wrapped } = layout;
1115
1312
  const matchesLayout = sameLayout(wrapped, currentLines);
1313
+ const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1116
1314
  const lineIssues = [];
1117
1315
  const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
1118
1316
  if (!matchesLayout) {
@@ -1171,8 +1369,11 @@ function analyzeParagraph(paragraph, options) {
1171
1369
  lineIssues.push({ type: "unbalanced", line });
1172
1370
  }
1173
1371
  }
1372
+ const splitLine = splitTokenLine(currentLines, tokens);
1373
+ if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {
1374
+ lineIssues.push({ type: "unbalanced", line: splitLine });
1375
+ }
1174
1376
  }
1175
- const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1176
1377
  const matchesWiderLayout = () => {
1177
1378
  for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {
1178
1379
  const wider = layoutAt(extra);
@@ -1191,6 +1392,18 @@ function analyzeParagraph(paragraph, options) {
1191
1392
  const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
1192
1393
  return { ...base, reflowable: true, issues, suggestion };
1193
1394
  }
1395
+ var LINE_COMMENT = "line comment";
1396
+ function analyzeLineParagraph(paragraph, options, language) {
1397
+ const first = paragraph.lines[0];
1398
+ const last = paragraph.lines.at(-1) ?? first;
1399
+ const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
1400
+ if (paragraph.kind === "code") {
1401
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };
1402
+ }
1403
+ const tier = structureOf(paragraph.lines, language)?.tier;
1404
+ const issues = tier === "code" || tier === "layout" ? [] : overflowIssues(paragraph.lines, options);
1405
+ return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };
1406
+ }
1194
1407
  function computeLineStarts(source) {
1195
1408
  const starts = [0];
1196
1409
  for (let index = 0; index < source.length; index += 1) {
@@ -1216,31 +1429,42 @@ function lineOf(lineStarts, offset) {
1216
1429
  function stripCarriageReturn(line) {
1217
1430
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1218
1431
  }
1219
- function shapeBlock(comment, sourceLines, lineStarts) {
1432
+ function blockKind(comment, language) {
1433
+ if (language.name === "rust") {
1434
+ return comment.doc ? `${comment.doc}-block-doc` : "block";
1435
+ }
1436
+ return comment.value.startsWith("*") ? "jsdoc" : "block";
1437
+ }
1438
+ function wholeComment(shape, lines, paragraphKind, reason) {
1439
+ const otherLines = lines.map((text, offset) => {
1440
+ return { index: shape.startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1441
+ });
1442
+ return { ...shape, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1443
+ }
1444
+ function isLegal(value, language) {
1445
+ return language.legal.test(replaceKeepingLines(value, CODE_SPAN, "``"));
1446
+ }
1447
+ function shapeBlock(comment, sourceLines, lineStarts, language) {
1220
1448
  const startLine = lineOf(lineStarts, comment.start);
1221
1449
  const endLine = lineOf(lineStarts, comment.end - 1);
1222
- const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
1450
+ const shape = { kind: blockKind(comment, language), startLine, endLine };
1223
1451
  const lines = sourceLines.slice(startLine, endLine + 1);
1224
- const wholeComment = (paragraphKind, reason) => {
1225
- const otherLines = lines.map((text, offset) => {
1226
- return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1227
- });
1228
- return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1229
- };
1230
- const value = comment.value.replace(/^\*/, "");
1231
- if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
1232
- return wholeComment("code", "legal comment");
1452
+ const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\*/, "");
1453
+ if (isLegal(comment.value, language)) {
1454
+ return wholeComment(shape, lines, "code", "legal comment");
1233
1455
  }
1234
- if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
1235
- return wholeComment("code", "directive");
1456
+ if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {
1457
+ return wholeComment(shape, lines, "code", "directive");
1236
1458
  }
1237
1459
  const other = () => {
1238
1460
  return wholeComment(
1461
+ shape,
1462
+ lines,
1239
1463
  "other",
1240
1464
  startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
1241
1465
  );
1242
1466
  };
1243
- if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1467
+ if (startLine === endLine || !/^\s*\/\*[*!]?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1244
1468
  return other();
1245
1469
  }
1246
1470
  const body = [];
@@ -1259,7 +1483,52 @@ function shapeBlock(comment, sourceLines, lineStarts) {
1259
1483
  hardBreak: HARD_BREAK.test(text)
1260
1484
  });
1261
1485
  }
1262
- return { kind, startLine, endLine, paragraphs: segment(body) };
1486
+ return { ...shape, paragraphs: segment(body, language) };
1487
+ }
1488
+ function shapeLineRun(group, sourceLines, lineStarts, language) {
1489
+ const startLine = lineOf(lineStarts, group[0].start);
1490
+ const endLine = startLine + group.length - 1;
1491
+ const kind = group[0].doc === "inner" ? "inner-line-doc" : "outer-line-doc";
1492
+ const shape = { kind, startLine, endLine };
1493
+ const lines = sourceLines.slice(startLine, endLine + 1);
1494
+ const values = group.map((comment) => comment.value.slice(1));
1495
+ if (isLegal(values.join("\n"), language)) {
1496
+ return wholeComment(shape, lines, "code", "legal comment");
1497
+ }
1498
+ if (LINE_DIRECTIVE.test(values[0])) {
1499
+ return wholeComment(shape, lines, "code", "directive");
1500
+ }
1501
+ const body = lines.map((line, offset) => {
1502
+ const [, marker, space, text] = /^(\s*\/\/[/!])( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1503
+ return {
1504
+ index: startLine + offset,
1505
+ prefix: `${marker} `,
1506
+ text: text.trimEnd(),
1507
+ verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
1508
+ hardBreak: HARD_BREAK.test(text)
1509
+ };
1510
+ });
1511
+ return { ...shape, paragraphs: segment(body, language) };
1512
+ }
1513
+ function shapeLineComment(group, sourceLines, lineStarts, language) {
1514
+ const startLine = lineOf(lineStarts, group[0].start);
1515
+ const endLine = startLine + group.length - 1;
1516
+ const shape = { kind: "line", startLine, endLine };
1517
+ const lines = sourceLines.slice(startLine, endLine + 1);
1518
+ if (isLegal(group.map((comment) => comment.value).join("\n"), language)) {
1519
+ return wholeComment(shape, lines, "code", "legal comment");
1520
+ }
1521
+ const body = lines.map((line, offset) => {
1522
+ const [, marker, space, text] = /^(\s*\/\/[/!]*)( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1523
+ return {
1524
+ index: startLine + offset,
1525
+ prefix: `${marker}${space}`,
1526
+ text: text.trimEnd(),
1527
+ verbatim: LINE_DIRECTIVE.test(text),
1528
+ hardBreak: HARD_BREAK.test(text)
1529
+ };
1530
+ });
1531
+ return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };
1263
1532
  }
1264
1533
  function ownsLines(comment, source, lineStarts) {
1265
1534
  const startLine = lineOf(lineStarts, comment.start);
@@ -1269,12 +1538,12 @@ function ownsLines(comment, source, lineStarts) {
1269
1538
  const after = source.slice(comment.end, lineEnd);
1270
1539
  return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
1271
1540
  }
1272
- function singleLineLength(paragraph, options) {
1541
+ function singleLineLength(paragraph, options, language) {
1273
1542
  const [first] = paragraph.lines;
1274
1543
  if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
1275
1544
  return sourceLineLength(first, options.tabWidth);
1276
1545
  }
1277
- if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
1546
+ if (paragraph.kind !== "prose" || structureOf(paragraph.lines, language)) {
1278
1547
  return Infinity;
1279
1548
  }
1280
1549
  const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
@@ -1289,6 +1558,58 @@ function describeFailure(errors, lineStarts) {
1289
1558
  const offset = first.labels.at(0)?.start;
1290
1559
  return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
1291
1560
  }
1561
+ function scanJavaScript(filename, text, lineStarts) {
1562
+ const { program, comments, errors } = parseSource(filename, text);
1563
+ if (errors.length > 0 && program.body.length === 0) {
1564
+ return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1565
+ }
1566
+ return {
1567
+ comments: comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")))
1568
+ };
1569
+ }
1570
+ function scanRustSource(text, lineStarts) {
1571
+ const { comments, error } = scanRust(text);
1572
+ if (error) {
1573
+ return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };
1574
+ }
1575
+ return {
1576
+ comments: comments.map((comment) => {
1577
+ return {
1578
+ type: comment.block ? "Block" : "Line",
1579
+ value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),
1580
+ start: comment.start,
1581
+ end: comment.end,
1582
+ doc: comment.doc
1583
+ };
1584
+ })
1585
+ };
1586
+ }
1587
+ function groupLineRuns(comments, text, lineStarts, markerOf) {
1588
+ const groups = [];
1589
+ let current;
1590
+ let previousLine = -1;
1591
+ let indent = "";
1592
+ let marker;
1593
+ for (const comment of comments) {
1594
+ const next = comment.type === "Line" ? markerOf(comment) : void 0;
1595
+ if (next === void 0 || !ownsLines(comment, text, lineStarts)) {
1596
+ current = void 0;
1597
+ continue;
1598
+ }
1599
+ const line = lineOf(lineStarts, comment.start);
1600
+ const before = text.slice(lineStarts[line], comment.start);
1601
+ if (current && line === previousLine + 1 && next === marker && before === indent) {
1602
+ current.push(comment);
1603
+ } else {
1604
+ current = [comment];
1605
+ indent = before;
1606
+ marker = next;
1607
+ groups.push(current);
1608
+ }
1609
+ previousLine = line;
1610
+ }
1611
+ return groups;
1612
+ }
1292
1613
  function analyzeSource(source, filename, options) {
1293
1614
  return inspectSource(source, filename, options).comments;
1294
1615
  }
@@ -1297,28 +1618,16 @@ function inspectSource(source, filename, options) {
1297
1618
  throw new Error("comments: maxWidth must be an integer at least as large as width");
1298
1619
  }
1299
1620
  const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
1300
- const { program, comments, errors } = parseSource(filename, text);
1621
+ const language = languageOf(filename);
1301
1622
  const lineStarts = computeLineStarts(text);
1302
- if (errors.length > 0 && program.body.length === 0) {
1303
- return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1623
+ const scanned = language.name === "rust" ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);
1624
+ if (scanned.syntaxError) {
1625
+ return { comments: [], syntaxError: scanned.syntaxError };
1304
1626
  }
1305
1627
  const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
1306
- const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
1307
1628
  const reports = [];
1308
- const blocks = parsed.filter((comment) => {
1309
- return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1310
- });
1311
- for (const block of blocks) {
1312
- const shape = shapeBlock(block, sourceLines, lineStarts);
1313
- const singleLineWidth = Math.max(
1314
- options.width,
1315
- Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1316
- );
1317
- const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1318
- const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
1319
- const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1320
- const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1321
- reports.push({
1629
+ const report = (shape, lines, paragraphs) => {
1630
+ return {
1322
1631
  file: filename,
1323
1632
  kind: shape.kind,
1324
1633
  startLine: shape.startLine + 1,
@@ -1328,8 +1637,47 @@ function inspectSource(source, filename, options) {
1328
1637
  maxWidth: options.maxWidth,
1329
1638
  lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
1330
1639
  text: lines.join("\n"),
1331
- paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
1640
+ paragraphs
1641
+ };
1642
+ };
1643
+ const blocks = scanned.comments.filter((comment) => {
1644
+ return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1645
+ });
1646
+ const runs = (markerOf) => {
1647
+ return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {
1648
+ return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));
1332
1649
  });
1650
+ };
1651
+ const shapes = [
1652
+ ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),
1653
+ ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),
1654
+ ...runs((comment) => comment.doc ? void 0 : "//").map(
1655
+ (group) => shapeLineComment(group, sourceLines, lineStarts, language)
1656
+ )
1657
+ ].toSorted((a, b) => a.startLine - b.startLine);
1658
+ for (const shape of shapes) {
1659
+ const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1660
+ if (shape.kind === "line") {
1661
+ const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));
1662
+ if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {
1663
+ reports.push(report(shape, lines, paragraphs));
1664
+ }
1665
+ continue;
1666
+ }
1667
+ const singleLineWidth = Math.max(
1668
+ options.width,
1669
+ Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1670
+ );
1671
+ const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1672
+ const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;
1673
+ const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1674
+ reports.push(
1675
+ report(
1676
+ shape,
1677
+ lines,
1678
+ shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language))
1679
+ )
1680
+ );
1333
1681
  }
1334
1682
  return { comments: reports };
1335
1683
  }
@@ -1350,6 +1698,9 @@ function rewrapComment(report) {
1350
1698
  }
1351
1699
  var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
1352
1700
  function codeFingerprint(source, filename) {
1701
+ if (languageOf(filename).name === "rust") {
1702
+ return rustFingerprint(source);
1703
+ }
1353
1704
  const { program, errors } = parseSource(filename, source);
1354
1705
  if (errors.length > 0) {
1355
1706
  return JSON.stringify({ source, errors: errors.map((error) => error.message) });
@@ -1363,7 +1714,7 @@ function codeFingerprint(source, filename) {
1363
1714
  // src/comments/agent.ts
1364
1715
  import { spawn } from "child_process";
1365
1716
  import { readFileSync } from "fs";
1366
- import path from "path";
1717
+ import path2 from "path";
1367
1718
  var DEFAULT_RULES = `
1368
1719
  # Comment rules
1369
1720
 
@@ -1371,7 +1722,7 @@ var DEFAULT_RULES = `
1371
1722
 
1372
1723
  When rules pull in different directions, the earlier one wins.
1373
1724
 
1374
- 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\`.
1725
+ 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).
1375
1726
  2. Keep the meaning. An edit may make a comment clearer, never different.
1376
1727
  3. Follow the style preferences below.
1377
1728
  4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
@@ -1384,10 +1735,12 @@ When rules pull in different directions, the earlier one wins.
1384
1735
  - 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.
1385
1736
  - Separate paragraphs with an empty comment line.
1386
1737
  - Never split a backtick code span across lines.
1738
+ - 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.
1387
1739
  - 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.
1388
1740
  - 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.
1389
1741
  - 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.
1390
1742
  - 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.
1743
+ - 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.
1391
1744
 
1392
1745
  ## Style
1393
1746
 
@@ -1398,8 +1751,16 @@ These are preferences for comment prose. They never apply to code or exact text
1398
1751
  - 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.
1399
1752
  - 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*\` \`\`.
1400
1753
  - 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.
1401
- - Block and JSDoc comments are capitalized and end with punctuation.
1402
- - Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
1754
+ - Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.
1755
+ - 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.
1756
+
1757
+ ## Rust doc comments
1758
+
1759
+ Rust doc comments are Markdown that rustdoc renders.
1760
+
1761
+ - 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)\`.
1762
+ - A link reference definition, like \`\` [\`Foo\`]: crate::Foo \`\`, stays on its own line as it is. Its colon is syntax, not a label.
1763
+ - 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.
1403
1764
 
1404
1765
  ## Editing
1405
1766
 
@@ -1408,11 +1769,11 @@ These are preferences for comment prose. They never apply to code or exact text
1408
1769
  `.trim();
1409
1770
 
1410
1771
  // src/comments/config.ts
1411
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1412
- import { createRequire } from "module";
1413
- import path2 from "path";
1772
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1773
+ import { createRequire as createRequire2 } from "module";
1774
+ import path3 from "path";
1414
1775
  import { pathToFileURL } from "url";
1415
- var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
1776
+ var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".rs"]);
1416
1777
  var CONFIG_FILES = [
1417
1778
  "comments.config.ts",
1418
1779
  "comments.config.mts",
@@ -1463,10 +1824,12 @@ function validateConfig(raw, filepath) {
1463
1824
  assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
1464
1825
  assertType(config.style, "boolean", "style", filepath);
1465
1826
  assertType(config.extensions, "array", "extensions", filepath);
1827
+ assertType(config.include, "array", "include", filepath);
1466
1828
  assertType(config.ignore, "array", "ignore", filepath);
1467
1829
  assertType(config.agent, "object", "agent", filepath);
1468
1830
  assertType(config.hook, "object", "hook", filepath);
1469
1831
  assertStrings(config.extensions, "extensions", filepath);
1832
+ assertStrings(config.include, "include", filepath);
1470
1833
  assertStrings(config.ignore, "ignore", filepath);
1471
1834
  for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
1472
1835
  const value = config[key];
@@ -1499,8 +1862,8 @@ function validateConfig(raw, filepath) {
1499
1862
  }
1500
1863
  assertType(agent.rules, "string", "agent.rules", filepath);
1501
1864
  assertType(agent.instructions, "string", "agent.instructions", filepath);
1502
- if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
1503
- throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
1865
+ if (agent.scope !== void 0 && !["suspicious", "failing", "all"].includes(agent.scope)) {
1866
+ throw new Error(`comments: \`agent.scope\` in ${filepath} must be one of suspicious, failing, all`);
1504
1867
  }
1505
1868
  const hook = config.hook ?? {};
1506
1869
  if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
@@ -1510,25 +1873,25 @@ function validateConfig(raw, filepath) {
1510
1873
  }
1511
1874
  async function loadConfig(cwd, options = {}) {
1512
1875
  if (options.explicitPath) {
1513
- const filepath = path2.resolve(cwd, options.explicitPath);
1876
+ const filepath = path3.resolve(cwd, options.explicitPath);
1514
1877
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1515
1878
  }
1516
- let directory = path2.resolve(cwd);
1879
+ let directory = path3.resolve(cwd);
1517
1880
  for (; ; ) {
1518
1881
  for (const name of CONFIG_FILES) {
1519
- const filepath = path2.join(directory, name);
1520
- if (existsSync(filepath)) {
1882
+ const filepath = path3.join(directory, name);
1883
+ if (existsSync2(filepath)) {
1521
1884
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1522
1885
  }
1523
1886
  }
1524
- const packageJsonPath = path2.join(directory, "package.json");
1525
- if (existsSync(packageJsonPath)) {
1887
+ const packageJsonPath = path3.join(directory, "package.json");
1888
+ if (existsSync2(packageJsonPath)) {
1526
1889
  const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
1527
1890
  if (value !== void 0) {
1528
1891
  return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
1529
1892
  }
1530
1893
  }
1531
- const parent = path2.dirname(directory);
1894
+ const parent = path3.dirname(directory);
1532
1895
  if (directory === options.stopDir || parent === directory) {
1533
1896
  return { config: {} };
1534
1897
  }