@d3lm/lint-preset 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -245,7 +245,7 @@ comments agent # hand suspicious comments to an agent, then
245
245
  comments check --cwd ../other-app # run as if started in another project
246
246
  ```
247
247
 
248
- Without targets, only comments in uncommitted changes and untracked files are checked. `--staged` limits that to staged changes, `--base main` checks everything since the merge base with `main`, and `--repo` checks every file. Targets can be files, directories, `file:line`, or `file:start-end`. A comment that contains `comments-ignore` is skipped. A file with a syntax error is skipped as a whole and named in a warning, because the parser stops at the error and never sees the comments after it. JSX is read in `.js` files too.
248
+ Without targets, only comments in uncommitted changes and untracked files are checked. `--base main` checks everything since the merge base with `main`, and `--repo` checks every file. `--whole-files` checks every comment in the changed files instead of only the comments on changed lines. Targets can be files, directories, `file:line`, or `file:start-end`. A comment that contains `comments-ignore` is skipped. A file with a syntax error is skipped as a whole and named in a warning, because the parser stops at the error and never sees the comments after it. JSX is read in `.js` files too.
249
249
 
250
250
  With `--json`, each paragraph of a comment comes with its issues and a `suggestion` that holds the exact rewrapped lines. Code blocks, `@example`, tables, drawings, directives, and other [text kept as written](#text-kept-as-written) have no suggestion.
251
251
 
@@ -285,7 +285,7 @@ A comment with an issue in a part that is kept is handed to the agent, which can
285
285
 
286
286
  ### Configuration
287
287
 
288
- `width` is the preferred line length; `maxWidth` is the hard maximum, including indentation and the comment prefix. The CLI defaults to a maximum of 120 columns (or `width`, if larger). A balanced parenthetical can extend a line past `width` when this keeps it together within `maxWidth`. In longer paragraphs, the wrapper balances all lines together and may split a parenthetical when that produces a better layout. If the parenthetical cannot fit on a line of its own, it wraps normally. For example, `width: 80, maxWidth: 120` keeps `(JSDoc types, \`{@link ...}\`)` together in the example below:
288
+ `width` is the preferred line length; `maxWidth` is the hard maximum, including indentation and the comment prefix. The CLI defaults to a maximum of 120 columns (or `width`, if larger). A balanced parenthetical can extend a line past `width` when this keeps it together within `maxWidth`. In longer paragraphs, the wrapper balances all lines together and may split a parenthetical when that produces a better layout. A line never sticks out more than 15 columns past the line before it, like a full second line after a first line that the orphan rule keeps short, when up to two more lines avoid that. If the parenthetical cannot fit on a line of its own, it wraps normally. For example, `width: 80, maxWidth: 120` keeps `(JSDoc types, \`{@link ...}\`)` together in the example below:
289
289
 
290
290
  ```ts
291
291
  /**
@@ -294,7 +294,7 @@ A comment with an issue in a part that is kept is handed to the agent, which can
294
294
  */
295
295
  ```
296
296
 
297
- The separate `singleLineWidth` allowance is capped by `maxWidth`. Code and documented layout regions retain their exemptions. An indivisible token in measured prose that exceeds `maxWidth` is reported as overflow; the wrapper preserves it, so resolving that case requires changing the maximum or the comment manually. The legacy `tolerance` option sets a maximum of `width + tolerance` when `maxWidth` is omitted; an explicit `maxWidth` takes precedence. Direct `analyzeSource` callers can omit `maxWidth` to retain the legacy width/tolerance behavior.
297
+ The separate `singleLineWidth` allowance is capped by `maxWidth`. Code and documented layout regions retain their exemptions. An indivisible token in measured prose that exceeds `maxWidth` is reported as overflow; the wrapper preserves it, so resolving that case requires changing the maximum or the comment manually. `tolerance` lets prose lines go up to that many columns past `width` when that saves a line in a paragraph of three or more lines, and it never goes past `maxWidth`. It defaults to 0. When `maxWidth` is omitted, `tolerance` also sets a legacy maximum of `width + tolerance`. Direct `analyzeSource` callers can omit `maxWidth` to retain the legacy width/tolerance behavior.
298
298
 
299
299
  Like prettier, `comments` looks for a config file in the current directory and its parents, up to the repository root. The config can be a `comments.config.{ts,mts,js,mjs,cjs}`, `.commentsrc`, or `.commentsrc.json` file, or a `comments` key in `package.json`. All options are optional.
300
300
 
@@ -306,6 +306,8 @@ export default defineConfig({
306
306
  // defaults to prettier's printWidth, or 80
307
307
  width: 100,
308
308
  maxWidth: 120,
309
+ // prose lines may go this many columns past `width` when that saves a line
310
+ tolerance: 0,
309
311
  // a comment that is one paragraph stays on one line up to this length, and wraps to `width` beyond it
310
312
  singleLineWidth: 120,
311
313
  avoidSentenceOrphans: true,
@@ -24,7 +24,7 @@ When rules pull in different directions, the earlier one wins.
24
24
 
25
25
  ## Layout
26
26
 
27
- - After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.
27
+ - After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together, and lines may go up to the configured tolerance past the width when that saves a line. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.
28
28
  - Don't end a line with the first word of a new sentence, clause, or parenthetical, like \`This\` after a period, \`the\` after a comma, or \`(the\`. Move it to the next line instead.
29
29
  - In a paragraph of two lines, the first line ends with a sentence whenever the rest fits on the second line and is no longer than the first. Don't end it with the start of the next sentence, like \`A wrap\` in \`...lay out differently. A wrap\`. Move those words to the second line instead.
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.
@@ -578,6 +578,8 @@ function hasWidow(lines) {
578
578
  }
579
579
  var FULLNESS_WEIGHT = 0.07;
580
580
  var PARENTHETICAL_SPLIT_WEIGHT = 2;
581
+ var MAX_STEP = 15;
582
+ var EXTRA_LINES = 2;
581
583
  function fewestLines(starts, target) {
582
584
  const total = starts.length - 1;
583
585
  const fewest = new Int32Array(total + 1);
@@ -710,7 +712,56 @@ function balanceTokens(tokens, firstLimit, restLimit, count, options, preferredL
710
712
  const bounds = [0, ...best.breaks, total];
711
713
  return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));
712
714
  }
715
+ function hasStep(lines, offset = 0) {
716
+ return lines.some((line, index) => {
717
+ if (index === 0) {
718
+ return false;
719
+ }
720
+ const before = lineLength(lines[index - 1]) + (index === 1 ? offset : 0);
721
+ return lineLength(line) - before > MAX_STEP;
722
+ });
723
+ }
724
+ function splitCostsOf(tokens, limit) {
725
+ const splitCosts = /* @__PURE__ */ new Map();
726
+ for (const group of parentheticalGroups(tokens, limit)) {
727
+ const length = lineLength(tokens.slice(group.start, group.end));
728
+ for (let at = group.start + 1; at < group.end; at += 1) {
729
+ splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);
730
+ }
731
+ }
732
+ return splitCosts;
733
+ }
734
+ function hasParentheticals(tokens, restLimit, options) {
735
+ if (options.parentheticalOverflow === void 0) {
736
+ return false;
737
+ }
738
+ return bindParentheticals(tokens, restLimit + options.parentheticalOverflow).some((token) => isParenthetical(token));
739
+ }
713
740
  function layoutTokens(tokens, firstLimit, restLimit, options) {
741
+ const lines = layoutLines(tokens, firstLimit, restLimit, options);
742
+ const offset = restLimit - firstLimit;
743
+ if (options.keepFirstLine || options.keepSteps || !hasStep(lines, offset)) {
744
+ return lines;
745
+ }
746
+ const overflow = hasParentheticals(tokens, restLimit, options) ? options.parentheticalOverflow ?? 0 : 0;
747
+ const splitCosts = overflow > 0 ? splitCostsOf(tokens, restLimit + overflow) : /* @__PURE__ */ new Map();
748
+ for (let count = lines.length + 1; count <= lines.length + EXTRA_LINES; count += 1) {
749
+ const even = balanceTokens(
750
+ tokens,
751
+ firstLimit + overflow,
752
+ restLimit + overflow,
753
+ count,
754
+ options,
755
+ restLimit,
756
+ splitCosts
757
+ );
758
+ if (even && !hasStep(even, offset)) {
759
+ return even;
760
+ }
761
+ }
762
+ return lines;
763
+ }
764
+ function layoutLines(tokens, firstLimit, restLimit, options) {
714
765
  if (options.parentheticalOverflow !== void 0) {
715
766
  const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);
716
767
  if (grouped.some((token) => isParenthetical(token))) {
@@ -744,14 +795,7 @@ function layoutTokens(tokens, firstLimit, restLimit, options) {
744
795
  return expanded;
745
796
  });
746
797
  }
747
- const splitCosts = /* @__PURE__ */ new Map();
748
- const groups = parentheticalGroups(tokens, restLimit + options.parentheticalOverflow);
749
- for (const group of groups) {
750
- const length = lineLength(tokens.slice(group.start, group.end));
751
- for (let at = group.start + 1; at < group.end; at += 1) {
752
- splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);
753
- }
754
- }
798
+ const splitCosts = splitCostsOf(tokens, restLimit + options.parentheticalOverflow);
755
799
  const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: void 0 });
