E32 review round 1 (T1–T5, s1). The test that holds docs/gates.md to the effective configuration could itself be passed by a narrowing it did not look at: - ESLint: besides each scope's sample, every config block is read on its own. A block that targets files no sample reaches fails as an "unsampled block"; a no-restricted-* list re-declared without an entry src has, an option that lets something through (allow…/ignore…/except…), a block-level `ignores` and EVERY global ignores object (not only the first) count. A scope switching a src rule at warn off now counts too (it skipped them). Two live narrowings surfaced and are registered: no-floating-promises' allowForKnownSafeCalls (src and e2e) and the three network owners' no-restricted-globals. - TypeScript: a strict sub-flag set false beside `strict: true`, an extends/exclude/files, a changed include, and a frontend-root config file no project checks. eslint.config.js is that file: listed, with the 43 errors strict checkJs reports in it (measured, none a defect). - Ruff, basedpyright, pytest and knip are read as key whitelists (an unknown key fails until someone decides whether it narrows); pyright's information/hint count as weak; ruff's extend-select is pinned; pytest's three keys are pinned exactly; knip's script must run both modes. Each new check was probed with a planted narrowing (an unsampled block, a block ignores, a second global ignores, a warn rule off in a scope, strictNullChecks false, a ruff extend-exclude, a pytest -m) and failed on it.
385 lines
17 KiB
TypeScript
385 lines
17 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { ESLint, type Linter } from "eslint";
|
|
import prettierConfig from "eslint-config-prettier";
|
|
import ts from "typescript";
|
|
import tseslint from "typescript-eslint";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
// The gate register (docs/gates.md) is held to the EFFECTIVE configuration (E32). A narrowing that
|
|
// is not on the register fails here — a rule switched off for a scope, a weak compiler flag, a ruff
|
|
// ignore, a knip exclusion, an accepted CVE. So "the gate checks X" is a claim this test verifies,
|
|
// not one a comment makes. (The comment that claimed the React Compiler's rules "are also linted"
|
|
// stood for months while the rule that would have said so was off.)
|
|
//
|
|
// Each narrowing is a (scope, id) pair; the register must hold a table row whose first cell is the
|
|
// scope and which names the id in backticks.
|
|
|
|
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const FRONTEND = `${REPO}frontend/`;
|
|
const read = (path: string) => readFileSync(REPO + path, "utf8");
|
|
|
|
/** register scope → every backticked id on its rows. */
|
|
function register(): Map<string, Set<string>> {
|
|
const rows = new Map<string, Set<string>>();
|
|
for (const line of read("docs/gates.md").split("\n")) {
|
|
if (!line.startsWith("|") || /^\|(\s*-+\s*\|)+\s*$/.test(line)) continue; // not the |---| rule
|
|
const scope = line.split("|")[1]!.trim();
|
|
const ids = rows.get(scope) ?? new Set<string>();
|
|
for (const m of line.matchAll(/`([^`]+)`/g)) ids.add(m[1]!);
|
|
rows.set(scope, ids);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function missingFrom(found: [scope: string, id: string][]): string[] {
|
|
const reg = register();
|
|
return found.filter(([scope, id]) => !reg.get(scope)?.has(id)).map(([s, id]) => `${s}: ${id}`);
|
|
}
|
|
|
|
const jsonc = (path: string) => {
|
|
const { config, error } = ts.parseConfigFileTextToJson(path, read(path));
|
|
if (error)
|
|
throw new Error(`${path}: ${ts.flattenDiagnosticMessageText(error.messageText, "\n")}`);
|
|
return config as Record<string, unknown>;
|
|
};
|
|
|
|
// --- ESLint ---------------------------------------------------------------------------------------
|
|
|
|
type Rules = Partial<Linter.RulesRecord>;
|
|
const severity = (v: Linter.RuleEntry | undefined): number => {
|
|
if (v === undefined) return -1; // not applied at all
|
|
const s = Array.isArray(v) ? v[0] : v;
|
|
return s === 2 || s === "error" ? 2 : s === 1 || s === "warn" ? 1 : 0;
|
|
};
|
|
// Formatting is Prettier's (its own lane): the rules eslint-config-prettier switches off are not a
|
|
// narrowing of anything.
|
|
const PRETTIER_OFF = new Set(
|
|
Object.entries(prettierConfig.rules ?? {})
|
|
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
|
|
.map(([r]) => r),
|
|
);
|
|
// …and the core rules typescript-eslint's compatibility layer switches off for TS files because the
|
|
// type checker reports them itself (no-undef, no-redeclare, no-dupe-keys, …).
|
|
const TSC_OWNED = new Set(
|
|
Object.entries(tseslint.configs.eslintRecommended.rules ?? {})
|
|
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
|
|
.map(([r]) => r),
|
|
);
|
|
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
|
|
// means), and every config block that targets files is checked on its own (below) — so a block no
|
|
// sample reaches fails rather than passing unseen (E32 review: a narrowing scoped to any other file
|
|
// changed none of the sampled configs and needed no row).
|
|
const SCOPES: Record<string, string> = {
|
|
src: "src/components/Feed.tsx",
|
|
"src/lib": "src/lib/format.ts",
|
|
"lib/http.ts": "src/lib/http.ts",
|
|
"lib/api/core.ts": "src/lib/api/core.ts",
|
|
"lib/messagesSocket.ts": "src/lib/messagesSocket.ts",
|
|
"src tests": "src/components/Feed.ytsort.dom.test.tsx",
|
|
"FocusSafeButton.tsx": "src/components/ui/FocusSafeButton.tsx",
|
|
"AnchoredPopover.tsx": "src/components/ui/AnchoredPopover.tsx",
|
|
"design-lab": "src/design-lab/batchA.tsx",
|
|
e2e: "e2e/nav.spec.ts",
|
|
"ts configs": "vite.config.ts",
|
|
"js configs": "eslint.config.js",
|
|
};
|
|
|
|
/** A flat-config `files` glob as a regex — the shapes this config uses: `**`, `*`, `{a,b}`. */
|
|
function globRegex(glob: string): RegExp {
|
|
let out = "";
|
|
for (let i = 0; i < glob.length; i++) {
|
|
const c = glob[i]!;
|
|
if (glob.startsWith("**/", i)) {
|
|
out += "(?:.*/)?";
|
|
i += 2;
|
|
} else if (c === "*") out += "[^/]*";
|
|
else if (c === "{") {
|
|
const end = glob.indexOf("}", i);
|
|
out += `(?:${glob
|
|
.slice(i + 1, end)
|
|
.split(",")
|
|
.join("|")})`;
|
|
i = end;
|
|
} else out += /[.+?^$()|[\]\\]/.test(c) ? `\\${c}` : c;
|
|
}
|
|
return new RegExp(`^${out}$`);
|
|
}
|
|
|
|
type Block = Linter.Config & { files?: (string | string[])[]; ignores?: string[] };
|
|
const blocks = async (): Promise<Block[]> =>
|
|
((await import(pathToFileURL(`${FRONTEND}eslint.config.js`).href)) as { default: Block[] })
|
|
.default;
|
|
|
|
// A rule option whose key says it lets something through (`allowShortCircuit`,
|
|
// `allowForKnownSafeCalls`, `ignoreRestSiblings`, `exceptAfterSingleLine`) narrows the rule, and a
|
|
// `no-restricted-*` list re-declared for a scope narrows it by every entry it leaves out.
|
|
const RELAXING_KEY = /^(allow|ignore|except)/;
|
|
const relaxes = (options: unknown[]): boolean =>
|
|
options.some(
|
|
(o) =>
|
|
o !== null &&
|
|
typeof o === "object" &&
|
|
Object.entries(o).some(
|
|
([k, v]) => RELAXING_KEY.test(k) && v !== false && !(Array.isArray(v) && v.length === 0),
|
|
),
|
|
);
|
|
function restrictedEntries(entry: Linter.RuleEntry | undefined): Set<string> {
|
|
const opts: unknown[] = Array.isArray(entry) ? entry.slice(1) : [];
|
|
const flat = opts.flatMap((o) => {
|
|
if (o === null || typeof o !== "object" || Array.isArray(o)) return [o];
|
|
const { paths = [], patterns = [] } = o as { paths?: unknown[]; patterns?: unknown[] };
|
|
return "paths" in o || "patterns" in o ? [...paths, ...patterns] : [o];
|
|
});
|
|
return new Set(flat.map((e) => JSON.stringify(e)));
|
|
}
|
|
|
|
async function eslintNarrowings(): Promise<[string, string][]> {
|
|
const eslint = new ESLint({ cwd: FRONTEND });
|
|
const rules: Record<string, Rules> = {};
|
|
for (const [scope, file] of Object.entries(SCOPES))
|
|
rules[scope] = ((await eslint.calculateConfigForFile(file)) as Linter.Config).rules ?? {};
|
|
const src = rules.src!;
|
|
const ignorable = (rule: string, all: Rules) =>
|
|
PRETTIER_OFF.has(rule) ||
|
|
TSC_OWNED.has(rule) ||
|
|
// a core rule off because its typescript-eslint twin replaces it is reported as the twin
|
|
(!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
|
|
for (const [rule, v] of Object.entries(src)) {
|
|
if (ignorable(rule, src)) continue;
|
|
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
|
|
// 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")) {
|
|
const ids = new Set<string>();
|
|
for (const [rule, v] of Object.entries(src)) {
|
|
if (ignorable(rule, src) || severity(v) === 0) continue;
|
|
const there = severity(rules[scope]![rule]);
|
|
if (there < severity(v)) ids.add(there < 0 ? family(rule) : rule);
|
|
}
|
|
for (const id of ids) found.push([scope, id]);
|
|
}
|
|
// 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
|
|
for (const block of await blocks()) {
|
|
if (!block.files) {
|
|
if (Object.keys(block).join() === "ignores")
|
|
for (const ignored of block.ignores ?? []) found.push(["everywhere", ignored]);
|
|
continue;
|
|
}
|
|
const globs = block.files.flat().map(globRegex);
|
|
const scope = Object.keys(SCOPES).find((s) => globs.some((g) => g.test(SCOPES[s]!)));
|
|
if (!scope) {
|
|
found.push(["unsampled block", block.files.flat().join(", ")]);
|
|
continue;
|
|
}
|
|
if (block.ignores?.length) found.push([scope, `ignores: ${block.ignores.join(", ")}`]);
|
|
for (const [rule, v] of Object.entries(block.rules ?? {})) {
|
|
if (ignorable(rule, src)) continue;
|
|
if (rule.includes("no-restricted-")) {
|
|
const own = restrictedEntries(v);
|
|
if ([...restrictedEntries(src[rule])].some((e) => !own.has(e))) found.push([scope, rule]);
|
|
} else if (Array.isArray(v) && relaxes(v.slice(1))) found.push([scope, rule]);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
describe("the gate register (docs/gates.md)", () => {
|
|
it("lists every ESLint rule narrowed, by scope", async () => {
|
|
expect(missingFrom(await eslintNarrowings())).toEqual([]);
|
|
}, 60_000);
|
|
|
|
it("keeps --max-warnings 0, which is what makes a warn-level rule fail", () => {
|
|
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
|
|
expect(pkg.scripts.lint).toMatch(/--max-warnings 0\b/);
|
|
});
|
|
|
|
// --- TypeScript ---------------------------------------------------------------------------------
|
|
const PROJECTS = [
|
|
"frontend/tsconfig.json",
|
|
"frontend/tsconfig.node.json",
|
|
"frontend/e2e/tsconfig.json",
|
|
];
|
|
const REQUIRED_ON = [
|
|
"strict",
|
|
"noUncheckedIndexedAccess",
|
|
"noUnusedLocals",
|
|
"noUnusedParameters",
|
|
"noFallthroughCasesInSwitch",
|
|
];
|
|
it.each(PROJECTS)("%s keeps the strict flags on", (path) => {
|
|
const opts = jsonc(path).compilerOptions as Record<string, unknown>;
|
|
expect(REQUIRED_ON.filter((flag) => opts[flag] !== true)).toEqual([]);
|
|
});
|
|
// `strict: true` is a bundle: a sub-flag set false beside it, an `extends` this test cannot see
|
|
// through, or a narrowed `include` / an `exclude` weakens the project while the five flags above
|
|
// still read true (E32 review).
|
|
const STRICT_FAMILY = [
|
|
"noImplicitAny",
|
|
"strictNullChecks",
|
|
"strictFunctionTypes",
|
|
"strictBindCallApply",
|
|
"strictPropertyInitialization",
|
|
"noImplicitThis",
|
|
"alwaysStrict",
|
|
"useUnknownInCatchVariables",
|
|
"strictBuiltinIteratorReturn",
|
|
];
|
|
const INCLUDE: Record<string, string[]> = {
|
|
"frontend/tsconfig.json": ["src"],
|
|
"frontend/tsconfig.node.json": [
|
|
"vite.config.ts",
|
|
"vitest.config.ts",
|
|
"reactPlugin.ts",
|
|
"devOnlyGuard.ts",
|
|
"playwright.config.ts",
|
|
"postcss.config.js",
|
|
],
|
|
"frontend/e2e/tsconfig.json": [".", "../playwright.config.ts"],
|
|
};
|
|
it("lists every weakening compiler flag", () => {
|
|
const found: [string, string][] = [];
|
|
for (const path of PROJECTS) {
|
|
const cfg = jsonc(path);
|
|
const opts = cfg.compilerOptions as Record<string, unknown>;
|
|
if (opts.skipLibCheck === true) found.push(["all three projects", "skipLibCheck"]);
|
|
for (const flag of STRICT_FAMILY) if (opts[flag] === false) found.push([path, flag]);
|
|
for (const key of ["extends", "exclude", "files"]) if (key in cfg) found.push([path, key]);
|
|
expect(cfg.include, path).toEqual(INCLUDE[path]);
|
|
}
|
|
expect(missingFrom(found)).toEqual([]);
|
|
});
|
|
it("type-checks every config file at the frontend root, or lists it", () => {
|
|
const checked = new Set(INCLUDE["frontend/tsconfig.node.json"]);
|
|
const roots = readdirSync(FRONTEND).filter((f) => /[.](ts|js|mjs|cjs)$/.test(f));
|
|
const found = roots
|
|
.filter((f) => !checked.has(f))
|
|
.map((f): [string, string] => ["config files", `${f} — not type-checked`]);
|
|
expect(missingFrom(found)).toEqual([]);
|
|
});
|
|
|
|
// --- Ruff -----------------------------------------------------------------------------------------
|
|
// Ruff, pyright, pytest and knip are read as KEY whitelists: a key this test does not know (an
|
|
// `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 —
|
|
// only `ignore` and the immutable calls used to be read).
|
|
const tomlKeys = (toml: string) =>
|
|
toml
|
|
.split("\n")
|
|
.map((l) => /^\s*([A-Za-z0-9_.-]+)\s*=/.exec(l)?.[1])
|
|
.filter((k): k is string => k !== undefined);
|
|
it("lists every ruff ignore and immutable call, and knows every key", () => {
|
|
const toml = read("backend/ruff.toml");
|
|
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", "ignore", "line-length", "target-version"].sort(),
|
|
);
|
|
// the selection on top of ruff's defaults (E, F) — dropping one narrows the lane
|
|
expect(list("extend-select")).toEqual([
|
|
"LOG",
|
|
"G",
|
|
"T20",
|
|
"TRY400",
|
|
"TRY401",
|
|
"S110",
|
|
"S112",
|
|
"BLE001",
|
|
"RUF100",
|
|
"PLW1510",
|
|
"B",
|
|
]);
|
|
const found: [string, string][] = [
|
|
...list("ignore").map((code): [string, string] => ["backend + scripts", code]),
|
|
...list("extend-immutable-calls").map((c): [string, string] => ["backend + scripts", c]),
|
|
];
|
|
expect(missingFrom(found)).toEqual([]);
|
|
});
|
|
|
|
// --- basedpyright ---------------------------------------------------------------------------------
|
|
const PYRIGHT_KEYS = [
|
|
"include",
|
|
"extraPaths",
|
|
"typeCheckingMode",
|
|
"pythonVersion",
|
|
"pythonPlatform",
|
|
];
|
|
it("lists the type-check mode, the baseline and every rule override, and knows every key", () => {
|
|
const cfg = jsonc("backend/pyrightconfig.json");
|
|
const found: [string, string][] = [];
|
|
if (cfg.typeCheckingMode !== "all" && cfg.typeCheckingMode !== "strict")
|
|
found.push(["backend/app + alembic", "typeCheckingMode"]);
|
|
found.push(["backend/app + alembic", "baseline.json"]);
|
|
const weak = [false, "none", "warning", "information", "hint"];
|
|
for (const [key, value] of Object.entries(cfg))
|
|
if (key.startsWith("report") && weak.includes(value as string | boolean))
|
|
found.push(["backend/app + alembic", key]);
|
|
expect(
|
|
Object.keys(cfg).filter((k) => !k.startsWith("report") && !PYRIGHT_KEYS.includes(k)),
|
|
).toEqual([]);
|
|
expect(cfg.include).toEqual(["app", "alembic"]); // tests/ out — its row says why
|
|
expect(missingFrom(found)).toEqual([]);
|
|
});
|
|
|
|
// --- pytest ---------------------------------------------------------------------------------------
|
|
it("keeps pytest strict: the whole tests/ tree, no selection, no warning filters, --strict-markers", () => {
|
|
const ini = read("backend/pytest.ini");
|
|
const keys = ini
|
|
.split("\n")
|
|
.filter((l) => !l.trim().startsWith("#"))
|
|
.map((l) => /^([a-z_]+)\s*=\s*(.*)$/.exec(l))
|
|
.filter((m) => m !== null);
|
|
expect(Object.fromEntries(keys.map((m) => [m[1], m[2]!.trim()]))).toEqual({
|
|
testpaths: "tests",
|
|
pythonpath: ".",
|
|
addopts: "-q -rs --strict-markers",
|
|
});
|
|
});
|
|
|
|
// --- knip -----------------------------------------------------------------------------------------
|
|
it("lists every knip production exclusion and every other key, and runs both modes", () => {
|
|
const cfg = jsonc("frontend/knip.jsonc") as { project?: string[] } & Record<string, unknown>;
|
|
const found: [string, string][] = (cfg.project ?? [])
|
|
.filter((p) => p.startsWith("!"))
|
|
.map((p): [string, string] => ["--production", p]);
|
|
for (const key of Object.keys(cfg).filter(
|
|
(k) => !["$schema", "project", "playwright"].includes(k),
|
|
))
|
|
found.push(["knip", key]);
|
|
expect(missingFrom(found)).toEqual([]);
|
|
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
|
|
expect(pkg.scripts.knip).toBe("knip && knip --production");
|
|
});
|
|
|
|
// --- accepted findings ----------------------------------------------------------------------------
|
|
it("accepts a CVE only with a statement, an expiry, and a row", () => {
|
|
const yaml = read(".trivyignore.yaml");
|
|
const entries = yaml
|
|
.split(/\n\s*- id:\s*/)
|
|
.slice(1)
|
|
.map((chunk) => ({
|
|
id: chunk.split("\n")[0]!.trim(),
|
|
statement: /\n\s*statement:\s*\S/.test(chunk),
|
|
expires: /\n\s*expired_at:\s*\d{4}-\d{2}-\d{2}/.test(chunk),
|
|
}));
|
|
expect(entries.filter((e) => !e.statement || !e.expires).map((e) => e.id)).toEqual([]);
|
|
const pipIgnores = read(".gitea/workflows/check.yml")
|
|
.split("\n")
|
|
.filter((l) => !l.trim().startsWith("#")) // the step's comment explains the flag; it is not one
|
|
.flatMap((l) => [...l.matchAll(/--ignore-vuln[ =](\S+)/g)].map((m) => m[1]!));
|
|
const found = [...entries.map((e) => e.id), ...pipIgnores].map((id): [string, string] => [
|
|
"accepted",
|
|
id,
|
|
]);
|
|
expect(missingFrom(found)).toEqual([]);
|
|
});
|
|
});
|