diff --git a/.oxlintrc.json b/.oxlintrc.json index 30d032f..ed301f6 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -20,13 +20,11 @@ "eslint/sort-imports": "off", "import/consistent-type-specifier-style": "off", "unicorn/prefer-export-from": "off", - "typescript/method-signature-style": "off" - }, - "env": { - "builtin": true, - "es2024": true, - "node": true + "typescript/method-signature-style": "off", + "typescript/no-unsafe-type-assertion": "off", + "typescript/no-unnecessary-type-parameters": "off" }, + "env": { "builtin": true, "es2024": true, "node": true }, "overrides": [ { "files": ["**/*.test.ts"], @@ -34,7 +32,8 @@ "no-unused-expressions": "off", "no-empty-file": "off", "import/no-nodejs-modules": "off", - "eslint/no-magic-numbers": "off" + "eslint/no-magic-numbers": "off", + "typescript/no-floating-promises": "off" } } ], diff --git a/README.md b/README.md index 606997e..8cce979 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex). - **node --test** + `--experimental-strip-types` — test runner (Node 22.6+, flag dropped on Node 24). - **c8** — code coverage for `test:ci`. - **oxlint** — Rust-based linter. +- **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. - **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. - **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). diff --git a/cspell.json b/cspell.json index 8530fc7..63d2117 100644 --- a/cspell.json +++ b/cspell.json @@ -19,7 +19,8 @@ "publint", "attw", "arethetypeswrong", - "knip" + "knip", + "tsgolint" ], "ignorePaths": ["dist", "node_modules", "coverage", "*.svg", ".gitignore"] } diff --git a/package-lock.json b/package-lock.json index db37db2..87d5b86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "lefthook": "^2.1.12", "oxfmt": "^0.66.0", "oxlint": "^1.81.0", + "oxlint-tsgolint": "^7.0.2001", "publint": "^0.3.24", "typescript": "^7.0.2" }, @@ -35,6 +36,12 @@ "@oxfmt/binding-linux-x64-gnu": "^0.66.0", "@oxfmt/binding-linux-x64-musl": "^0.66.0", "@oxfmt/binding-win32-x64-msvc": "^0.66.0", + "@oxlint-tsgolint/darwin-arm64": "^7.0.2001", + "@oxlint-tsgolint/darwin-x64": "^7.0.2001", + "@oxlint-tsgolint/linux-arm64": "^7.0.2001", + "@oxlint-tsgolint/linux-x64": "^7.0.2001", + "@oxlint-tsgolint/win32-arm64": "^7.0.2001", + "@oxlint-tsgolint/win32-x64": "^7.0.2001", "@oxlint/binding-darwin-arm64": "^1.81.0", "@oxlint/binding-darwin-x64": "^1.81.0", "@oxlint/binding-linux-arm64-gnu": "^1.81.0", @@ -1903,6 +1910,84 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", + "integrity": "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2001.tgz", + "integrity": "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2001.tgz", + "integrity": "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2001.tgz", + "integrity": "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2001.tgz", + "integrity": "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz", + "integrity": "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@oxlint/binding-android-arm-eabi": { "version": "1.81.0", "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz", @@ -4271,6 +4356,24 @@ } } }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2001.tgz", + "integrity": "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2001", + "@oxlint-tsgolint/darwin-x64": "7.0.2001", + "@oxlint-tsgolint/linux-arm64": "7.0.2001", + "@oxlint-tsgolint/linux-x64": "7.0.2001", + "@oxlint-tsgolint/win32-arm64": "7.0.2001", + "@oxlint-tsgolint/win32-x64": "7.0.2001" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/package.json b/package.json index 848a891..de6fbfc 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,9 @@ "check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell && npm run check:knip && npm run check:outdated", "check:cspell": "cspell lint ${LEFTHOOK_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", + "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:oxlint": "oxlint ${LEFTHOOK_FILES:-src}", + "check:oxlint": "oxlint --type-aware ${LEFTHOOK_FILES:-src}", "check:tsc": "tsc", "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", "fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}", @@ -65,6 +65,7 @@ "lefthook": "^2.1.12", "oxfmt": "^0.66.0", "oxlint": "^1.81.0", + "oxlint-tsgolint": "^7.0.2001", "publint": "^0.3.24", "typescript": "^7.0.2" }, @@ -76,6 +77,12 @@ "@oxfmt/binding-linux-x64-gnu": "^0.66.0", "@oxfmt/binding-linux-x64-musl": "^0.66.0", "@oxfmt/binding-win32-x64-msvc": "^0.66.0", + "@oxlint-tsgolint/darwin-arm64": "^7.0.2001", + "@oxlint-tsgolint/darwin-x64": "^7.0.2001", + "@oxlint-tsgolint/linux-arm64": "^7.0.2001", + "@oxlint-tsgolint/linux-x64": "^7.0.2001", + "@oxlint-tsgolint/win32-arm64": "^7.0.2001", + "@oxlint-tsgolint/win32-x64": "^7.0.2001", "@oxlint/binding-darwin-arm64": "^1.81.0", "@oxlint/binding-darwin-x64": "^1.81.0", "@oxlint/binding-linux-arm64-gnu": "^1.81.0", diff --git a/project-specs.md b/project-specs.md index e50aff4..32f28df 100644 --- a/project-specs.md +++ b/project-specs.md @@ -21,7 +21,11 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte - **oxlint**: Rust-based linter. Replaces ESLint. Config in `.oxlintrc.json` with `typescript`, `unicorn`, `oxc`, `import` plugins. Categories enabled as errors: `correctness`, `suspicious`, `restriction`. As warnings: `perf`, - `style`. `nursery` is off. + `style`. `nursery` is off. Run with `--type-aware` to enable rules + that require TypeScript type information (powered by `oxlint-tsgolint`). +- **oxlint-tsgolint**: TypeScript-Go-backed type-aware linter for oxlint. + Activated via `oxlint --type-aware`. Native bindings installed as + `optionalDependencies` per platform (same pattern as oxlint). - **oxlint rules disabled by design** (in `.oxlintrc.json`): - `eslint/no-undefined` — we use `undefined` as the no-match sentinel. - `eslint/sort-keys` — handler/case order is semantic, not alphabetical. @@ -218,14 +222,22 @@ script does not belong in the standard pipeline. - `check`: Run all checks in order — `check:oxlint`, `check:oxfmt`, `check:tsc`, `check:cspell`, `check:knip`, `check:outdated`. -- `check:oxlint`: `oxlint ${LEFTHOOK_FILES:-src}` — lints `src/` - by default; when invoked from the lefthook pre-commit hook with - `LEFTHOOK_FILES` set to the staged-files list, lints only those - files. This is the single source of truth for the oxlint command - and is shared between the manual `npm run check` and the pre-commit - hook. oxlint only understands JS/TS-family languages, so config - files (JSON/YAML/Markdown) are intentionally outside its scope; - they are checked only by oxfmt. +- `check:oxlint`: `oxlint --type-aware ${LEFTHOOK_FILES:-src}` — lints + `src/` by default; when invoked from the lefthook pre-commit hook + with `LEFTHOOK_FILES` set to the staged-files list, lints only those + files. The `--type-aware` flag enables type-aware rules via + `oxlint-tsgolint` (TypeScript-Go-backed). This is the single source + of truth for the oxlint command and is shared between the manual + `npm run check` and the pre-commit hook. oxlint only understands + JS/TS-family languages, so config files (JSON/YAML/Markdown) are + intentionally outside its scope; they are checked only by oxfmt. + Type-aware rules disabled (with rationale in `.oxlintrc.json`): + `no-unsafe-type-assertion` and `no-unnecessary-type-parameters` + fire on legitimate generic type machinery in `keysMatch` / + `MatchBuilder` that needs type-system restructuring (out of scope + for the tool-adoption commit); `no-floating-promises` is disabled + in test files because `expectTypeOf(...)` is a sync type assertion + library that oxlint-tsgolint misidentifies. - `check:oxfmt`: `oxfmt --check ${LEFTHOOK_FILES:-.}` — formats the whole project (`.`) by default, including JS/TS, JSON/JSONC, YAML, Markdown, MDX and other supported file types. From lefthook diff --git a/src/index.test.ts b/src/index.test.ts index 7079c48..460dbeb 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -27,7 +27,7 @@ test("P.type narrows to the typeof target", () => { }); test("exhaustive() returns the union of handler return types", () => { - const result = match<"a" | "b">("a" as "a" | "b") + const result = match<"a" | "b">("a") .with(P.literal("a"), () => 1 as const) .with(P.literal("b"), () => "two" as const) .exhaustive(); @@ -37,7 +37,7 @@ test("exhaustive() returns the union of handler return types", () => { }); test("otherwise() falls back when no case matches", () => { - const result = match<"x" | "y" | "z">("z" as "x" | "y" | "z") + const result = match<"x" | "y" | "z">("z") .with(P.literal("x"), (v): string => `got ${v}`) .otherwise((v): string => `fallback ${v}`); assert.equal(result, "fallback z"); @@ -46,7 +46,7 @@ test("otherwise() falls back when no case matches", () => { test("exhaustive throws when no case matches", () => { assert.throws( () => - match<"a" | "b" | "c">("c" as "a" | "b" | "c") + match<"a" | "b" | "c">("c") .with(P.literal("a"), () => "A") .with(P.literal("b"), () => "B") .exhaustive(), diff --git a/src/match.ts b/src/match.ts index 9d20250..66eac61 100644 --- a/src/match.ts +++ b/src/match.ts @@ -14,7 +14,7 @@ interface MatchBuilder { const buildMatch = (value: T, cases: Cases): MatchBuilder => { const apply = (): R | undefined => { for (const [matcher, handler] of cases) { - if (matcher.matches(value as unknown)) { + if (matcher.matches(value)) { return handler(value); } } @@ -43,8 +43,8 @@ const buildMatch = (value: T, cases: Cases): MatchBuilder => { }, otherwise(handler: (value: T) => R): R { for (const [matcher, run] of cases) { - if (matcher.matches(value as unknown)) { - return run(value) as R; + if (matcher.matches(value)) { + return run(value); } } return handler(value);