Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions eslint.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -640,6 +640,252 @@ const verifyStandInPlugin = {
},
};

// ---------------------------------------------------------------------------
// issue #9758 — a statement swallowed by an unterminated block comment.
// ---------------------------------------------------------------------------
//
// A block comment that is never closed is NOT a syntax error. The next comment
// terminator in the file closes it — normally the docblock of the following
// declaration — so the file parses, every gate stays green, and the only
// symptom is a statement that quietly stopped existing. #9640 is one instance:
// `export { maskComments };` in `scripts/pm/dispatch-gates.mjs` had been comment
// TEXT, while that module's own header went on claiming the re-export.
//
// The class is invisible to review and trivial to parse: the swallowed line
// looks like code (textually it IS code) and the terminator looks like it
// belongs to the docblock below. ESLint already visits every file with a real
// parser and hands a rule the comment nodes for free, so the checker is the
// predicate below and nothing else.
//
// ## Why a rule here and not a `check:*` family — #9758's question, measured
//
// #9758 recorded the class at 1 instance in 4,595 files and asked whether that
// base rate earns a checker at all. Re-derived on this tree — 4,679 files, a
// real parser rather than the repo masker — the count is 0: #9640 was the
// singleton and its PR repaired it.
//
// 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, all to hold a
// class nobody trips. It is not an argument against THIS, which is one rule in
// the file that already carries three inline plugins, under a `pnpm lint` that
// already parses all 4,679 files. Zero new CI steps, zero new required
// contexts, zero new check families, zero new dispatch leads.
// `verify-stand-in/no-asserted-driver-argument` below is the 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.
//
// ## The false-positive surface, which is the whole risk — measured
//
// 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:
//
// • all 240,529 of their interior source lines carry the `*` 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 (`@example` blocks — `* import { createHonoApp } from …`,
// `* export default app;`) and every one of them is a marker line. Without
// the exemption those 540 are false positives; with it they are invisible.
// • the predicate matches statement SHAPES, not statement keywords. `let`,
// `class`, `type`, `import` and `export` are ordinary English words, and a
// keyword test flags prose that opens with one. `let us assume…` does not
// match `let <ident> [:=]`; `class hierarchies are…` does not match
// `class <ident> (extends|implements|{|<)`; `import lists are…` does not
// match an import form. Those three are in the pinned cases below.
//
// Against those two, plus the block-comment OPENER signature (comments do not
// nest, so a `/*` at the head of a line inside a comment span is the structural
// signature of "never closed"), a sweep over 261,536 candidate lines in 4,679
// files reports 0. So the rule ships with no baseline and no ignores beyond the
// build directories — there is nothing to grandfather.
//
// ## What it cannot see, stated rather than implied
//
// ESLint never sees a file that does not parse, so this rule's domain is
// exactly the SILENT half of the class. When the swallowed span happens to
// leave behind text that is not valid JavaScript — a glob literal such as
// `packages/` + `**` + `/*.ts` carries a comment terminator and closes a
// phantom span mid-token — the parser rejects the file loudly and no checker is
// needed. That split is also why the predicate is allowed to be narrow: the
// loud half is already covered, by the compiler.
//
// The other half it cannot see is a swallowed line that is neither
// statement-shaped nor a comment opener — a bare call, a JSX fragment, an
// object continuation. Widening to "any line inside a block comment without a
// `*` marker" would catch those and measures 0 on this tree too, but it is a
// STYLE claim wearing a defect's clothes: it would reject a perfectly ordinary
// marker-less `/* TODO: … */` block, and this repo lints with
// `--no-inline-config`, so there would be no per-site escape from it. The
// escape that does exist is the house style itself — prefix the line with `*`.

/** The rule's own id, exported for the same reason as the two ids above. */
export const COMMENT_SWALLOW_RULE_ID = 'comment-swallow/no-code-inside-block-comment';

export const COMMENT_SWALLOW_MESSAGE =
'This line is inside a block comment, and it is shaped like code. That is the signature of a ' +
'block comment that was never closed: an unterminated opener is not a syntax error — the next ' +
'comment terminator in the file closes it, usually 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: `export { maskComments };` was comment text for ' +
'as long as nobody looked). Add the missing terminator to the comment above. If the line really ' +
'is prose or a deliberately commented-out example, prefix it with the `*` marker every one of ' +
"this tree's 240,529 block-comment lines already carries — that is the exemption, and it is the " +
'house style rather than an opt-out. See issues #9640 and #9758.';