756
800
  for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {
757
801
  const balanced2 = balanceTokens(
@@ -781,6 +825,19 @@ function layoutTokens(tokens, firstLimit, restLimit, options) {
781
825
  return lines;
782
826
  }
783
827
  }
828
+ if (options.tolerance > 0 && greedy.length > 2) {
829
+ const fewer = balanceTokens(
830
+ tokens,
831
+ firstLimit + options.tolerance,
832
+ restLimit + options.tolerance,
833
+ greedy.length - 1,
834
+ options,
835
+ restLimit
836
+ );
837
+ if (fewer && !hasStep(fewer, restLimit - firstLimit)) {
838
+ return fewer;
839
+ }
840
+ }
784
841
  const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);
785
842
  if (balanced) {
786
843
  return balanced;
@@ -1270,11 +1327,11 @@ function analyzeParagraph(paragraph, options) {
1270
1327
  parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
1271
1328
  keepFirstLine: paragraph.kind === "tag"
1272
1329
  };
1273
- const layOut = (from) => {
1274
- const even2 = layoutTokens(from, firstLimit, restLimit, layoutOptions);
1275
- return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, layoutOptions) };
1330
+ const layOut = (from, settings) => {
1331
+ const even2 = layoutTokens(from, firstLimit, restLimit, settings);
1332
+ return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
1276
1333
  };
