♻️ Move type-aware config to .oxlintrc.json; use source-level disable directives

Cleaner separation of concerns:
- options.typeAware: true in .oxlintrc.json activates type-aware
  rules declaratively (equivalent to --type-aware CLI flag, but
  the script command stays clean: just 'oxlint ...')
- Remove the 3 type-aware rule disables from .oxlintrc.json
- Add source-level oxlint-disable directives instead:
  - 4x typescript/no-unsafe-type-assertion (pattern.ts: keysMatch
    Object.keys() cast, candidate[] cast; structuralMatcher value
    as S cast; match.ts: handler as ... cast in nextCases)
  - 1x typescript/no-unnecessary-type-parameters (pattern.ts:
    keysMatch <S extends object>)
  - 1x file-level typescript/no-floating-promises in index.test.ts
    (expectTypeOf() is a sync type-assertion library that the
    type-aware linter misidentifies)

Disabling rules at the source (next to the line that needs the
exemption) documents intent more clearly than a global config
override, and makes the trade-off visible to anyone reading the
code. Re-enabling a rule in the future only requires removing the
inline comment, not editing a central config.
This commit is contained in:
tmu committed 2026-09-04 23:27:58 +02:00
1 parent 2b3ef0f721
commit 34e9b52ee8
7 files changed
+38 -27

No files matched your search

