@d3lm/lint-preset 1.5.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/comments.cjs CHANGED
@@ -1,8 +1,75 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }require('./chunk-56WU3HSD.cjs');
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunk56WU3HSDcjs = require('./chunk-56WU3HSD.cjs');
2
4
 
3
5
  // src/comments/analyze.ts
4
6
  var _oxcparser = require('oxc-parser');
5
7
 
8
+ // src/comments/rust.ts
9
+ var _fs = require('fs');
10
+ var _module = require('module');
11
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
12
+ var _url = require('url');
13
+ var BINARY_NAME = "comments";
14
+ function platformSuffixes() {
15
+ const { platform, arch } = process;
16
+ switch (platform) {
17
+ case "darwin": {
18
+ return [`darwin-${arch}`, "darwin-universal"];
19
+ }
20
+ case "linux": {
21
+ return arch === "arm" ? ["linux-arm-gnueabihf", "linux-arm-musleabihf"] : [`linux-${arch}-gnu`, `linux-${arch}-musl`];
22
+ }
23
+ case "win32": {
24
+ return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];
25
+ }
26
+ default: {
27
+ return [`${platform}-${arch}`];
28
+ }
29
+ }
30
+ }
31
+ function findBinaries() {
32
+ const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);
33
+ for (let directory = _path2.default.dirname(_url.fileURLToPath.call(void 0, _chunk56WU3HSDcjs.importMetaUrl)); ; directory = _path2.default.dirname(directory)) {
34
+ const found = names.map((name) => _path2.default.join(directory, "native", name)).filter((file) => _fs.existsSync.call(void 0, file));
35
+ if (found.length > 0) {
36
+ return found;
37
+ }
38
+ if (_path2.default.dirname(directory) === directory) {
39
+ return [];
40
+ }
41
+ }
42
+ }
43
+ var binding;
44
+ function loadBinding() {
45
+ if (binding) {
46
+ return binding;
47
+ }
48
+ const require2 = _module.createRequire.call(void 0, _chunk56WU3HSDcjs.importMetaUrl);
49
+ const errors = [];
50
+ for (const file of findBinaries()) {
51
+ try {
52
+ binding = require2(file);
53
+ return binding;
54
+ } catch (error) {
55
+ errors.push(`${file}: ${error.message}`);
56
+ }
57
+ }
58
+ const reason = errors.length > 0 ? `, and loading it failed (${errors.join("; ")})` : "";
59
+ throw new Error(
60
+ `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\`.`
61
+ );
62
+ }
63
+ function scanRust(source) {
64
+ return loadBinding().scanRust(source);
65
+ }
66
+ function rustFingerprint(source) {
67
+ return loadBinding().rustFingerprint(source);
68
+ }
69
+ function isRustCode(text) {
70
+ return loadBinding().isRustCode(text);
71
+ }
72
+
6
73
  // src/comments/style.ts
7
74
  var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
8
75
  var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
@@ -126,12 +193,94 @@ function findCodeSpanEnd(text, start) {
126
193
  }
127
194
  return -1;
128
195
  }
