feat(lint): a comment budget rule, and what calibrating it against our code showed - #1625
Open
dawsontoth wants to merge 2 commits into
Open
feat(lint): a comment budget rule, and what calibrating it against our code showed#1625dawsontoth wants to merge 2 commits into
dawsontoth wants to merge 2 commits into
Conversation
Nothing on npm does this. `eslint-plugin-comment-density` is an unfinished scaffold that enforces a *minimum* density, and `eslint-plugin-max-comments-per-function` (the one plugin with the right idea) requires `eslint/lib/util/ast-utils`, an internal path removed back in ESLint 6, so it cannot load at all. So: a local oxlint JS plugin, written to the ESLint v9 rule API so it also runs under ESLint. It budgets comment *sites* rather than comments or lines — a run of own-line comments is one site however long — because per-line budgeting prices a considered paragraph above the `// increment i` it should be displacing. Trailing comments never merge; directives and JSDoc are exempt; each site is charged to its innermost block, so extraction actually clears a warning. Budgets are calibrated against this repo rather than guessed: at 12/file and 4/block it flags 30 files and 52 blocks (2.3% of src). Severity is `warn`, and oxlint exits 0 on warnings, so neither CI nor the pre-commit hook starts failing. Worth knowing before tightening it: the densest scopes here are commented *well* (Radix ref-loop hazards in useResizableDialog, the describe_all/describe_table race in DatabaseTableView). They trip the budget because the functions are large, which makes this a scope-size signal more than a comment-quality one — recorded in AGENTS.md so nobody answers a warning by deleting the load-bearing prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a custom oxlint/ESLint plugin, comment-budget, which enforces a configurable limit on the number of comment sites per file and per block. It includes documentation in AGENTS.md, configuration in .oxlintrc.json, and a comprehensive test suite. The review feedback highlights three key improvement opportunities: making the line-boundary check in isOwnLine robust against carriage returns (\r), optimizing the O(N^2) array spreading inside the block-grouping loop to O(N), and appending .cmd to the oxlint binary path on Windows to ensure cross-platform test execution.
Coverage Report
File CoverageNo changed files found. |
Three review comments from gemini-code-assist, all real: - `isOwnLine` only stopped its leftward indentation scan at LF, so with bare-CR line endings it ran on into the previous line, found that line's `;`, and classified an own-line comment as a trailing aside — which also stopped it merging with its neighbours. Now stops at CR too, which drops the `\r` special-case from the loop body. Covered by a new test that fails with a count of 2 against the old code and 1 against the new. - Accumulating each block's sites with a spread copied the whole array per site, O(N^2) in a block's comment count. Push into the existing array instead. Behaviour is unchanged: the repo still reports exactly 82 warnings. - The test harness spawned `node_modules/.bin/oxlint`, which is POSIX-only. The suggested `.cmd` suffix would not have fixed Windows on its own — Node refuses to spawn `.cmd` without `shell: true` (CVE-2024-27980 hardening). Instead run oxlint's declared `bin`, which is a plain Node script, through `process.execPath`, resolved via `oxlint/package.json` since pnpm puts the real package in the store outside this worktree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dawsontoth
marked this pull request as ready for review
August 14, 2026 17:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a lint rule that caps comment volume, to push toward code that explains itself rather than prose
that drifts from it. Discussed as a draft; the team is on board, so this is now proposed for merge.
Scope: this repo only. Not extracting a standalone package for now — the rule file is written to
the ESLint v9 API and would need no changes to become one later, but nothing here depends on that.
Nothing on npm does this
Worth recording, because "surely this exists" was the first assumption:
eslint-plugin-comment-densityCode comment coverage should be greater than…), the exact opposite.eslint-plugin-max-comments-per-functionmax-lines-per-function.requireseslint/lib/util/ast-utils, an internal path removed in ESLint 6 — cannot load.eslint-plugin-no-commentseslint-plugin-complexitycomplexity/commentrequires a comment above a complexity threshold. Inverse of this.eslint-plugin-comment-length,eslint-plugin-eslint-comments,@stylistic/*What this adds
tools/oxlint-plugins/comment-budget.js— the rule, written to the ESLint v9 rule API so the samefile runs under oxlint's
jsPluginsand under ESLint unchanged. Ships nothing to the bundle;pnpm lintover the whole repo still finishes in 0.24s with it loaded..oxlintrc.json—warnat 12 comments per file and 4 per block.tools/oxlint-plugins/comment-budget.test.js— 13 cases, driven through the real oxlint binaryrather than by calling
create()directly, so they catch oxlint API drift rather than re-testingour own assumptions.
AGENTS.md— how to read a warning, and the escape hatch.It counts comment sites, not comments or lines
A run of own-line comments on consecutive lines is one site however long it runs. A five-line
paragraph explaining a race costs exactly what a lone
// increment icosts.That is the central design decision. Budgeting per line would price the considered explanation
above the throwaway, inverting the incentive we want. Trailing comments never merge with
neighbours (
a = 1; // whythenb = 2; // whyis two asides, not a paragraph). Directives(
eslint-*,oxlint-*,@ts-*,dprint-*,c8 ignore,#region) and JSDoc/TSDoc are exempt — abudget that eats the escape hatches just teaches people to disable the budget. A
/*** banner ***/is not exempt.
Sites are charged to their innermost enclosing block, so splitting a long function genuinely
clears a warning instead of relocating it.
What calibration showed
Budgets are measured, not chosen by taste. Across
src(1,239 files, 596 with comments): median 3sites per file, p90 = 9, p95 = 12, max 40.
maxPerFilemaxPerBlockAt 12/4 that is 82 warnings repo-wide (30 file, 52 block).
warnseverity, and oxlint exits 0 onwarnings, so neither CI nor the pre-commit hook starts failing.
Read the flagged set carefully, because it complicates the premise. The densest scopes in the
repo are the best-commented code we have:
useResizableDialog.ts(31 sites) — Radix invoking a ref more than once per render and loopingforever; why size is persisted but position deliberately is not.
DatabaseTableView.tsx(16 in one block) — whydescribe_allanddescribe_tablerace and whichcopy wins; a
#1199reference for the primary-key-changed-after-rows-existed case.None of that survives being renamed into the code. It is exactly the load-bearing "why" no identifier
can carry. Those files trip the budget because the functions are large, not because the comments
drifted.
So, honestly: this is closer to a scope-size signal than a comment-quality signal. Its advice
("extract a named function") is right for the code it flags, but for the reason
max-lines-per-functionwould give. The rule cannot distinguish a drifting comment from anirreplaceable one — it only measures volume.
That is why it is a
warnwith a generous budget, and whyAGENTS.mdstates plainly that answeringa warning by deleting prose leaves us worse off than before the rule existed. The failure mode to
guard against is someone trimming the
#1199note to get under a number.Still open, and fine to settle after merge
error? Our lint baseline was previously spotless, so 82 permanent warnings doerode that signal. Suggest revisiting once the flagged set is worked down.
replaces, and tests hold the extreme tail (220 of the 596 commented files; median 3, max 40).
Leaving it single-budget for now — a second knob is easier to add on evidence than to remove.
Verification
Full suite 292 files / 2,268 tests green,
tsc -bclean,dprint checkclean,pnpm lintexits 0.The 13 rule tests cover budget arithmetic, paragraph grouping, the blank-line break, trailing
comments, CR line endings, directive and JSDoc exemption, banner comments, innermost-block
attribution, switch-case scoping, one-report-per-scope, and the inline disable.
Two things checked while preparing this, both fine:
jsPluginsexists in the oxlint config schemaback to our declared
^1.32.0floor (lockfile pins 1.75.0), and a broken plugin path fails the wholelint run with exit 1 rather than silently dropping the rule.
Known gap:
tools/sits outside both tsconfig projects and the plugin is plain JS, sotsc -bdoesnot check it. The tests are its only safety net.
🤖 Generated with Claude Code