/**
* Statement SHAPES, not statement keywords — see the false-positive section
* above. Each pattern is anchored on the punctuation that makes the line code
* rather than a sentence that happens to open with a reserved word.
*/
export const COMMENT_SWALLOW_PATTERNS = [
// `export { a };` / `export * from './x';` / `export default f;`
/^export\s*(?:\{|\*|default\b)/,
// the four import forms, and none of `import lists are checked elsewhere,`
/^import\s*(?:\{|\*|type\s|['"]|[A-Za-z_$][\w$]*\s*(?:,|from\b))/,
// a binding: `const x =`, `let { a } =`, `export var n:` — never `let us …`
/^(?:export\s+)?(?:const|let|var)\s+(?:[{[]|[A-Za-z_$][\w$]*\s*[:=])/,
// `function f(`, `async function* g<`, `export default function h(`
/^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*[A-Za-z_$][\w$]*\s*[(<]/,
// `class C {` / `export abstract class C extends D` — never `class hierarchies …`
/^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+[A-Za-z_$][\w$]*\s*(?:extends\b|implements\b|\{|<)/,
// the TypeScript declaration shapes
/^(?:export\s+)?(?:declare\s+)?interface\s+[A-Za-z_$][\w$]*\s*(?:extends\b|\{|<)/,
/^(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+[A-Za-z_$][\w$]*\s*\{/,
/^(?:export\s+)?type\s+[A-Za-z_$][\w$]*\s*[=<]/,
// CommonJS, which `scripts/` still writes
/^(?:module\.exports|exports\.[A-Za-z_$][\w$]*)\s*=/,
// a block-comment OPENER. Comments do not nest, so one that begins a line
// inside a comment span is the structural signature of "never closed" — and
// in the #9640 shape it is the docblock of the declaration below, which is
// exactly the thing that made the defect unreadable.
/^\/\*/,
];

/**
* True when this source line, taken on its own, reads as code rather than
* prose. The caller supplies the fact that the line lies inside a block-comment
* span; this half decides nothing about spans and everything about shape.
*/
export function looksLikeSwallowedCode(sourceLine) {
const text = sourceLine.trim();
if (!text || text.startsWith('*')) return false;
return COMMENT_SWALLOW_PATTERNS.some((pattern) => pattern.test(text));
}

/**
* The extension list is load-bearing and DELIBERATELY wider than the
* `verify-stand-in` block's: it carries `js`/`jsx`/`mjs`/`cjs`. The only
* instance this class has ever had on this tree was `scripts/pm/
* dispatch-gates.mjs`, a `.mjs` file, and a block copied from the
* `{ts,tsx,mts,cts}` sibling below would have covered every file except the
* one kind that has actually carried the defect. `assertCommentSwallow` pins
* it, so the narrowing cannot happen quietly.
*/
export const COMMENT_SWALLOW_FILES = ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'];

/**
* The predicate's contract, pinned as cases rather than trusted.
*
* 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 — and `pnpm lint` reports the same nothing in all three. That
* is the dead-pin shape the three sibling guards in this file each pay a gate
* script to avoid. This one does not need a gate script: the predicate is a
* pure function of one line, so its cases can run where the config loads, on
* every `pnpm lint` in CI and locally, at a cost of ~20 regex tests. The span
* half needs no pinning — that is the parser's answer, not ours.
*/
export const COMMENT_SWALLOW_CASES = [
// FIRES — the class.
['export { maskComments };', true], // the #9640 statement itself
['const RE = /a*' + '/;', true],
[' const limit = 10;', true], // indented, inside a function body
['module.exports = { a: 1 };', true],
['export type Id = string;', true],
['export function selfTest() {', true],
['import { readFileSync } from "node:fs";', true],
['/** the docblock whose terminator closed the phantom span */', true],
// SILENT — prose, and marked-up example code.
[' * import { createHonoApp } from "@objectstack/hono";', false],
[' * export default app;', false],
['let us assume the value is null, and', false],
['class hierarchies are not the point here;', false],
['import lists are checked elsewhere,', false],
['export the module however you like;', false],
['type checking happens later', false],
[' */', false],
['', false],
];

function assertCommentSwallow() {
const wrong = COMMENT_SWALLOW_CASES.filter(([line, expected]) => looksLikeSwallowedCode(line) !== expected);
if (wrong.length > 0) {
throw new Error(
`${COMMENT_SWALLOW_RULE_ID}: the detector no longer matches its pinned cases — ` +
`${wrong.length} of ${COMMENT_SWALLOW_CASES.length} disagree, starting with ` +
`${JSON.stringify(wrong[0][0])} (expected ${wrong[0][1] ? 'a report' : 'silence'}). ` +
'A rule with a zero baseline cannot be trusted to be quiet for the right reason, so this ' +
'runs where the config loads. Fix COMMENT_SWALLOW_PATTERNS, or amend the case if the ' +
'contract really changed — never delete it to get lint green. See issue #9758.'
);
}
if (!COMMENT_SWALLOW_FILES.some((glob) => /\bmjs\b/.test(glob) && /\bjs\b/.test(glob))) {
throw new Error(
`${COMMENT_SWALLOW_RULE_ID}: COMMENT_SWALLOW_FILES stopped covering plain JavaScript. The ` +
'one instance this class has ever had on this tree was a `.mjs` file under `scripts/`, so a ' +
'TypeScript-only scope is a guard that cannot see the only place the defect has occurred. ' +
'See issue #9758.'
);
}
}

assertCommentSwallow();

const commentSwallowPlugin = {
rules: {
'no-code-inside-block-comment': {
meta: {
type: 'problem',
docs: {
description:
'Ban a code-shaped line inside a block-comment span — the signature of a block ' +
'comment that was never closed.',
},
schema: [],
messages: { swallowed: COMMENT_SWALLOW_MESSAGE },
},
create(context) {
return {
Program() {
const { sourceCode } = context;
const lines = sourceCode.lines;
for (const comment of sourceCode.getAllComments()) {
if (comment.type !== 'Block') continue;
const { start, end } = comment.loc;
if (end.line === start.line) continue;
// From the line AFTER the opener through the terminator's own
// line. The opener's line is excluded because whatever precedes a
// `/*` on it is live code; the terminator's line is INCLUDED
// because a span can close mid-line, on a line that is otherwise
// a statement — a regex or a glob literal a few lines down
// carries a terminator and ends the phantom span right there.
for (let line = start.line + 1; line <= end.line; line++) {
const text = lines[line - 1];
if (text === undefined || !looksLikeSwallowedCode(text)) continue;
context.report({
loc: { start: { line, column: 0 }, end: { line, column: text.length } },
messageId: 'swallowed',
});
}
}
},
};
},
},
},
};

export default [
{
files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'],
Expand DownExpand Up@@ -897,4 +1143,30 @@ export default [
plugins: { 'verify-stand-in': verifyStandInPlugin },
rules: { 'verify-stand-in/no-asserted-driver-argument': 'error' },
},
// issue #9758 — a statement swallowed by an unterminated block comment. The
// rationale, the re-derived base rate and the false-positive bound are all on
// `COMMENT_SWALLOW_MESSAGE` above.
//
// ⚠️ `COMMENT_SWALLOW_FILES` is wider than the `verify-stand-in` block's
// pattern directly above: it carries `js`/`jsx`/`mjs`/`cjs`, because the one
// instance this class has ever had was a `.mjs` file under `scripts/`.
// Copying the sibling's `{ts,tsx,mts,cts}` scope here would produce a guard
// that covers 4,326 package files and not the one kind of file the defect has
// actually occurred in. `assertCommentSwallow` refuses that narrowing.
//
// No baseline and no `ignores` beyond the build directories, for the same
// reason `verify-stand-in` has none: the tree measures 0 today (261,536
// candidate lines, 4,679 files), so there is nothing to grandfather and every
// future report is a new defect. Adding an entry later would mean the state
// stopped being locked.
{
files: COMMENT_SWALLOW_FILES,
ignores: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/.turbo/**'],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
},
plugins: { 'comment-swallow': commentSwallowPlugin },
rules: { 'comment-swallow/no-code-inside-block-comment': 'error' },
},
];
Loading