196
+ function isEscaped(text, index) {
197
+ let backslashes = 0;
198
+ while (text[index - 1 - backslashes] === "\\") {
199
+ backslashes += 1;
200
+ }
201
+ return backslashes % 2 === 1;
202
+ }
203
+ function findUnescaped(text, char, start) {
204
+ for (let index = start; index < text.length; index += 1) {
205
+ if (text[index] === "\\") {
206
+ index += 1;
207
+ } else if (text[index] === char) {
208
+ return index;
209
+ }
210
+ }
211
+ return -1;
212
+ }
213
+ function findClosingBracket(text, start) {
214
+ let depth = 0;
215
+ for (let index = start; index < text.length; index += 1) {
216
+ switch (text[index]) {
217
+ case "\\": {
218
+ index += 1;
219
+ break;
220
+ }
221
+ case "`": {
222
+ const close = findCodeSpanEnd(text, index);
223
+ index = close === -1 ? index + backtickRun(text, index) - 1 : close;
224
+ break;
225
+ }
226
+ case "[": {
227
+ depth += 1;
228
+ break;
229
+ }
230
+ case "]": {
231
+ depth -= 1;
232
+ if (depth === 0) {
233
+ return index;
234
+ }
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ return -1;
240
+ }
241
+ function findDestinationEnd(text, start) {
242
+ let depth = 0;
243
+ for (let index = start; index < text.length; index += 1) {
244
+ const char = text[index];
245
+ let close = -1;
246
+ if (char === "\\") {
247
+ index += 1;
248
+ } else if (char === "<" && text.slice(start + 1, index).trim() === "") {
249
+ close = findUnescaped(text, ">", index + 1);
250
+ } else if ((char === '"' || char === "'") && /\s/.test(text[index - 1])) {
251
+ close = findUnescaped(text, char, index + 1);
252
+ } else if (char === "(") {
253
+ depth += 1;
254
+ } else if (char === ")" && --depth === 0) {
255
+ return index;
256
+ }
257
+ index = close === -1 ? index : close;
258
+ }
259
+ return -1;
260
+ }
261
+ function findLinkEnd(text, start) {
262
+ const close = findClosingBracket(text, start);
263
+ if (close === -1 || text[close + 1] !== "(") {
264
+ return close;
265
+ }
266
+ const end = findDestinationEnd(text, close + 1);
267
+ return end === -1 ? close : end;
268
+ }
129
269
  function tokenize(text) {
270
+ return splitWords(text, true);
271
+ }
272
+ function splitWords(text, keepLinks) {
130
273
  const tokens = [];
131
274
  let current = "";
132
275
  let index = 0;
133
276
  while (index < text.length) {
134
277
  const char = text[index];
278
+ const linkEnd = keepLinks && char === "[" && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;
279
+ if (linkEnd !== -1) {
280
+ current += splitWords(text.slice(index, linkEnd + 1), false).join(" ");
281
+ index = linkEnd + 1;
282
+ continue;
283
+ }
135
284
  const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
136
285
  if (char === "`" && close === -1) {
137
286
  const run = backtickRun(text, index);
@@ -227,7 +376,7 @@ function isOrphan(previous, token) {
227
376
  return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
228
377
  }
229
378
  function startsBlock(token) {
230
- return /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
379
+ return /^(?:\+|-+|=+|\*+|_{3,}|\d+[.)]|#{1,6})$/.test(token) || /^(?:>|@[A-Za-z]|`{3}|~{3}|\||<(?:\/?[A-Za-z]|[!?]))/.test(token);
231
380
  }
232
381
  function bindLineStarts(tokens, startsLine2 = startsBlock) {
233
382
  const units = [];
@@ -665,6 +814,7 @@ var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "p
665
814
  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)/;
666
815
  var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
667
816
  var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
817
+ var RUST_LEGAL_COMMENT = /@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
668
818
  var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
669
819
  var BOX_DRAWING = /[\u2500-\u257F]/;
670
820
  var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
@@ -739,12 +889,35 @@ ${text}
739
889
  }
740
890
  return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
741
891
  }
742
- function isCode(texts) {
743
- const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
744
- return parsesAsStatements(text) || parsesAsProperties(text);
892
+ function isCode(texts, language) {
893
+ return language.isCode(texts.map((line) => line.replace(LIST_MARKER, "")).join("\n"));
745
894
  }
746
895
  function isCodeLine(text) {
747
- 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);
896
+ return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text);
897
+ }
898
+ var JAVASCRIPT = {
899
+ name: "javascript",
900
+ isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),
901
+ isCodeLine: (text) => {
902
+ return isCodeLine(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
903
+ },
904
+ legal: LEGAL_COMMENT,
905
+ blockDirective: BLOCK_DIRECTIVE,
906
+ tags: true,
907
+ joinsCodeSpans: true
908
+ };
909
+ var RUST = {
910
+ name: "rust",
911
+ isCode: (text) => isRustCode(text),
912
+ isCodeLine: (text) => {
913
+ 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);
914
+ },
915
+ legal: RUST_LEGAL_COMMENT,
916
+ tags: false,
917
+ joinsCodeSpans: false
918
+ };
919
+ function languageOf(filename) {
920
+ return filename.endsWith(".rs") ? RUST : JAVASCRIPT;
748
921
  }
749
922
  function layoutReason(prose, raw) {
750
923
  if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
@@ -758,8 +931,10 @@ function layoutReason(prose, raw) {
758
931
  }
759
932
  return void 0;
760
933
  }
934
+ var THEMATIC_BREAK = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
761
935
  function isMarkup(prose) {
762
- return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
936
+ const text = prose.replace(LIST_MARKER, "");
937
+ return /^#{1,6}(?:\s|$)/.test(text) || /^\[[^\]]+\]:/.test(text) || /^<(?:\/?[a-z]|[!?])/i.test(text) || text.startsWith(">") || /^(?:=+|-+)$/.test(prose) || THEMATIC_BREAK.test(prose);
763
938
  }
764
939
  function mask(text) {
765
940
  return text.replaceAll(/[^\n]/g, "x");
@@ -786,11 +961,14 @@ function hasAlignedColumns(lines) {
786
961
  }
787
962
  return false;
788
963
  }
789
- function structureOf(lines) {
964
+ function structureOf(lines, language) {
790
965
  const trimmed = lines.map((line) => line.text.trim()).join("\n");
791
966
  const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
792
967
  const proseLines = stripNonProse(trimmed).split("\n");
793
- if (isCode(lines.map((line) => line.text.trim()))) {
968
+ if (isCode(
969
+ lines.map((line) => line.text.trim()),
970
+ language
971
+ )) {
794
972
  return { tier: "code", reason: "code" };
795
973
  }
796
974
  let layout;
@@ -799,7 +977,7 @@ function structureOf(lines) {
799
977
  for (const [position, line] of texts.entries()) {
800
978
  const prose = proseLines[position].trim();
801
979
  layout ??= layoutReason(prose, _nullishCoalesce(_optionalChain([lines, 'access', _6 => _6[position], 'optionalAccess', _7 => _7.text]), () => ( "")));
802
- if (isCodeLine(line.trim())) {
980
+ if (language.isCodeLine(line.trim())) {
803
981
  codeLike = true;
804
982
  } else if (isMarkup(prose)) {
805
983
  manual ??= "markup";
@@ -838,7 +1016,7 @@ var FenceTracker = class {
838
1016
  return true;
839
1017
  }
840
1018
  };
841
- function segment(lines) {
1019
+ function segment(lines, language) {
842
1020
  const paragraphs = [];
843
1021
  const fence = new FenceTracker();
844
1022
  let current;
@@ -875,6 +1053,10 @@ function segment(lines) {
875
1053
  continue;
876
1054
  }
877
1055
  if (line.verbatim) {
1056
+ if (fence.next(text)) {
1057
+ start("code", line, "", "code block");
1058
+ continue;
1059
+ }
878
1060
  start("code", line, "", "directive");
879
1061
  flush();
880
1062
  continue;
@@ -899,7 +1081,7 @@ function segment(lines) {
899
1081
  }
900
1082
  continue;
901
1083
  }
902
- const tag = JSDOC_TAG.exec(text);
1084
+ const tag = language.tags ? JSDOC_TAG.exec(text) : null;
903
1085
  if (tag) {
904
1086
  start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
905
1087
  inCodeTag = CODE_TAGS.has(tag[1]);
@@ -1006,7 +1188,19 @@ function byLine(issues) {
1006
1188
  function sameLayout(a, b) {
1007
1189
  return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
1008
1190
  }
1009
- function analyzeParagraph(paragraph, options) {
1191
+ function splitTokenLine(lines, tokens) {
1192
+ let next = 0;
1193
+ return lines.findIndex((words) => {
1194
+ const line = words.join(" ");
1195
+ let text = "";
1196
+ while (text.length < line.length && next < tokens.length) {
1197
+ text += `${text ? " " : ""}${tokens[next]}`;
1198
+ next += 1;
1199
+ }
1200
+ return text !== line;
1201
+ });
1202
+ }
1203
+ function analyzeParagraph(paragraph, options, language) {
1010
1204
  const first = paragraph.lines[0];
1011
1205
  const last = _nullishCoalesce(paragraph.lines.at(-1), () => ( first));
1012
1206
  const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
@@ -1033,7 +1227,8 @@ function analyzeParagraph(paragraph, options) {
1033
1227
  const structure2 = structureOf(
1034
1228
  prose.lines.map((line) => {
1035
1229
  return { ...line, text: stripBlockSyntax(line.text) };
1036
- })
1230
+ }),
1231
+ language
1037
1232
  );
1038
1233
  const issues2 = keptIssues(prose, structure2, options);
1039
1234
  return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
@@ -1042,7 +1237,7 @@ function analyzeParagraph(paragraph, options) {
1042
1237
  const issues2 = keptIssues(paragraph, structure2, options);
1043
1238
  return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
1044
1239
  };
1045
- const structure = structureOf(paragraph.lines);
1240
+ const structure = structureOf(paragraph.lines, language);
1046
1241
  if (structure) {
1047
1242
  return keep(structure);
1048
1243
  }
@@ -1061,9 +1256,9 @@ function analyzeParagraph(paragraph, options) {
1061
1256
  parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
1062
1257
  keepFirstLine: paragraph.kind === "tag"
1063
1258
  };
1064
- const layOut = (from, settings) => {
1065
- const even2 = layoutTokens(from, firstLimit, restLimit, settings);
1066
- return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
1259
+ const layOut = (from, settings, extra) => {
1260
+ const even2 = layoutTokens(from, firstLimit + extra, restLimit + extra, settings);
1261
+ return { even: even2, wrapped: breakAfterSentence(even2, firstLimit + extra, restLimit + extra, settings) };
1067
1262
  };
1068
1263
  const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
1069
1264
  const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
@@ -1074,22 +1269,26 @@ function analyzeParagraph(paragraph, options) {
1074
1269
  if (!units.every((unit) => staysInLimit(unit))) {
1075
1270
  return keep({ tier: "manual", reason: "unbreakable text across lines" });
1076
1271
  }
1272
+ const text = currentLines.map((words) => words.join(" ")).join("\n");
1273
+ if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes("\n"))) {
1274
+ return keep({ tier: "manual", reason: "code span across lines" });
1275
+ }
1077
1276
  const bodyOf = (lines) => {
1078
1277
  return lines.map((words, position) => {
1079
- const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1080
- return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
1278
+ const text2 = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1279
+ return { ...first, text: text2, verbatim: false, hardBreak: HARD_BREAK.test(text2) };
1081
1280
  });
1082
1281
  };
1083
- const settle = (settings) => {
1282
+ const settle = (settings, extra = 0) => {
1084
1283
  let current = units;
1085
- let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
1284
+ let { even: even2, wrapped: wrapped2 } = layOut(current, settings, extra);
1086
1285
  for (; ; ) {
1087
1286
  const body = bodyOf(wrapped2);
1088
- const rewrap = structureOf(body);
1287
+ const rewrap = structureOf(body, language);
1089
1288
  if (!rewrap) {
1090
1289
  return { even: even2, wrapped: wrapped2 };
1091
1290
  }
1092
- const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1291
+ const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== void 0);
1093
1292
  const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
1094
1293
  const pair = `${current[start - 1]} ${current[start]}`;
1095
1294
  if (body.length < 2 || !staysInLimit(pair)) {
@@ -1097,16 +1296,25 @@ function analyzeParagraph(paragraph, options) {
1097
1296
  return { tier: "manual", reason: `rewrap would read as ${reading}` };
1098
1297
  }
1099
1298
  current = current.toSpliced(start - 1, 2, pair);
1100
- ({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
1299
+ ({ even: even2, wrapped: wrapped2 } = layOut(current, settings, extra));
1101
1300
  }
1102
1301
  };
1103
- const settled = settle(layoutOptions);
1104
- const layout = "wrapped" in settled ? settled : settle({ ...layoutOptions, keepSteps: true });
1302
+ const layoutAt = (extra) => {
1303
+ const settings = {
1304
+ ...layoutOptions,
1305
+ tolerance: layoutOptions.tolerance - extra,
1306
+ parentheticalOverflow: layoutOptions.parentheticalOverflow === void 0 ? void 0 : layoutOptions.parentheticalOverflow - extra
1307
+ };
1308
+ const settled = settle(settings, extra);
1309
+ return "wrapped" in settled ? settled : settle({ ...settings, keepSteps: true }, extra);
1310
+ };
1311
+ const layout = layoutAt(0);
1105
1312
  if (!("wrapped" in layout)) {
1106
1313
  return keep(layout);
1107
1314
  }
1108
1315
  const { even, wrapped } = layout;
1109
1316
  const matchesLayout = sameLayout(wrapped, currentLines);
1317
+ const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1110
1318
  const lineIssues = [];
1111
1319
  const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
1112
1320
  if (!matchesLayout) {
@@ -1165,14 +1373,41 @@ function analyzeParagraph(paragraph, options) {
1165
1373
  lineIssues.push({ type: "unbalanced", line });
1166
1374
  }
1167
1375
  }
1376
+ const splitLine = splitTokenLine(currentLines, tokens);
1377
+ if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {
1378
+ lineIssues.push({ type: "unbalanced", line: splitLine });
1379
+ }
1168
1380
  }
1169
- const suggestion = wrapped.map(
1381
+ const matchesWiderLayout = () => {
1382
+ for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {
1383
+ const wider = layoutAt(extra);
1384
+ if ("wrapped" in wider && sameLayout(wider.wrapped, currentLines)) {
1385
+ return true;
1386
+ }
1387
+ }
1388
+ return false;
1389
+ };
1390
+ const accepted = lineIssues.some((issue) => isVerdict(issue)) && matchesWiderLayout();
1391
+ const suggested = accepted ? currentLines : wrapped;
1392
+ const suggestion = suggested.map(
1170
1393
  (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
1171
1394
  );
1172
- const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
1395
+ const wrapIssues = lineIssues.filter((issue) => !accepted || !isVerdict(issue)).map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
1173
1396
  const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
1174
1397
  return { ...base, reflowable: true, issues, suggestion };
1175
1398
  }
1399
+ var LINE_COMMENT = "line comment";
1400
+ function analyzeLineParagraph(paragraph, options, language) {
1401
+ const first = paragraph.lines[0];
1402
+ const last = _nullishCoalesce(paragraph.lines.at(-1), () => ( first));
1403
+ const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
1404
+ if (paragraph.kind === "code") {
1405
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };
1406
+ }
1407
+ const tier = _optionalChain([structureOf, 'call', _41 => _41(paragraph.lines, language), 'optionalAccess', _42 => _42.tier]);
1408
+ const issues = tier === "code" || tier === "layout" ? [] : overflowIssues(paragraph.lines, options);
1409
+ return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };
1410
+ }
1176
1411
  function computeLineStarts(source) {
1177
1412
  const starts = [0];
1178
1413
  for (let index = 0; index < source.length; index += 1) {
@@ -1198,31 +1433,42 @@ function lineOf(lineStarts, offset) {
1198
1433
  function stripCarriageReturn(line) {
1199
1434
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1200
1435
  }
1201
- function shapeBlock(comment, sourceLines, lineStarts) {
1436
+ function blockKind(comment, language) {
1437
+ if (language.name === "rust") {
1438
+ return comment.doc ? `${comment.doc}-block-doc` : "block";
1439
+ }
1440
+ return comment.value.startsWith("*") ? "jsdoc" : "block";
1441
+ }
1442
+ function wholeComment(shape, lines, paragraphKind, reason) {
1443
+ const otherLines = lines.map((text, offset) => {
1444
+ return { index: shape.startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1445
+ });
1446
+ return { ...shape, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1447
+ }
1448
+ function isLegal(value, language) {
1449
+ return language.legal.test(replaceKeepingLines(value, CODE_SPAN, "``"));
1450
+ }
1451
+ function shapeBlock(comment, sourceLines, lineStarts, language) {
1202
1452
  const startLine = lineOf(lineStarts, comment.start);
1203
1453
  const endLine = lineOf(lineStarts, comment.end - 1);
1204
- const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
1454
+ const shape = { kind: blockKind(comment, language), startLine, endLine };
1205
1455
  const lines = sourceLines.slice(startLine, endLine + 1);
1206
- const wholeComment = (paragraphKind, reason) => {
1207
- const otherLines = lines.map((text, offset) => {
1208
- return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1209
- });
1210
- return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1211
- };
1212
- const value = comment.value.replace(/^\*/, "");
1213
- if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
1214
- return wholeComment("code", "legal comment");
1456
+ const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\*/, "");
1457
+ if (isLegal(comment.value, language)) {
1458
+ return wholeComment(shape, lines, "code", "legal comment");
1215
1459
  }
1216
- if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
1217
- return wholeComment("code", "directive");
1460
+ if (LINE_DIRECTIVE.test(value) || _optionalChain([language, 'access', _43 => _43.blockDirective, 'optionalAccess', _44 => _44.test, 'call', _45 => _45(value)])) {
1461
+ return wholeComment(shape, lines, "code", "directive");
1218
1462
  }
1219
1463
  const other = () => {
1220
1464
  return wholeComment(
1465
+ shape,
1466
+ lines,
1221
1467
  "other",
1222
1468
  startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
1223
1469
  );
1224
1470
  };
1225
- if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(_nullishCoalesce(lines.at(-1), () => ( "")))) {
1471
+ if (startLine === endLine || !/^\s*\/\*[*!]?$/.test(lines[0]) || !/^\s*\*\/$/.test(_nullishCoalesce(lines.at(-1), () => ( "")))) {
1226
1472
  return other();
1227
1473
  }
1228
1474
  const body = [];
@@ -1241,7 +1487,52 @@ function shapeBlock(comment, sourceLines, lineStarts) {
1241
1487
  hardBreak: HARD_BREAK.test(text)
1242
1488
  });
1243
1489
  }
1244
- return { kind, startLine, endLine, paragraphs: segment(body) };
1490
+ return { ...shape, paragraphs: segment(body, language) };
1491
+ }
1492
+ function shapeLineRun(group, sourceLines, lineStarts, language) {
1493
+ const startLine = lineOf(lineStarts, group[0].start);
1494
+ const endLine = startLine + group.length - 1;
1495
+ const kind = group[0].doc === "inner" ? "inner-line-doc" : "outer-line-doc";
1496
+ const shape = { kind, startLine, endLine };
1497
+ const lines = sourceLines.slice(startLine, endLine + 1);
1498
+ const values = group.map((comment) => comment.value.slice(1));
1499
+ if (isLegal(values.join("\n"), language)) {
1500
+ return wholeComment(shape, lines, "code", "legal comment");
1501
+ }
1502
+ if (LINE_DIRECTIVE.test(values[0])) {
1503
+ return wholeComment(shape, lines, "code", "directive");
1504
+ }
1505
+ const body = lines.map((line, offset) => {
1506
+ const [, marker, space, text] = _nullishCoalesce(/^(\s*\/\/[/!])( ?)(.*)$/.exec(line), () => ( ["", "", "", line]));
1507
+ return {
1508
+ index: startLine + offset,
1509
+ prefix: `${marker} `,
1510
+ text: text.trimEnd(),
1511
+ verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
1512
+ hardBreak: HARD_BREAK.test(text)
1513
+ };
1514
+ });
1515
+ return { ...shape, paragraphs: segment(body, language) };
1516
+ }
1517
+ function shapeLineComment(group, sourceLines, lineStarts, language) {
1518
+ const startLine = lineOf(lineStarts, group[0].start);
1519
+ const endLine = startLine + group.length - 1;
1520
+ const shape = { kind: "line", startLine, endLine };
1521
+ const lines = sourceLines.slice(startLine, endLine + 1);
1522
+ if (isLegal(group.map((comment) => comment.value).join("\n"), language)) {
1523
+ return wholeComment(shape, lines, "code", "legal comment");
1524
+ }
1525
+ const body = lines.map((line, offset) => {
1526
+ const [, marker, space, text] = _nullishCoalesce(/^(\s*\/\/[/!]*)( ?)(.*)$/.exec(line), () => ( ["", "", "", line]));
1527
+ return {
1528
+ index: startLine + offset,
1529
+ prefix: `${marker}${space}`,
1530
+ text: text.trimEnd(),
1531
+ verbatim: LINE_DIRECTIVE.test(text),
1532
+ hardBreak: HARD_BREAK.test(text)
1533
+ };
1534
+ });
1535
+ return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };
1245
1536
  }
1246
1537
  function ownsLines(comment, source, lineStarts) {
1247
1538
  const startLine = lineOf(lineStarts, comment.start);
@@ -1251,12 +1542,12 @@ function ownsLines(comment, source, lineStarts) {
1251
1542
  const after = source.slice(comment.end, lineEnd);
1252
1543
  return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
1253
1544
  }
1254
- function singleLineLength(paragraph, options) {
1545
+ function singleLineLength(paragraph, options, language) {
1255
1546
  const [first] = paragraph.lines;
1256
1547
  if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
1257
1548
  return sourceLineLength(first, options.tabWidth);
1258
1549
  }
1259
- if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
1550
+ if (paragraph.kind !== "prose" || structureOf(paragraph.lines, language)) {
1260
1551
  return Infinity;
1261
1552
  }
1262
1553
  const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
@@ -1268,9 +1559,61 @@ function parseSource(filename, source) {
1268
1559
  }
1269
1560
  function describeFailure(errors, lineStarts) {
1270
1561
  const [first] = errors;
1271
- const offset = _optionalChain([first, 'access', _41 => _41.labels, 'access', _42 => _42.at, 'call', _43 => _43(0), 'optionalAccess', _44 => _44.start]);
1562
+ const offset = _optionalChain([first, 'access', _46 => _46.labels, 'access', _47 => _47.at, 'call', _48 => _48(0), 'optionalAccess', _49 => _49.start]);
1272
1563
  return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
1273
1564
  }
1565
+ function scanJavaScript(filename, text, lineStarts) {
1566
+ const { program, comments, errors } = parseSource(filename, text);
1567
+ if (errors.length > 0 && program.body.length === 0) {
1568
+ return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1569
+ }
1570
+ return {
1571
+ comments: comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")))
1572
+ };
1573
+ }
1574
+ function scanRustSource(text, lineStarts) {
1575
+ const { comments, error } = scanRust(text);
1576
+ if (error) {
1577
+ return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };
1578
+ }
1579
+ return {
1580
+ comments: comments.map((comment) => {
1581
+ return {
1582
+ type: comment.block ? "Block" : "Line",
1583
+ value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),
1584
+ start: comment.start,
1585
+ end: comment.end,
1586
+ doc: comment.doc
1587
+ };
1588
+ })
1589
+ };
1590
+ }
1591
+ function groupLineRuns(comments, text, lineStarts, markerOf) {
1592
+ const groups = [];
1593
+ let current;
1594
+ let previousLine = -1;
1595
+ let indent = "";
1596
+ let marker;
1597
+ for (const comment of comments) {
1598
+ const next = comment.type === "Line" ? markerOf(comment) : void 0;
1599
+ if (next === void 0 || !ownsLines(comment, text, lineStarts)) {
1600
+ current = void 0;
1601
+ continue;
1602
+ }
1603
+ const line = lineOf(lineStarts, comment.start);
1604
+ const before = text.slice(lineStarts[line], comment.start);
1605
+ if (current && line === previousLine + 1 && next === marker && before === indent) {
1606
+ current.push(comment);
1607
+ } else {
1608
+ current = [comment];
1609
+ indent = before;
1610
+ marker = next;
1611
+ groups.push(current);
1612
+ }
1613
+ previousLine = line;
1614
+ }
1615
+ return groups;
1616
+ }
1274
1617
  function analyzeSource(source, filename, options) {
1275
1618
  return inspectSource(source, filename, options).comments;
1276
1619
  }
@@ -1279,39 +1622,66 @@ function inspectSource(source, filename, options) {
1279
1622
  throw new Error("comments: maxWidth must be an integer at least as large as width");
1280
1623
  }
1281
1624
  const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
1282
- const { program, comments, errors } = parseSource(filename, text);
1625
+ const language = languageOf(filename);
1283
1626
  const lineStarts = computeLineStarts(text);
1284
- if (errors.length > 0 && program.body.length === 0) {
1285
- return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1627
+ const scanned = language.name === "rust" ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);
1628
+ if (scanned.syntaxError) {
1629
+ return { comments: [], syntaxError: scanned.syntaxError };
1286
1630
  }
1287
1631
  const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
1288
- const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
1289
1632
  const reports = [];
1290
- const blocks = parsed.filter((comment) => {
1291
- return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1292
- });
1293
- for (const block of blocks) {
1294
- const shape = shapeBlock(block, sourceLines, lineStarts);
1295
- const singleLineWidth = Math.max(
1296
- options.width,
1297
- Math.min(_nullishCoalesce(options.singleLineWidth, () => ( options.width)), _nullishCoalesce(options.maxWidth, () => ( Infinity)))
1298
- );
1299
- const singleLineLimit = Math.min(singleLineWidth + options.tolerance, _nullishCoalesce(options.maxWidth, () => ( Infinity)));
1300
- const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
1301
- const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1302
- const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1303
- reports.push({
1633
+ const report = (shape, lines, paragraphs) => {
1634
+ return {
1304
1635
  file: filename,
1305
1636
  kind: shape.kind,
1306
1637
  startLine: shape.startLine + 1,
1307
1638
  endLine: shape.endLine + 1,
1308
- indent: _nullishCoalesce(_optionalChain([/^[\t ]*/, 'access', _45 => _45.exec, 'call', _46 => _46(lines[0]), 'optionalAccess', _47 => _47[0]]), () => ( "")),
1639
+ indent: _nullishCoalesce(_optionalChain([/^[\t ]*/, 'access', _50 => _50.exec, 'call', _51 => _51(lines[0]), 'optionalAccess', _52 => _52[0]]), () => ( "")),
1309
1640
  width: options.width,
1310
1641
  maxWidth: options.maxWidth,
1311
1642
  lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
1312
1643
  text: lines.join("\n"),
1313
- paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
1644
+ paragraphs
1645
+ };
1646
+ };
1647
+ const blocks = scanned.comments.filter((comment) => {
1648
+ return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1649
+ });
1650
+ const runs = (markerOf) => {
1651
+ return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {
1652
+ return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));
1314
1653
  });
1654
+ };
1655
+ const shapes = [
1656
+ ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),
1657
+ ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),
1658
+ ...runs((comment) => comment.doc ? void 0 : "//").map(
1659
+ (group) => shapeLineComment(group, sourceLines, lineStarts, language)
1660
+ )
1661
+ ].toSorted((a, b) => a.startLine - b.startLine);
1662
+ for (const shape of shapes) {
1663
+ const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1664
+ if (shape.kind === "line") {
1665
+ const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));
1666
+ if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {
1667
+ reports.push(report(shape, lines, paragraphs));
1668
+ }
1669
+ continue;
1670
+ }
1671
+ const singleLineWidth = Math.max(
1672
+ options.width,
1673
+ Math.min(_nullishCoalesce(options.singleLineWidth, () => ( options.width)), _nullishCoalesce(options.maxWidth, () => ( Infinity)))
1674
+ );
1675
+ const singleLineLimit = Math.min(singleLineWidth + options.tolerance, _nullishCoalesce(options.maxWidth, () => ( Infinity)));
1676
+ const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;
1677
+ const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1678
+ reports.push(
1679
+ report(
1680
+ shape,
1681
+ lines,
1682
+ shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language))
1683
+ )
1684
+ );
1315
1685
  }
1316
1686
  return { comments: reports };
1317
1687
  }
@@ -1332,6 +1702,9 @@ function rewrapComment(report) {
1332
1702
  }
1333
1703
  var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
1334
1704
  function codeFingerprint(source, filename) {
1705
+ if (languageOf(filename).name === "rust") {
1706
+ return rustFingerprint(source);
1707
+ }
1335
1708
  const { program, errors } = parseSource(filename, source);
1336
1709
  if (errors.length > 0) {
1337
1710
  return JSON.stringify({ source, errors: errors.map((error) => error.message) });
@@ -1344,8 +1717,8 @@ function codeFingerprint(source, filename) {
1344
1717
 
1345
1718
  // src/comments/agent.ts
1346
1719
  var _child_process = require('child_process');
1347
- var _fs = require('fs');
1348
- var _path = require('path'); var _path2 = _interopRequireDefault(_path);
1720
+
1721
+
1349
1722
  var DEFAULT_RULES = `
1350
1723
  # Comment rules
1351
1724
 
@@ -1353,7 +1726,7 @@ var DEFAULT_RULES = `
1353
1726
 
1354
1727
  When rules pull in different directions, the earlier one wins.
1355
1728
 
1356
- 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\`.
1729
+ 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).
1357
1730
  2. Keep the meaning. An edit may make a comment clearer, never different.
1358
1731
  3. Follow the style preferences below.
1359
1732
  4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
@@ -1366,10 +1739,12 @@ When rules pull in different directions, the earlier one wins.
1366
1739
  - 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.
1367
1740
  - Separate paragraphs with an empty comment line.
1368
1741
  - Never split a backtick code span across lines.
1742
+ - 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.
1369
1743
  - 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.
1370
1744
  - 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.
1371
1745
  - 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.
1372
1746
  - 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.
1747
+ - 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.
1373
1748
 
1374
1749
  ## Style
1375
1750
 
@@ -1380,8 +1755,16 @@ These are preferences for comment prose. They never apply to code or exact text
1380
1755
  - 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.
1381
1756
  - 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*\` \`\`.
1382
1757
  - 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.
1383
- - Block and JSDoc comments are capitalized and end with punctuation.
1384
- - Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
1758
+ - Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.
1759
+ - 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.
1760
+
1761
+ ## Rust doc comments
1762
+
1763
+ Rust doc comments are Markdown that rustdoc renders.
1764
+
1765
+ - 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)\`.
1766
+ - A link reference definition, like \`\` [\`Foo\`]: crate::Foo \`\`, stays on its own line as it is. Its colon is syntax, not a label.
1767
+ - 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.
1385
1768
 
1386
1769
  ## Editing
1387
1770
 
@@ -1391,10 +1774,10 @@ These are preferences for comment prose. They never apply to code or exact text
1391
1774
 
1392
1775
  // src/comments/config.ts
1393
1776
 
1394
- var _module = require('module');
1395
1777
 
1396
- var _url = require('url');
1397
- var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
1778
+
1779
+
1780
+ var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".rs"]);
1398
1781
  var CONFIG_FILES = [
1399
1782
  "comments.config.ts",
1400
1783
  "comments.config.mts",
@@ -1445,10 +1828,12 @@ function validateConfig(raw, filepath) {
1445
1828
  assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
1446
1829
  assertType(config.style, "boolean", "style", filepath);
1447
1830
  assertType(config.extensions, "array", "extensions", filepath);
1831
+ assertType(config.include, "array", "include", filepath);
1448
1832
  assertType(config.ignore, "array", "ignore", filepath);
1449
1833
  assertType(config.agent, "object", "agent", filepath);
1450
1834
  assertType(config.hook, "object", "hook", filepath);
1451
1835
  assertStrings(config.extensions, "extensions", filepath);
1836
+ assertStrings(config.include, "include", filepath);
1452
1837
  assertStrings(config.ignore, "ignore", filepath);
1453
1838
  for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
1454
1839
  const value = config[key];
@@ -1481,8 +1866,8 @@ function validateConfig(raw, filepath) {
1481
1866
  }
1482
1867
  assertType(agent.rules, "string", "agent.rules", filepath);
1483
1868
  assertType(agent.instructions, "string", "agent.instructions", filepath);
1484
- if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
1485
- throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
1869
+ if (agent.scope !== void 0 && !["suspicious", "failing", "all"].includes(agent.scope)) {
1870
+ throw new Error(`comments: \`agent.scope\` in ${filepath} must be one of suspicious, failing, all`);
1486
1871
  }
1487
1872
  const hook = _nullishCoalesce(config.hook, () => ( {}));
1488
1873
  if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {