From 99fd85ca7e86b043bc17fa52621940b9e2548fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 3 Sep 2026 21:33:04 +0000 Subject: [PATCH] :wrench: Resolve oxlint warnings; defer stylistic rules to oxfmt --- .oxlintrc.json | 9 +++- project-specs.md | 26 ++++++++-- src/index.test.ts | 2 +- src/match.ts | 4 +- src/pattern.ts | 125 +++++++++++++++++++++++++--------------------- 5 files changed, 99 insertions(+), 67 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index daf442c..30d032f 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -13,7 +13,14 @@ "eslint/no-undefined": "off", "eslint/sort-keys": "off", "eslint/id-length": "off", - "import/no-named-export": "off" + "import/no-named-export": "off", + "eslint/one-var": "off", + "import/group-exports": "off", + "import/exports-last": "off", + "eslint/sort-imports": "off", + "import/consistent-type-specifier-style": "off", + "unicorn/prefer-export-from": "off", + "typescript/method-signature-style": "off" }, "env": { "builtin": true, diff --git a/project-specs.md b/project-specs.md index 520f54a..04ebab1 100644 --- a/project-specs.md +++ b/project-specs.md @@ -27,11 +27,27 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte - `eslint/sort-keys` — handler/case order is semantic, not alphabetical. - `eslint/id-length` — `T`, `R`, `U`, `V` are standard TS generics. - `import/no-named-export` — false positive on library entry re-exports. - For `*.test.ts` files additionally: `no-unused-expressions` (for - `expectTypeOf(...)` calls), `no-empty-file` (we have a single import - per test file in some cases), `import/no-nodejs-modules` (we use - `node:test`/`node:assert`/`node:fs` intentionally), `eslint/no-magic-numbers` - (literals in tests are fine). + - Stylistic rules superseded by oxfmt (oxfmt is the canonical + formatter; these rules either conflict with its output or + duplicate features oxfmt already provides): + - `eslint/one-var` — oxfmt uses comma-joined `const` + declarations; oxlint wanted to split them. + - `import/group-exports` — oxfmt keeps separate `export` + statements as-is. + - `import/exports-last` — statement ordering is up to oxfmt. + - `eslint/sort-imports` — replaced by oxfmt's built-in + `sortImports` (enabled in `.oxfmtrc.json`). + - `import/consistent-type-specifier-style` — inline + `import { type X, Y }` is intentional for grouping. + - `unicorn/prefer-export-from` — conflicts with how the + barrel `src/index.ts` re-exports through `src/match.ts`. + - `typescript/method-signature-style` — method signatures + in `interface` are conventional TS ergonomics. + For `*.test.ts` files additionally: `no-unused-expressions` (for + `expectTypeOf(...)` calls), `no-empty-file` (we have a single import + per test file in some cases), `import/no-nodejs-modules` (we use + `node:test`/`node:assert`/`node:fs` intentionally), `eslint/no-magic-numbers` + (literals in tests are fine). - **Import sorting**: built into oxfmt (no separate plugin). - **cspell**: Basic spelling configuration. Words dictionary in `cspell.json` covers tooling names (`oxlint`, `oxfmt`, `oxc`, `nodenext`, diff --git a/src/index.test.ts b/src/index.test.ts index 8f6ada1..7079c48 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3,7 +3,7 @@ import { test } from "node:test"; import { expectTypeOf } from "expect-type"; -import { match, P, type Matcher } from "./index.ts"; +import { type Matcher, P, match } from "./index.ts"; test("match returns a builder", () => { const builder = match("x"); diff --git a/src/match.ts b/src/match.ts index a7b6c84..9d20250 100644 --- a/src/match.ts +++ b/src/match.ts @@ -1,8 +1,6 @@ import { P, type Matcher, type Pattern } from "./pattern.ts"; -type Cases = ReadonlyArray< - readonly [Matcher, (value: unknown) => R] ->; +type Cases = readonly (readonly [Matcher, (value: unknown) => R])[]; interface MatchBuilder { with( diff --git a/src/pattern.ts b/src/pattern.ts index 7261c72..9e61cce 100644 --- a/src/pattern.ts +++ b/src/pattern.ts @@ -14,69 +14,80 @@ const literalMatcher = < ): Matcher => ({ matches: (candidate): candidate is L => candidate === value, }); - const typeMatcher = ( - type: - | "string" - | "number" - | "boolean" - | "bigint" - | "symbol" - | "undefined" - | "object" - | "function", -): Matcher => { - const matches = (value: unknown): value is T => { - if (type === "undefined") return value === undefined; - if (type === "object") - return ( - (typeof value === "object" && value !== null) || - typeof value === "function" - ); - return typeof value === type; - }; - return { matches }; -}; - -const whenMatcher = ( - predicate: (value: unknown) => value is T, -): Matcher => ({ - matches: predicate, -}); - -const whenMatcherAny = ( - predicate: (value: unknown) => boolean, -): Matcher => ({ - matches: (value: unknown): value is T => predicate(value), -}); - -const structuralMatcher = ( - shape: S, - refine?: (value: S) => value is T, -): Matcher => ({ - matches: (value: unknown): value is T => { - if (typeof value !== "object" || value === null) return false; - const candidate = value as S; - for (const key of Object.keys(shape) as Array) { - if (!(key in candidate)) return false; - const expected = shape[key]; - const actual = candidate[key]; - if ( - typeof expected === "object" && - expected !== null && - "matches" in expected - ) { - const nested = expected as unknown as Matcher; - if (!nested.matches(actual)) return false; + type: + | "string" + | "number" + | "boolean" + | "bigint" + | "symbol" + | "undefined" + | "object" + | "function", + ): Matcher => { + const matches = (value: unknown): value is T => { + if (type === "undefined") { + return value === undefined; + } + if (type === "object") { + return ( + (typeof value === "object" && value !== null) || + typeof value === "function" + ); + } + return typeof value === type; + }; + return { matches }; + }, + whenMatcher = ( + predicate: (value: unknown) => value is T, + ): Matcher => ({ + matches: predicate, + }), + whenMatcherAny = ( + predicate: (value: unknown) => boolean, + ): Matcher => ({ + matches: (value: unknown): value is T => predicate(value), + }), + isNestedMatcher = (expected: unknown): expected is Matcher => + typeof expected === "object" && + expected !== null && + "matches" in expected, + keysMatch = (shape: S, candidate: object): boolean => { + for (const key of Object.keys(shape) as (keyof S)[]) { + if (!(key in candidate)) { + return false; + } + const expected = shape[key], + actual = candidate[key as keyof object]; + if (isNestedMatcher(expected)) { + if (!expected.matches(actual)) { + return false; + } } else if (actual !== expected) { return false; } } - return refine - ? refine(candidate) - : (true as T extends S ? true : never); + return true; }, -}); + structuralMatcher = ( + shape: S, + refine?: (value: S) => value is T, + ): Matcher => ({ + matches: (value: unknown): value is T => { + if (typeof value !== "object" || value === null) { + return false; + } + const candidate = value as S; + if (!keysMatch(shape, candidate)) { + return false; + } + if (refine && !refine(candidate)) { + return false; + } + return true; + }, + }); export const P = { literal: literalMatcher,