1277
- let units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
1334
+ const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
1278
1335
  const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
1279
1336
  const written = currentLines.map((words) => ` ${words.join(" ")} `);
1280
1337
  const staysInLimit = (unit) => {
@@ -1283,29 +1340,38 @@ function analyzeParagraph(paragraph, options) {
1283
1340
  if (!units.every((unit) => staysInLimit(unit))) {
1284
1341
  return keep({ tier: "manual", reason: "unbreakable text across lines" });
1285
1342
  }
1286
- let { even, wrapped } = layOut(units);
1287
1343
  const bodyOf = (lines) => {
1288
1344
  return lines.map((words, position) => {
1289
1345
  const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1290
1346
  return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
1291
1347
  });
1292
1348
  };
1293
- for (; ; ) {
1294
- const body = bodyOf(wrapped);
1295
- const rewrap = structureOf(body);
1296
- if (!rewrap) {
1297
- break;
1298
- }
1299
- const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1300
- const start = wrapped.slice(0, Math.max(tipping, 1)).flat().length;
1301
- const pair = `${units[start - 1]} ${units[start]}`;
1302
- if (body.length < 2 || !staysInLimit(pair)) {
1303
- const reading = rewrap.tier === "code-like" ? "code" : rewrap.reason;
1304
- return keep({ tier: "manual", reason: `rewrap would read as ${reading}` });
1349
+ const settle = (settings) => {
1350
+ let current = units;
1351
+ let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
1352
+ for (; ; ) {
1353
+ const body = bodyOf(wrapped2);
1354
+ const rewrap = structureOf(body);
1355
+ if (!rewrap) {
1356
+ return { even: even2, wrapped: wrapped2 };
1357
+ }
1358
+ const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1359
+ const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
1360
+ const pair = `${current[start - 1]} ${current[start]}`;
1361
+ if (body.length < 2 || !staysInLimit(pair)) {
1362
+ const reading = rewrap.tier === "code-like" ? "code" : rewrap.reason;
1363
+ return { tier: "manual", reason: `rewrap would read as ${reading}` };
1364
+ }
1365
+ current = current.toSpliced(start - 1, 2, pair);
1366
+ ({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
1305
1367
  }
1306
- units = units.toSpliced(start - 1, 2, pair);
1307
- ({ even, wrapped } = layOut(units));
1368
+ };
1369
+ const settled = settle(layoutOptions);
1370
+ const layout = "wrapped" in settled ? settled : settle({ ...layoutOptions, keepSteps: true });
1371
+ if (!("wrapped" in layout)) {
1372
+ return keep(layout);
1308
1373
  }
1374
+ const { even, wrapped } = layout;
1309
1375
  const matchesLayout = sameLayout(wrapped, currentLines);
1310
1376
  const lineIssues = [];
1311
1377
  const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
@@ -2114,23 +2180,12 @@ function snapshotChanges(root) {
2114
2180
  var DIFF_FLAGS = ["diff", "-U0", "--no-color", "--no-ext-diff", "--no-renames", "--src-prefix=a/", "--dst-prefix=b/"];
2115
2181
  function getChangedLines(root, scope = {}) {
2116
2182
  if (!hasHead(root)) {
2117
- const files = scope.staged ? splitPaths(gitOrThrow(root, ["diff", "-z", "--cached", "--name-only", "--diff-filter=d"])) : listFiles2(root);
2118
- return new Map(files.map((file) => [file, "all"]));
2119
- }
2120
- let args;
2121
- if (scope.staged) {
2122
- args = [...DIFF_FLAGS, "--cached", "HEAD"];
2123
- } else if (scope.base) {
2124
- const mergeBase = gitOrThrow(root, ["merge-base", scope.base, "HEAD"]).trim();
2125
- args = [...DIFF_FLAGS, mergeBase];
2126
- } else {
2127
- args = [...DIFF_FLAGS, "HEAD"];
2183
+ return new Map(listFiles2(root).map((file) => [file, "all"]));
2128
2184
  }
2129
- const changed = parseUnifiedDiff(gitOrThrow(root, args));
2130
- if (!scope.staged) {
2131
- for (const file of listUntrackedFiles(root)) {
2132
- changed.set(file, "all");
2133
- }
2185
+ const target = scope.base ? gitOrThrow(root, ["merge-base", scope.base, "HEAD"]).trim() : "HEAD";
2186
+ const changed = parseUnifiedDiff(gitOrThrow(root, [...DIFF_FLAGS, target]));
2187
+ for (const file of listUntrackedFiles(root)) {
2188
+ changed.set(file, "all");
2134
2189
  }
2135
2190
  return changed;
2136
2191
  }
@@ -2245,7 +2300,7 @@ var Project = class {
2245
2300
  throw new Error("comments: not a git repository, so pass files or directories explicitly");
2246
2301
  }
2247
2302
  for (const [file, lines] of getChangedLines(this.root, scope)) {
2248
- add(path4.join(this.root, file), lines);
2303
+ add(path4.join(this.root, file), scope.wholeFiles ? "all" : lines);
2249
2304
  }
2250
2305
  return files;
2251
2306
  }
@@ -2756,8 +2811,8 @@ Targets are files, directories, file:line, or file:start-end. Without targets, o
2756
2811
  overlapping uncommitted changes (and untracked files) are considered.
2757
2812
 
2758
2813
  Options:
2759
- --staged Only consider staged changes
2760
2814
  --base <ref> Consider changes since the merge base with <ref>
2815
+ --whole-files Consider every comment in the changed files, not only comments on changed lines
2761
2816
  --repo Consider every file in the repository
2762
2817
  --suspicious Only comments with layout issues (overflow, early line ends, last lines of one or two words)
2763
2818
  or hints (possible orphans, em-dashes, banners, ...). For agent and hook (the default), only
@@ -2777,7 +2832,7 @@ Config overrides (take precedence over the config file):
2777
2832
  --width <n> Preferred comment line length
2778
2833
  --max-width <n> Hard limit for prose (default: 120, never below width)
2779
2834
  --single-line-width <n> Longest line a one-paragraph comment may stay on (default: 120)
2780
- --tolerance <n> Legacy hard limit of width + n, unless max-width is set
2835
+ --tolerance <n> Columns prose may go past width to save a line, within max-width (default: 0)
2781
2836
  --[no-]orphans Keep a sentence's first word off the end of a line, and end the first of two lines
2782
2837
  with a sentence when the second is no longer than the first (default: on)
2783
2838
  --[no-]style Look for possible semicolons, em-dashes, dashes, label colons, emphasis, and banners
@@ -2803,8 +2858,8 @@ var OPTIONS = {
2803
2858
  "max-files": { type: "string" },
2804
2859
  "max-agents": { type: "string" },
2805
2860
  instructions: { type: "string" },
2806
- staged: { type: "boolean" },
2807
2861
  base: { type: "string" },
2862
+ "whole-files": { type: "boolean" },
2808
2863
  repo: { type: "boolean" },
2809
2864
  suspicious: { type: "boolean" },
2810
2865
  all: { type: "boolean" },
@@ -2891,7 +2946,7 @@ async function createProject(cwd, values) {
2891
2946
  return { project, configPath: loaded.filepath };
2892
2947
  }
2893
2948
  function scopeFrom(positionals, values) {
2894
- return { targets: positionals, staged: values.staged, base: values.base, repo: values.repo };
2949
+ return { targets: positionals, base: values.base, wholeFiles: values["whole-files"], repo: values.repo };
2895
2950
  }
2896
2951
  function selectFrom(values, project) {
2897
2952
  if (values.all) {
@@ -2952,9 +3007,8 @@ function commentsOf(analyses) {
2952
3007
  function hintEmptyScope(analyses, scope) {
2953
3008
  const isDefaultScope = (scope.targets ?? []).length === 0 && !scope.repo && !scope.base;
2954
3009
  if (analyses.length === 0 && isDefaultScope) {
2955
- const changes = scope.staged ? "staged changes" : "uncommitted changes";
2956
3010
  log2(
2957
- `comments: no ${changes} in JavaScript or TypeScript files. Pass --repo, --base <ref>, or files to widen the scope.`
3011
+ "comments: no uncommitted changes in JavaScript or TypeScript files. Pass --repo, --base <ref>, or files to widen the scope."
2958
3012
  );
2959
3013
  }
2960
3014
  }