From 627c085cee3da8893252c49262726478c2a92a77 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:29:19 +0800 Subject: [PATCH] chore(lint): enforce reviewed promise and fallback contracts --- .oxlintrc.json | 32 ++++++++++++++---- docs/testing.md | 16 +++++++-- .../filesystem/typedLintRules.test.ts | 33 +++++++++++++++---- .../reconstructionProofApplicability.ts | 2 ++ 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 1a8119d65e..5cc6ed8952 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -50,13 +50,13 @@ "rea/no-evidence-json-preadmission": "error", "rea/no-positional-tool-contract": "error", "rea/no-fixture-root-cleanup": "error", - "typescript/no-floating-promises": "warn", + "typescript/no-floating-promises": "error", "typescript/no-misused-promises": "warn", - "typescript/return-await": ["warn", "error-handling-correctness-only"], + "typescript/return-await": ["error", "error-handling-correctness-only"], "typescript/switch-exhaustiveness-check": [ - "warn", + "error", { - "considerDefaultExhaustiveForUnions": false + "considerDefaultExhaustiveForUnions": true } ] }, @@ -148,9 +148,29 @@ } }, { - "files": ["src/**/*.test.ts"], + "files": [ + "scripts/lib/http3-capture-assertions.test.mjs", + "scripts/lib/http3-verifier-lifecycle.test.mjs" + ], "rules": { - "typescript/no-floating-promises": "error" + "typescript/no-floating-promises": [ + "error", + { + "allowForKnownSafeCalls": [ + { + "from": "package", + "package": "@types/node", + "name": "test" + } + ] + } + ] + } + }, + { + "files": ["scripts/fixtures/javascript-recovery/code-split/entry.mjs"], + "rules": { + "typescript/no-floating-promises": "off" } } ], diff --git a/docs/testing.md b/docs/testing.md index f9dc40c84a..553cf71d27 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -169,14 +169,24 @@ deletion; mutable bindings and mixed root arrays remain outside its scope. Focused `.only` declarations, broken assertion promise chains, and unused disable directives are errors. `vitest/valid-expect` warns (`maxArgs: 2`) because valid stored and collected assertions can be awaited later. `import/no-duplicates` warns with -`preferInline: false`, preserving separate type imports. +`preferInline: false`. Fast lint stays syntax-based. `npm run lint:typed` runs serially with pinned `oxlint-tsgolint` 7.0.2005 and records native plus typed diagnostics in `.cache/static-checks/typed-oxlint.json`. Production `src` treats `no-misused-promises` as an error; tests/fixtures warn. `no-floating-promises` is -an error in `src/**/*.test.ts`; other scopes, return-await and exhaustiveness -findings warn pending triage. +an error throughout the lint inventory. `return-await` is an error in +error-handling scopes so asynchronous completion stays inside its catch or +cleanup boundary. Exhaustiveness is an error when a union member has neither a +case nor a default. Intentional defaults count as handling the remaining union; +new members covered by those defaults therefore require behavioral review. + +Two HTTP/3 source-test files allow the runner-owned `node:test` registration API; +ordinary unowned promises in those files still fail. The authored code-split +recovery input is exempt from floating-promise policy because it is analysis +input, rather than application orchestration. Preserve framework-owned callbacks +such as an awaited `Promise.finally`; inspect the actual completion owner before +changing a callback solely to satisfy its declared `void` return type. `npm run knip:production` writes advisory candidates to `.cache/static-checks/production-reachability.json`; execution/report errors fail. diff --git a/tests/boundary/filesystem/typedLintRules.test.ts b/tests/boundary/filesystem/typedLintRules.test.ts index 74694abb22..eaf3ddf178 100644 --- a/tests/boundary/filesystem/typedLintRules.test.ts +++ b/tests/boundary/filesystem/typedLintRules.test.ts @@ -60,12 +60,28 @@ export function promiseCondition(): string { "src/nonexhaustive.ts": ` type Outcome = { kind: "open" } | { kind: "closed" }; +export function describeOutcome(outcome: Outcome): string { + switch (outcome.kind) { + case "open": return "open"; + } + return "unknown"; +} +`, + "src/with-fallback.ts": ` +type Outcome = { kind: "open" } | { kind: "closed" }; export function describeOutcome(outcome: Outcome): string { switch (outcome.kind) { case "open": return "open"; default: return "unknown"; } } +`, + "scripts/lib/http3-capture-assertions.test.mjs": ` +import { test as registerTest } from "node:test"; +registerTest("runner owns this registration", async () => { + await Promise.resolve(); +}); +Promise.resolve("ordinary unowned promise remains visible"); `, "src/unawaited-finally.ts": ` export async function cleanupBeforeCompletion(): Promise { @@ -85,10 +101,10 @@ it("keeps test-only promise conditions advisory", () => { "tests/unjoined.test.ts": ` import { expect, it } from "vitest"; const cliTest = it.extend<{ label: string }>({ label: "owned" }); -it("warns about an unjoined boundary-test assertion", () => { +it("rejects an unjoined boundary-test assertion", () => { expect(Promise.resolve("ready")).resolves.toBe("ready"); }); -cliTest("warns in an extended boundary-test callback", ({ label }) => { +cliTest("rejects an unjoined extended boundary-test assertion", ({ label }) => { expect(Promise.resolve(label)).resolves.toBe("owned"); }); `, @@ -116,6 +132,8 @@ processTest( JSON.stringify({ compilerOptions: { strict: true, + allowJs: true, + checkJs: true, noEmit: true, skipLibCheck: true, module: "NodeNext", @@ -176,17 +194,18 @@ processTest( ) .sort(), ).toEqual([ + "scripts/lib/http3-capture-assertions.test.mjs: error: typescript(no-floating-promises)", "src/misused.ts: error: typescript(no-misused-promises)", - "src/nonexhaustive.ts: warning: typescript(switch-exhaustiveness-check)", + "src/nonexhaustive.ts: error: typescript(switch-exhaustiveness-check)", "src/test-misused.test.ts: warning: typescript(no-misused-promises)", - "src/unawaited-finally.ts: warning: typescript(return-await)", + "src/unawaited-finally.ts: error: typescript(return-await)", "src/unjoined.test.ts: error: typescript(no-floating-promises)", "src/unjoined.test.ts: error: typescript(no-floating-promises)", - "tests/unjoined.test.ts: warning: typescript(no-floating-promises)", - "tests/unjoined.test.ts: warning: typescript(no-floating-promises)", + "tests/unjoined.test.ts: error: typescript(no-floating-promises)", + "tests/unjoined.test.ts: error: typescript(no-floating-promises)", ]); expect( diagnostics.filter(({ severity }) => severity === "error"), - ).toHaveLength(3); + ).toHaveLength(8); }, ); diff --git a/tests/fixtures/reconstructionProofApplicability.ts b/tests/fixtures/reconstructionProofApplicability.ts index c1f71e1761..8d2b9428e8 100644 --- a/tests/fixtures/reconstructionProofApplicability.ts +++ b/tests/fixtures/reconstructionProofApplicability.ts @@ -133,6 +133,8 @@ export const reconstructionProofApplicabilityCases = () => ); const proof = result.bindings[0]!; switch (specification.name) { + case "matching implementation and revision": + break; case "stronger proof authority": input.manifest.bindings[0]!.fixtures[0]!.authority = "integration"; input.manifest.bindings[0]!.verifier!.authority = "integration";