+3 -5
View File
@@ -20,10 +20,9 @@
"eslint/sort-imports": "off", "eslint/sort-imports": "off",
"import/consistent-type-specifier-style": "off", "import/consistent-type-specifier-style": "off",
"unicorn/prefer-export-from": "off", "unicorn/prefer-export-from": "off",
"typescript/method-signature-style": "off", "typescript/method-signature-style": "off"
"typescript/no-unsafe-type-assertion": "off",
"typescript/no-unnecessary-type-parameters": "off"
}, },
"options": { "typeAware": true },
"env": { "builtin": true, "es2024": true, "node": true }, "env": { "builtin": true, "es2024": true, "node": true },
"overrides": [ "overrides": [
{ {
@@ -32,8 +31,7 @@
"no-unused-expressions": "off", "no-unused-expressions": "off",
"no-empty-file": "off", "no-empty-file": "off",
"import/no-nodejs-modules": "off", "import/no-nodejs-modules": "off",
"eslint/no-magic-numbers": "off", "eslint/no-magic-numbers": "off"
"typescript/no-floating-promises": "off"
} }
} }
], ],
+2 -2
View File
@@ -14,8 +14,8 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
- **TypeScript 7** — type checker and build (`tsc`). - **TypeScript 7** — type checker and build (`tsc`).
- **node --test** + `--experimental-strip-types` — test runner (Node 22.6+, flag dropped on Node 24). - **node --test** + `--experimental-strip-types` — test runner (Node 22.6+, flag dropped on Node 24).
- **c8** — code coverage for `test:ci`. - **c8** — code coverage for `test:ci`.
- **oxlint** — Rust-based linter. - **oxlint** — Rust-based linter. Type-aware rules are enabled via `options.typeAware: true` in `.oxlintrc.json` (powered by `oxlint-tsgolint`).
- **oxlint-tsgolint** — type-aware linting via typescript-go (powers `--type-aware` in `oxlint`). Catches unsafe type assertions, unnecessary type parameters, and other type-system issues that regular oxlint can't see. - **oxlint-tsgolint** — type-aware linting via typescript-go (declarative via `.oxlintrc.json`, no CLI flag). Catches unsafe type assertions, unnecessary type parameters, and other type-system issues that regular oxlint can't see. Source-level `oxlint-disable` directives are used to silence known false positives (e.g., `expectTypeOf()` in test files).
- **oxfmt** — Rust-based formatter (Prettier-compatible). Formats JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; built-in `package.json` key sorting replaces `sort-package-json`. - **oxfmt** — Rust-based formatter (Prettier-compatible). Formats JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; built-in `package.json` key sorting replaces `sort-package-json`.
- **cspell** — spell checking. - **cspell** — spell checking.
- **knip** — finds unused dependencies, exports, and files. Scoped via `--include dependencies,exports,files` to skip the noisy `types` category (which produces false positives for libraries whose exported types are part of the public API). - **knip** — finds unused dependencies, exports, and files. Scoped via `--include dependencies,exports,files` to skip the noisy `types` category (which produces false positives for libraries whose exported types are part of the public API).
+1 -1
View File
@@ -40,7 +40,7 @@
"check:knip": "knip --include dependencies,exports,files", "check:knip": "knip --include dependencies,exports,files",
"check:outdated": "check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-darwin-arm64,@oxfmt/binding-darwin-x64,@oxfmt/binding-linux-arm64-gnu,@oxfmt/binding-linux-arm64-musl,@oxfmt/binding-linux-x64-gnu,@oxfmt/binding-linux-x64-musl,@oxfmt/binding-win32-x64-msvc,@oxlint/binding-darwin-arm64,@oxlint/binding-darwin-x64,@oxlint/binding-linux-arm64-gnu,@oxlint/binding-linux-arm64-musl,@oxlint/binding-linux-x64-gnu,@oxlint/binding-linux-x64-musl,@oxlint/binding-win32-x64-msvc,@oxlint-tsgolint/darwin-arm64,@oxlint-tsgolint/darwin-x64,@oxlint-tsgolint/linux-arm64,@oxlint-tsgolint/linux-x64,@oxlint-tsgolint/win32-arm64,@oxlint-tsgolint/win32-x64", "check:outdated": "check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-darwin-arm64,@oxfmt/binding-darwin-x64,@oxfmt/binding-linux-arm64-gnu,@oxfmt/binding-linux-arm64-musl,@oxfmt/binding-linux-x64-gnu,@oxfmt/binding-linux-x64-musl,@oxfmt/binding-win32-x64-msvc,@oxlint/binding-darwin-arm64,@oxlint/binding-darwin-x64,@oxlint/binding-linux-arm64-gnu,@oxlint/binding-linux-arm64-musl,@oxlint/binding-linux-x64-gnu,@oxlint/binding-linux-x64-musl,@oxlint/binding-win32-x64-msvc,@oxlint-tsgolint/darwin-arm64,@oxlint-tsgolint/darwin-x64,@oxlint-tsgolint/linux-arm64,@oxlint-tsgolint/linux-x64,@oxlint-tsgolint/win32-arm64,@oxlint-tsgolint/win32-x64",
"check:oxfmt": "oxfmt --check ${LEFTHOOK_FILES:-.}", "check:oxfmt": "oxfmt --check ${LEFTHOOK_FILES:-.}",
"check:oxlint": "oxlint --type-aware ${LEFTHOOK_FILES:-src}", "check:oxlint": "oxlint ${LEFTHOOK_FILES:-src}",
"check:tsc": "tsc", "check:tsc": "tsc",
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
"fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}", "fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}",
+25 -19
View File
@@ -21,11 +21,14 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
- **oxlint**: Rust-based linter. Replaces ESLint. Config in `.oxlintrc.json` - **oxlint**: Rust-based linter. Replaces ESLint. Config in `.oxlintrc.json`
with `typescript`, `unicorn`, `oxc`, `import` plugins. Categories enabled with `typescript`, `unicorn`, `oxc`, `import` plugins. Categories enabled
as errors: `correctness`, `suspicious`, `restriction`. As warnings: `perf`, as errors: `correctness`, `suspicious`, `restriction`. As warnings: `perf`,
`style`. `nursery` is off. Run with `--type-aware` to enable rules `style`. `nursery` is off. Type-aware rules are enabled via
that require TypeScript type information (powered by `oxlint-tsgolint`). `options.typeAware: true` in `.oxlintrc.json` (no CLI flag needed);
the linter then uses `oxlint-tsgolint` for rules that require type
information.
- **oxlint-tsgolint**: TypeScript-Go-backed type-aware linter for oxlint. - **oxlint-tsgolint**: TypeScript-Go-backed type-aware linter for oxlint.
Activated via `oxlint --type-aware`. Native bindings installed as Activated by `options.typeAware: true` in `.oxlintrc.json`. Native
`optionalDependencies` per platform (same pattern as oxlint). bindings installed as `optionalDependencies` per platform (same
pattern as oxlint).
- **oxlint rules disabled by design** (in `.oxlintrc.json`): - **oxlint rules disabled by design** (in `.oxlintrc.json`):
- `eslint/no-undefined` — we use `undefined` as the no-match sentinel. - `eslint/no-undefined` — we use `undefined` as the no-match sentinel.
- `eslint/sort-keys` — handler/case order is semantic, not alphabetical. - `eslint/sort-keys` — handler/case order is semantic, not alphabetical.
@@ -222,22 +225,25 @@ script does not belong in the standard pipeline.
- `check`: Run all checks in order — `check:oxlint`, `check:oxfmt`, - `check`: Run all checks in order — `check:oxlint`, `check:oxfmt`,
`check:tsc`, `check:cspell`, `check:knip`, `check:outdated`. `check:tsc`, `check:cspell`, `check:knip`, `check:outdated`.
- `check:oxlint`: `oxlint --type-aware ${LEFTHOOK_FILES:-src}` — lints - `check:oxlint`: `oxlint ${LEFTHOOK_FILES:-src}` — lints `src/` by
`src/` by default; when invoked from the lefthook pre-commit hook default; when invoked from the lefthook pre-commit hook with
with `LEFTHOOK_FILES` set to the staged-files list, lints only those `LEFTHOOK_FILES` set to the staged-files list, lints only those
files. The `--type-aware` flag enables type-aware rules via files. This is the single source of truth for the oxlint command
`oxlint-tsgolint` (TypeScript-Go-backed). This is the single source and is shared between the manual `npm run check` and the pre-commit
of truth for the oxlint command and is shared between the manual hook. Type-aware rules are activated declaratively via
`npm run check` and the pre-commit hook. oxlint only understands `options.typeAware: true` in `.oxlintrc.json` (not via a CLI flag),
JS/TS-family languages, so config files (JSON/YAML/Markdown) are so the script command stays clean. oxlint only understands JS/TS-
family languages, so config files (JSON/YAML/Markdown) are
intentionally outside its scope; they are checked only by oxfmt. intentionally outside its scope; they are checked only by oxfmt.
Type-aware rules disabled (with rationale in `.oxlintrc.json`): Source-level `oxlint-disable` directives are used to silence
`no-unsafe-type-assertion` and `no-unnecessary-type-parameters` type-aware false positives in the generic type machinery: 4× for
fire on legitimate generic type machinery in `keysMatch` / `typescript/no-unsafe-type-assertion` and 1× for
`MatchBuilder` that needs type-system restructuring (out of scope `typescript/no-unnecessary-type-parameters` in `src/pattern.ts` /
for the tool-adoption commit); `no-floating-promises` is disabled `src/match.ts` (legitimate type machinery that needs restructuring,
in test files because `expectTypeOf(...)` is a sync type assertion documented in the source); 1× file-level for
library that oxlint-tsgolint misidentifies. `typescript/no-floating-promises` in `src/index.test.ts`
(`expectTypeOf(...)` is a sync type-assertion library that
oxlint-tsgolint misidentifies).
- `check:oxfmt`: `oxfmt --check ${LEFTHOOK_FILES:-.}` — formats the - `check:oxfmt`: `oxfmt --check ${LEFTHOOK_FILES:-.}` — formats the
whole project (`.`) by default, including JS/TS, JSON/JSONC, whole project (`.`) by default, including JS/TS, JSON/JSONC,
YAML, Markdown, MDX and other supported file types. From lefthook YAML, Markdown, MDX and other supported file types. From lefthook
+1
View File
@@ -1,3 +1,4 @@
/* oxlint-disable typescript/no-floating-promises -- expectTypeOf() is a sync type-assertion library that the type-aware linter misidentifies as a promise */
import { strict as assert } from "node:assert"; import { strict as assert } from "node:assert";
import { test } from "node:test"; import { test } from "node:test";
+1
View File
@@ -28,6 +28,7 @@ const buildMatch = <T, R>(value: T, cases: Cases<R>): MatchBuilder<T, R> => {
): MatchBuilder<T, R | V> { ): MatchBuilder<T, R | V> {
const nextCases: Cases<R | V> = [ const nextCases: Cases<R | V> = [
...cases, ...cases,
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
[pattern, handler as (value: unknown) => R | V], [pattern, handler as (value: unknown) => R | V],
]; ];
return buildMatch(value, nextCases); return buildMatch(value, nextCases);
+5
View File
@@ -53,12 +53,16 @@ const typeMatcher = <T>(
typeof expected === "object" && typeof expected === "object" &&
expected !== null && expected !== null &&
"matches" in expected, "matches" in expected,
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters
keysMatch = <S extends object>(shape: S, candidate: object): boolean => { keysMatch = <S extends object>(shape: S, candidate: object): boolean => {
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
for (const key of Object.keys(shape) as (keyof S)[]) { for (const key of Object.keys(shape) as (keyof S)[]) {
if (!(key in candidate)) { if (!(key in candidate)) {
return false; return false;
} }
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
const expected = shape[key], const expected = shape[key],
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
actual = candidate[key as keyof object]; actual = candidate[key as keyof object];
if (isNestedMatcher(expected)) { if (isNestedMatcher(expected)) {
if (!expected.matches(actual)) { if (!expected.matches(actual)) {
@@ -78,6 +82,7 @@ const typeMatcher = <T>(
if (typeof value !== "object" || value === null) { if (typeof value !== "object" || value === null) {
return false; return false;
} }
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
const candidate = value as S; const candidate = value as S;
if (!keysMatch(shape, candidate)) { if (!keysMatch(shape, candidate)) {
return false; return false;