Uh oh!
There was an error while loading. Please reload this page.
eslint: catch a statement swallowed by an unterminated block comment - #10429
Merged
os-zhuang merged 1 commit intoAug 20, 2026
Merged
Conversation
os-zhuang
marked this pull request as ready for review
August 20, 2026 18:46
os-zhuang
enabled auto-merge
August 20, 2026 18:46
This was referenced Aug 20, 2026
Merged
github-merge-queueBot
removed this pull request from the merge queue due to failed status checks
Aug 20, 2026
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
deleted the
claude/issue-9758-unterminated-block-comment-checker
branch
August 20, 2026 21:30
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 freeto 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.
Fixes#9758
A block comment that is never closed is not a syntax error. The next terminator in the file closes it — normally the docblock of the following declaration — so the file parses, every gate stays green, and a statement quietly stops existing while the header above it goes on describing it. #9640 was one instance:
export { maskComments };inscripts/pm/dispatch-gates.mjshad been comment text.#9758 is a disposition card: it recorded the class rather than building for it, and asked whether the base rate earns a checker. This PR answers yes, on cost, and adds one rule —
comment-swallow/no-code-inside-block-comment— toeslint.config.mjs, the file that already carries three inline plugins. Nothing else changes: no new file, no new script, no workflow step, no required context, no check family, no dispatch lead.Answering the card's question, measured
The base rate is now 0, not 1 in 4,595. Re-derived on
origin/mainat b9a5c46 over 4,679 files — with@typescript-eslint/parser's comment nodes rather than the repo masker — the class has zero instances. #9640 was the singleton and its PR repaired it.premise_still_validfor the card's own measurement: the count it took is no longer the count.A zero base rate is a real argument against a new gate family — a script, a workflow step, a required context, its own self-test and watch hints, and one more line of reader attention in every dispatch derivation. It is not an argument against this, because the marginal cost here is:
check:*familiesdispatch-gates.mjsstill discovers 117)pnpm lintwall time, whole treeverify-stand-in/no-asserted-driver-argumentis the in-file precedent for a guard that starts at a zero baseline: the state worth having is that the count stays 0, and holding a 0 is what a rule is for. This one likewise ships with no baseline and noignoresbeyond the build directories.The false-positive surface, which is the whole risk
Commented-out code is legitimate and common, so a predicate that flags every code-shaped line inside a block comment is unshippable. Two facts bound it, both measured over this tree's 21,007 multi-line block comments:
*prose marker. Not most — all. So exempting marker lines costs no recall on real prose, and it is what makes the predicate quiet: 540 interior lines are code-shaped (@exampleblocks:* import { createHonoApp } from …,* export default app;) and every one of them is a marker line. Without the exemption those 540 red; with it they are invisible. This is the criterion narrowing on evidence, and it is the whole difference between 540 and 0.let,class,type,importandexportare ordinary English words.let us assume…does not matchlet <ident> [:=];class hierarchies are…does not matchclass <ident> (extends|implements|{|<);import lists are…matches no import form. All three are pinned cases.Against those, plus the block-comment opener signature (comments do not nest, so a
/*heading a line inside a comment span is the structural signature of "never closed"), the sweep reports:The card's own looser criteria, for comparison, measured on the same corpus:
/*anywhere inside a block comment → 321 hits, all prose (* reads live \examples/**` for the nameless cases`). Statement-keyword-at-any-indent with no marker exemption → the 540 above. Both are the unshippable versions.Positive control — it fires on the real #9640, reconstructed in the tree
The defect was reintroduced by deleting the terminator of the docblock above
export { maskComments };inscripts/pm/dispatch-gates.mjs, confirmed on disk by anchored counts (original 3-line form0occurrences, mutated form1), then linted:Both signatures: line 528 is the swallowed statement, line 530 is the
/**of the docblock below whose terminator closed the phantom span. With that defect in the tree:Restored byte-identically afterwards —
git hash-objectmatchesgit rev-parse HEAD:<path>— and both legs re-run green.Ablation — the rule can go red, and so can its self-check
A rule with a zero baseline is green three ways: the tree is clean, the predicate was edited into matching nothing, or the config block stopped matching files.
pnpm lintreports the same nothing in all three. The three sibling guards in this file each pay a gate script to tell those apart; this one does not need one, because the predicate is a pure function of a single line, so its cases run where the config loads — on everypnpm lint, in CI and locally, for ~20 regex tests.Both arms were mutated and confirmed on disk by anchored before/after counts (target text
1 → 0, injected text0 → 1), then restored to a blob sha matchingHEAD:^export→^exportXX)"export { maskComments };"(expected a report)";pnpm lintexits 2COMMENT_SWALLOW_FILESto{ts,tsx,mts,cts}That second arm guards a real trap. The whole-tree block directly above this one is scoped
**/*.{ts,tsx,mts,cts}, and the only instance this class has ever had was a.mjsfile underscripts/. A block copied from the sibling would cover 4,326 package files and not the one kind of file the defect has actually occurred in.Limits, stated rather than implied
packages/+**+/*.tscarries a terminator and closes a phantom span mid-token — the parser already rejects the file loudly. That split is why the predicate is allowed to be narrow: the loud half belongs to the compiler.*marker" catches those and also measures 0 on this tree — but it is a style claim wearing a defect's clothes, it would reject an ordinary marker-less/* TODO: … */, and this repo lints with--no-inline-config, so there would be no per-site escape. The escape that does exist is the house style: prefix the line with*.check:pm-dispatch-gatesalready pins the one statement [finding] dispatch-gates'export { maskComments }is swallowed by an unterminated block comment — the docblock claims a re-export the module does not have #9640 repaired, and it does go red on the reconstruction above (2 of 421 cases). That covers one statement in one file; this covers 4,679.Gates
Union re-derived at final commit
0964f06f3and run on that tree:The last four are run because they
importfromeslint.config.mjsand this change adds a throw path to its module load. The sweeps and the control harness were run outsideos-verify-lockand are declared: pure source-text scanners that neither build nor readdist/.pnpm lintwas run inside it.dispatch-gates.mjsnames no check family foreslint.config.mjs, which is the same fact as the cost table above: this rule rides a gate that already exists.Clause ②: not reachable — the change touches no
packages/spec/src/**path and alters no contract accept/reject behaviour or public surface.Changeset: none.
eslint.config.mjspublishes nothing, so this carriesskip-changeset.Filed, not fixed here
#10427—scanSource(scripts/js-comment-mask.mjs) desyncs on a nested template literal inside${...}, and 16 files diverge from a real parser's comment ranges: 15 in the direction that module's own header calls the worse one (real comments read as live code — 9,405 bytes inview.zod.tsalone), 2 in the blinding direction (~85 lines of live code blanked out ofcheck-governed-prose.mjs). Found because #9758's sweep usesscanSourceas its instrument; its 13 apparent hits on currentmainare all phantoms from that desync, which is part of why this rule reads the parser's comment nodes instead. Out of scope here and untouched by this PR.Generated by Claude Code