test(gates): the register test parses ruff's TOML and holds pytest.ini line by line

Round 2 of the E32 review found the key whitelists blind: a line regex saw neither a
[lint.per-file-ignores] table nor a quoted key, and pytest.ini's continuation lines and
key: value form slipped past. Ruff's config is now parsed (smol-toml) and held to its
key paths; pytest.ini to its whole lines; the CI ruff steps to --config backend/ruff.toml,
which is what makes that file the whole configuration.

ESLint: every glob of a block must reach a sample (reactPlugin.ts and devOnlyGuard.ts
rode along unseen), a block's own severity is held to src, a no-restricted-* list is held
to the src block's declaration rather than Feed.tsx's effective config, named global
ignores count, an ignored sample is named, and linterOptions are checked. tsconfig.node
must keep allowJs + checkJs while it lists a .js file; knip's positive glob is pinned.
This commit is contained in:
2026-10-04 04:16:52 +02:00
parent 64e8f1da3d
commit 3f6a9efd57
4 changed files with 141 additions and 71 deletions
+6 -4
View File
@@ -15,10 +15,12 @@ not an oversight; a narrowing missing from here is a test failure.
How the test matches: each narrowing must appear in a table row below as its `code-quoted` id, in a How the test matches: each narrowing must appear in a table row below as its `code-quoted` id, in a
row whose **Scope** cell names the scope exactly as the test reports it. For ESLint it reads each row whose **Scope** cell names the scope exactly as the test reports it. For ESLint it reads each
scope's effective config through a sample file AND every config block on its own: a block that scope's effective config through its sample files AND every config block on its own: a block glob
targets files no sample reaches fails as an `unsampled block` (add a sample), a rule a block or src that no sample matches fails as an `unsampled block` (add a sample), a rule a block or src sets
sets weaker than src counts, so does a `no-restricted-*` list that leaves out an entry src has, and weaker than src counts, so does a `no-restricted-*` list that leaves out an entry the src block
so does an option that lets something through (`allow…`, `ignore…`, `except…`) (E32 review). declares, so does an option that lets something through (`allow…`, `ignore…`, `except…`), and so
does any `linterOptions` but an erroring `reportUnusedDisableDirectives` (E32 review). Ruff's TOML
is parsed and held to a whitelist of key paths, pytest.ini to a whitelist of whole lines.
## ESLint (frontend) ## ESLint (frontend)
+1
View File
@@ -46,6 +46,7 @@
"knip": "6.27.0", "knip": "6.27.0",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"prettier": "3.6.2", "prettier": "3.6.2",
"smol-toml": "1.9.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "8.71.0", "typescript-eslint": "8.71.0",
+1
View File
@@ -62,6 +62,7 @@
"knip": "6.27.0", "knip": "6.27.0",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"prettier": "3.6.2", "prettier": "3.6.2",
"smol-toml": "1.9.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "8.71.0", "typescript-eslint": "8.71.0",
+133 -67
View File
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import { ESLint, type Linter } from "eslint"; import { ESLint, type Linter } from "eslint";
import prettierConfig from "eslint-config-prettier"; import prettierConfig from "eslint-config-prettier";
import { parse as parseToml } from "smol-toml";
import ts from "typescript"; import ts from "typescript";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
@@ -68,24 +69,30 @@ const TSC_OWNED = new Set(
); );
const family = (rule: string) => (rule.includes("/") ? `${rule.split("/")[0]}/*` : rule); const family = (rule: string) => (rule.includes("/") ? `${rule.split("/")[0]}/*` : rule);
// Each scope is checked through a sample file's EFFECTIVE config (what a rule family not applied there // Each scope is checked through its sample files' EFFECTIVE config (what a rule family not applied
// means), and every config block that targets files is checked on its own (below) — so a block no // there means), and every config block that targets files is checked on its own (below): each of its
// sample reaches fails rather than passing unseen (E32 review: a narrowing scoped to any other file // globs must reach a sample, so a glob no sample reaches fails rather than passing unseen (E32 review:
// changed none of the sampled configs and needed no row). // a narrowing scoped to any other file changed none of the sampled configs and needed no row; round
const SCOPES: Record<string, string> = { // 2: a block was sampled through ONE of its globs, and `reactPlugin.ts` rode along unseen).
src: "src/components/Feed.tsx", const SCOPES: Record<string, string[]> = {
"src/lib": "src/lib/format.ts", src: ["src/components/Feed.tsx"],
"lib/http.ts": "src/lib/http.ts", "src/lib": ["src/lib/format.ts"],
"lib/api/core.ts": "src/lib/api/core.ts", "lib/http.ts": ["src/lib/http.ts"],
"lib/messagesSocket.ts": "src/lib/messagesSocket.ts", "lib/api/core.ts": ["src/lib/api/core.ts"],
"src tests": "src/components/Feed.ytsort.dom.test.tsx", "lib/messagesSocket.ts": ["src/lib/messagesSocket.ts"],
"FocusSafeButton.tsx": "src/components/ui/FocusSafeButton.tsx", "src tests": ["src/components/Feed.ytsort.dom.test.tsx"],
"AnchoredPopover.tsx": "src/components/ui/AnchoredPopover.tsx", "FocusSafeButton.tsx": ["src/components/ui/FocusSafeButton.tsx"],
"design-lab": "src/design-lab/batchA.tsx", "AnchoredPopover.tsx": ["src/components/ui/AnchoredPopover.tsx"],
e2e: "e2e/nav.spec.ts", "design-lab": ["src/design-lab/batchA.tsx"],
"ts configs": "vite.config.ts", e2e: ["e2e/nav.spec.ts"],
"js configs": "eslint.config.js", "ts configs": ["vite.config.ts", "reactPlugin.ts", "devOnlyGuard.ts"],
"js configs": ["eslint.config.js"],
}; };
const SAMPLES = Object.entries(SCOPES).flatMap(([scope, files]) =>
files.map((f): [scope: string, file: string] => [scope, f]),
);
// The block every src file gets: the reference a scope's re-declared `no-restricted-*` list is held to.
const SRC_GLOB = "src/**/*.{ts,tsx}";
/** A flat-config `files` glob as a regex — the shapes this config uses: `**`, `*`, `{a,b}`. */ /** A flat-config `files` glob as a regex — the shapes this config uses: `**`, `*`, `{a,b}`. */
function globRegex(glob: string): RegExp { function globRegex(glob: string): RegExp {
@@ -138,55 +145,88 @@ function restrictedEntries(entry: Linter.RuleEntry | undefined): Set<string> {
async function eslintNarrowings(): Promise<[string, string][]> { async function eslintNarrowings(): Promise<[string, string][]> {
const eslint = new ESLint({ cwd: FRONTEND }); const eslint = new ESLint({ cwd: FRONTEND });
const rules: Record<string, Rules> = {}; const found: [string, string][] = [];
for (const [scope, file] of Object.entries(SCOPES)) const rules: [scope: string, rules: Rules][] = [];
rules[scope] = ((await eslint.calculateConfigForFile(file)) as Linter.Config).rules ?? {}; for (const [scope, file] of SAMPLES) {
const src = rules.src!; const cfg = (await eslint.calculateConfigForFile(file)) as Linter.Config | undefined;
if (!cfg) {
found.push([scope, `${file} is ignored`]); // ignored files get no config at all
continue;
}
rules.push([scope, cfg.rules ?? {}]);
// an unused `eslint-disable` is an error on every file, or a stale suppression stays armed
if (severity(cfg.linterOptions?.reportUnusedDisableDirectives as Linter.RuleEntry) !== 2)
found.push([scope, "reportUnusedDisableDirectives"]);
}
const src = rules.find(([scope]) => scope === "src")![1];
const ignorable = (rule: string, all: Rules) => const ignorable = (rule: string, all: Rules) =>
PRETTIER_OFF.has(rule) || PRETTIER_OFF.has(rule) ||
TSC_OWNED.has(rule) || TSC_OWNED.has(rule) ||
// a core rule off because its typescript-eslint twin replaces it is reported as the twin // a core rule off because its typescript-eslint twin replaces it is reported as the twin
(!rule.includes("/") && `@typescript-eslint/${rule}` in all); (!rule.includes("/") && `@typescript-eslint/${rule}` in all);
const found: [string, string][] = [];
// src: everything at off or warn, and every rule whose options let something through // src: everything at off or warn, and every rule whose options let something through
for (const [rule, v] of Object.entries(src)) { for (const [rule, v] of Object.entries(src)) {
if (ignorable(rule, src)) continue; if (ignorable(rule, src)) continue;
if (severity(v) < 2 || (Array.isArray(v) && relaxes(v.slice(1)))) found.push(["src", rule]); if (severity(v) < 2 || (Array.isArray(v) && relaxes(v.slice(1)))) found.push(["src", rule]);
} }
// every other scope: what is WEAKER there than in src (a rule family not applied at all is one // every other sample: what is WEAKER there than in src (a rule family not applied at all is one
// id). A src rule at warn counts too: switched off for a scope, it is narrowed there. // id). A src rule at warn counts too: switched off for a scope, it is narrowed there.
for (const scope of Object.keys(SCOPES).filter((s) => s !== "src")) { for (const [scope, theirs] of rules.filter(([s]) => s !== "src")) {
const ids = new Set<string>(); const ids = new Set<string>();
for (const [rule, v] of Object.entries(src)) { for (const [rule, v] of Object.entries(src)) {
if (ignorable(rule, src) || severity(v) === 0) continue; if (ignorable(rule, src) || severity(v) === 0) continue;
const there = severity(rules[scope]![rule]); const there = severity(theirs[rule]);
if (there < severity(v)) ids.add(there < 0 ? family(rule) : rule); if (there < severity(v)) ids.add(there < 0 ? family(rule) : rule);
} }
for (const id of ids) found.push([scope, id]); for (const id of ids) found.push([scope, id]);
} }
// every config block: a global `ignores` is a narrowing everywhere; one that targets files must // every config block: a global `ignores` is a narrowing everywhere; one that targets files must
// reach a sample, and what it re-declares is held to src // reach a sample through EACH glob, and what it declares is held to src on its own — a later block
for (const block of await blocks()) { // that switches a rule back on for the sample would otherwise hide where the first one is off
if (!block.files) { const all = await blocks();
if (Object.keys(block).join() === "ignores") const isSrcBlock = (b: Block) => b.files?.flat().join() === SRC_GLOB;
for (const ignored of block.ignores ?? []) found.push(["everywhere", ignored]); const reference = new Map<string, Set<string>>(); // the FIRST src-block declaration of each list
for (const block of all.filter(isSrcBlock))
for (const [rule, v] of Object.entries(block.rules ?? {}))
if (rule.includes("no-restricted-") && !reference.has(rule))
reference.set(rule, restrictedEntries(v));
let globalIgnores = 0;
for (const block of all) {
// ESLint reads a block as GLOBAL ignores when `ignores` is its only key besides `name`
if (
!block.files &&
Object.keys(block)
.filter((k) => k !== "name")
.join() === "ignores"
) {
globalIgnores++;
for (const ignored of block.ignores ?? []) found.push(["everywhere", ignored]);
continue; continue;
} }
const globs = block.files.flat().map(globRegex); for (const [key, v] of Object.entries(block.linterOptions ?? {}))
const scope = Object.keys(SCOPES).find((s) => globs.some((g) => g.test(SCOPES[s]!))); if (key !== "reportUnusedDisableDirectives" || severity(v as Linter.RuleEntry) !== 2)
if (!scope) { found.push(["linterOptions", key]);
found.push(["unsampled block", block.files.flat().join(", ")]); if (!block.files) continue;
continue; let scope: string | undefined;
for (const glob of block.files.flat()) {
const hit = SAMPLES.find(([, file]) => globRegex(glob).test(file));
if (!hit) found.push(["unsampled block", glob]);
scope ??= hit?.[0];
} }
if (!scope) continue;
if (block.ignores?.length) found.push([scope, `ignores: ${block.ignores.join(", ")}`]); if (block.ignores?.length) found.push([scope, `ignores: ${block.ignores.join(", ")}`]);
for (const [rule, v] of Object.entries(block.rules ?? {})) { for (const [rule, v] of Object.entries(block.rules ?? {})) {
if (ignorable(rule, src)) continue; if (ignorable(rule, src)) continue;
if (rule.includes("no-restricted-")) { if (rule.includes("no-restricted-")) {
const ref = reference.get(rule);
const own = restrictedEntries(v); const own = restrictedEntries(v);
if ([...restrictedEntries(src[rule])].some((e) => !own.has(e))) found.push([scope, rule]); if (ref && [...ref].some((e) => !own.has(e))) found.push([scope, rule]);
} else if (Array.isArray(v) && relaxes(v.slice(1))) found.push([scope, rule]); } else if (severity(v) < severity(src[rule]) || (Array.isArray(v) && relaxes(v.slice(1))))
found.push([scope, rule]);
} }
} }
// the detection above must still see the block every config has, or `everywhere` is never checked
if (globalIgnores === 0) found.push(["everywhere", "(no global ignores block found)"]);
return found; return found;
} }
@@ -262,29 +302,39 @@ describe("the gate register (docs/gates.md)", () => {
.filter((f) => !checked.has(f)) .filter((f) => !checked.has(f))
.map((f): [string, string] => ["config files", `${f} — not type-checked`]); .map((f): [string, string] => ["config files", `${f} — not type-checked`]);
expect(missingFrom(found)).toEqual([]); expect(missingFrom(found)).toEqual([]);
// a `.js` in `include` is dropped without a word unless allowJs, and read unchecked without
// checkJs (E32 review round 2)
const opts = jsonc("frontend/tsconfig.node.json").compilerOptions as Record<string, unknown>;
if ([...checked].some((f) => /[.][mc]?js$/.test(f)))
expect([opts.allowJs, opts.checkJs]).toEqual([true, true]);
}); });
// --- Ruff ----------------------------------------------------------------------------------------- // --- Ruff -----------------------------------------------------------------------------------------
// Ruff, pyright, pytest and knip are read as KEY whitelists: a key this test does not know (an // Ruff, pyright, pytest and knip are read as whitelists: a key this test does not know (an
// `extend-exclude`, a `lint.extend-ignore`, a narrowing `select`, a pyright `exclude`, a pytest // `extend-exclude`, a `lint.extend-ignore`, a narrowing `select`, a pyright `exclude`, a pytest
// `-m "not slow"`, a knip `rules`) fails until someone decides whether it narrows (E32 review — // `-m "not slow"`, a knip `rules`) fails until someone decides whether it narrows (E32 review —
// only `ignore` and the immutable calls used to be read). // only `ignore` and the immutable calls used to be read). The TOML is PARSED, not scanned line by
const tomlKeys = (toml: string) => // line: a `[lint.per-file-ignores]` table or a quoted key is a key path like any other (E32 review
toml // round 2 — a line regex saw neither).
.split("\n") const keyPaths = (o: Record<string, unknown>, prefix = ""): string[] =>
.map((l) => /^\s*([A-Za-z0-9_.-]+)\s*=/.exec(l)?.[1]) Object.entries(o).flatMap(([k, v]) =>
.filter((k): k is string => k !== undefined); v !== null && typeof v === "object" && !Array.isArray(v)
it("lists every ruff ignore and immutable call, and knows every key", () => { ? keyPaths(v as Record<string, unknown>, `${prefix}${k}.`)
const toml = read("backend/ruff.toml"); : [`${prefix}${k}`],
const list = (key: string) => {
const m = new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m").exec(toml);
return m ? [...m[1]!.matchAll(/"([^"]+)"/g)].map((x) => x[1]!) : [];
};
expect(tomlKeys(toml).sort()).toEqual(
["extend-immutable-calls", "extend-select", "line-length", "target-version"].sort(),
); );
it("lists every ruff ignore and immutable call, and knows every key", () => {
const cfg = parseToml(read("backend/ruff.toml"));
expect(keyPaths(cfg).sort()).toEqual(
[
"lint.extend-select",
"lint.flake8-bugbear.extend-immutable-calls",
"line-length",
"target-version",
].sort(),
);
const lint = cfg.lint as Record<string, unknown>;
// the selection on top of ruff's defaults (E, F) — dropping one narrows the lane // the selection on top of ruff's defaults (E, F) — dropping one narrows the lane
expect(list("extend-select")).toEqual([ expect(lint["extend-select"]).toEqual([
"LOG", "LOG",
"G", "G",
"T20", "T20",
@@ -297,12 +347,24 @@ describe("the gate register (docs/gates.md)", () => {
"PLW1510", "PLW1510",
"B", "B",
]); ]);
const found: [string, string][] = [ const immutable = (lint["flake8-bugbear"] as Record<string, string[]>)[
...list("ignore").map((code): [string, string] => ["backend + scripts", code]), "extend-immutable-calls"
...list("extend-immutable-calls").map((c): [string, string] => ["backend + scripts", c]), ]!;
]; const found = immutable.map((c): [string, string] => ["backend + scripts", c]);
expect(missingFrom(found)).toEqual([]); expect(missingFrom(found)).toEqual([]);
}); });
// Every ruff lane passes `--config backend/ruff.toml`, which makes ruff ignore any other config
// file it would discover (measured: a nested `ruff.toml` selecting the `D` rules changed nothing).
// So that flag is what makes this file the whole configuration, and it is held here.
it("runs every CI ruff step on backend/ruff.toml", () => {
const steps = read(".gitea/workflows/check.yml")
.split("\n")
.filter((l) => /python -m ruff /.test(l));
expect(steps.length).toBe(2);
expect(steps.filter((l) => !l.includes("--config backend/ruff.toml backend scripts"))).toEqual(
[],
);
});
// --- basedpyright --------------------------------------------------------------------------------- // --- basedpyright ---------------------------------------------------------------------------------
const PYRIGHT_KEYS = [ const PYRIGHT_KEYS = [
@@ -330,18 +392,20 @@ describe("the gate register (docs/gates.md)", () => {
}); });
// --- pytest --------------------------------------------------------------------------------------- // --- pytest ---------------------------------------------------------------------------------------
// Held line by line, not key by key: pytest's ini reader also takes `key: value` and indented
// continuation lines (`addopts = -q` + ` -m "not slow"`), and a key regex saw neither (E32 review
// round 2). Every line that is not blank or a comment must be one of these, and all must be there.
it("keeps pytest strict: the whole tests/ tree, no selection, no warning filters, --strict-markers", () => { it("keeps pytest strict: the whole tests/ tree, no selection, no warning filters, --strict-markers", () => {
const ini = read("backend/pytest.ini"); const lines = read("backend/pytest.ini")
const keys = ini
.split("\n") .split("\n")
.filter((l) => !l.trim().startsWith("#")) .map((l) => l.trimEnd())
.map((l) => /^([a-z_]+)\s*=\s*(.*)$/.exec(l)) .filter((l) => l.trim() !== "" && !/^[#;]/.test(l.trim()));
.filter((m) => m !== null); expect(lines).toEqual([
expect(Object.fromEntries(keys.map((m) => [m[1], m[2]!.trim()]))).toEqual({ "[pytest]",
testpaths: "tests", "testpaths = tests",
pythonpath: ".", "pythonpath = .",
addopts: "-q -rs --strict-markers", "addopts = -q -rs --strict-markers",
}); ]);
}); });
// --- knip ----------------------------------------------------------------------------------------- // --- knip -----------------------------------------------------------------------------------------
@@ -357,6 +421,8 @@ describe("the gate register (docs/gates.md)", () => {
expect(missingFrom(found)).toEqual([]); expect(missingFrom(found)).toEqual([]);
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> }; const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
expect(pkg.scripts.knip).toBe("knip && knip --production"); expect(pkg.scripts.knip).toBe("knip && knip --production");
// the positive pattern is the project itself: narrowing it drops files from both modes
expect((cfg.project ?? []).filter((p) => !p.startsWith("!"))).toEqual(["**/*.{ts,tsx,js}!"]);
}); });
// --- accepted findings ---------------------------------------------------------------------------- // --- accepted findings ----------------------------------------------------------------------------