From 91ac39c59c0105496d8b3cb3fad005016ab38838 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Sat, 7 Nov 2026 21:35:03 +0100 Subject: [PATCH] feat(oxlint-config): widen the library rule set and fix test override globs --- .changeset/library-rule-audit.md | 9 ++ oxlint.config.ts | 15 +-- .../oxlint-config/src/cloud-setup-claude.ts | 2 +- packages/oxlint-config/src/cloud-setup-cli.ts | 1 + .../src/cloud-setup-toolchains.ts | 2 +- packages/oxlint-config/src/index.ts | 111 ++++++++++++++---- .../src/library-overrides.test.ts | 71 +++++++++++ packages/oxlint-config/src/plugin.ts | 2 +- .../src/publish-artifact-archive.test.ts | 4 +- .../src/publish-artifact-syntax.ts | 2 + .../publish-build-target-vite-dts-helper.ts | 2 +- .../oxlint-config/src/toolchain-changed.ts | 2 +- .../oxlint-config/src/toolchain-rust.test.ts | 2 +- .../src/toolchain-workflow-trust.test.ts | 4 +- packages/oxlint-config/src/typecheck-probe.ts | 1 + .../src/no-raw-route-query-client.ts | 5 +- .../oxlint-plugin/src/no-unsafe-inner-html.ts | 8 +- packages/oxlint-plugin/src/utils.ts | 2 +- packages/test-kit/src/rejection.test.ts | 1 + 19 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 .changeset/library-rule-audit.md create mode 100644 packages/oxlint-config/src/library-overrides.test.ts diff --git a/.changeset/library-rule-audit.md b/.changeset/library-rule-audit.md new file mode 100644 index 00000000..8d4fea2d --- /dev/null +++ b/.changeset/library-rule-audit.md @@ -0,0 +1,9 @@ +--- +"@stll/oxlint-config": major +"@stll/oxlint-plugin": patch +"@stll/test-kit": patch +--- + +`library()` enables more bug-shaped rules outside oxlint's default correctness category: `eqeqeq` (with `== null` allowed), `no-var`, `prefer-const`, `no-case-declarations`, `default-case-last`, `array-callback-return`, `no-self-compare`, `no-unneeded-ternary`, `no-useless-concat`, `no-useless-constructor`, `preserve-caught-error`, `no-implied-eval`, `no-new-func`, `no-extend-native`, `no-proto`, TypeScript `ban-ts-comment`, `no-require-imports`, `no-empty-object-type`, `no-unsafe-function-type`, `no-unnecessary-type-constraint`, `no-confusing-non-null-assertion`, `no-deprecated`, `no-unnecessary-type-assertion`, `no-unnecessary-template-expression`, `no-unnecessary-boolean-literal-compare`, `restrict-plus-operands`, `no-unsafe-enum-comparison`, `no-mixed-enums`, `prefer-promise-reject-errors`, `use-unknown-in-catch-callback-variable`, `prefer-includes`, `prefer-string-starts-ends-with` and `prefer-find`, unicorn `prefer-node-protocol`, `prefer-array-flat-map`, `no-instanceof-builtins`, `no-accessor-recursion` and `require-post-message-target-origin`, `oxc/misrefactored-assign-op`, import `no-duplicates`, `no-self-import`, `no-absolute-path` and `no-empty-named-blocks`, and `promise/no-multiple-resolved`. Files selected with `react.files` now also get React's correctness rules (keys, duplicate props, state mutation, removed APIs), except `exhaustive-deps` and `jsx-no-undef`, which the compiler rules and TypeScript already cover. + +The test override now matches nested `__tests__/` and `__test__/` folders and `.mts`, `.cts`, `.mjs` and `.cjs` test files. Its globs were anchored at the config directory, so tests inside packages did not get it. diff --git a/oxlint.config.ts b/oxlint.config.ts index d3b40fe2..8abf767a 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -25,15 +25,10 @@ export default library({ overrides: [ { // Oxlint presents plugin AST nodes as intentionally untyped values. - // Type-aware any-flow and condition rules cannot add safety inside the - // visitor implementation; the passive fixtures prove rule behavior. + // Type-aware condition rules cannot add safety inside the visitor + // implementation; the passive fixtures prove rule behavior. files: ["packages/oxlint-plugin/src/**/*.ts"], rules: { - "typescript/no-unsafe-assignment": "off", - "typescript/no-unsafe-member-access": "off", - "typescript/no-unsafe-call": "off", - "typescript/no-unsafe-return": "off", - "typescript/no-unsafe-argument": "off", "typescript/strict-boolean-expressions": "off", "typescript/no-unnecessary-condition": "off", }, @@ -52,6 +47,12 @@ export default library({ "typescript/no-confusing-void-expression": "off", "unicorn/no-useless-spread": "off", "no-sparse-arrays": "off", + // Fixtures spell out the import, URL and rejection shapes the + // test-reliability rules detect, including unidiomatic ones. + "unicorn/prefer-node-protocol": "off", + "import/no-duplicates": "off", + "typescript/no-unnecessary-template-expression": "off", + "typescript/prefer-promise-reject-errors": "off", }, }, { diff --git a/packages/oxlint-config/src/cloud-setup-claude.ts b/packages/oxlint-config/src/cloud-setup-claude.ts index 5746e52f..a05aeb37 100644 --- a/packages/oxlint-config/src/cloud-setup-claude.ts +++ b/packages/oxlint-config/src/cloud-setup-claude.ts @@ -257,7 +257,7 @@ const parseSettings = (text: string) => { return settings; } catch (error) { if (error instanceof SyntaxError) - throw new Error("settings must contain valid JSON"); + throw new Error("settings must contain valid JSON", { cause: error }); throw error; } }; diff --git a/packages/oxlint-config/src/cloud-setup-cli.ts b/packages/oxlint-config/src/cloud-setup-cli.ts index f4b1bec1..3c525e52 100644 --- a/packages/oxlint-config/src/cloud-setup-cli.ts +++ b/packages/oxlint-config/src/cloud-setup-cli.ts @@ -44,6 +44,7 @@ const readClaudeSettings = async (destination: string) => { } catch (error) { throw new Error( `cannot safely merge ${cloudClaudeSettingsPath}: ${error instanceof Error ? error.message : String(error)}; ${cloudSessionStartFix}`, + { cause: error }, ); } }; diff --git a/packages/oxlint-config/src/cloud-setup-toolchains.ts b/packages/oxlint-config/src/cloud-setup-toolchains.ts index fead91d9..3a830af3 100644 --- a/packages/oxlint-config/src/cloud-setup-toolchains.ts +++ b/packages/oxlint-config/src/cloud-setup-toolchains.ts @@ -10,8 +10,8 @@ import { cloudExtraTools, isCloudPackageTool, parseCloudRustNames, + type parseCloudSetup, } from "./cloud-setup-schema"; -import type { parseCloudSetup } from "./cloud-setup-schema"; import type { RustException } from "./toolchain-rust"; type ResolveCloudToolchainsOptions = { diff --git a/packages/oxlint-config/src/index.ts b/packages/oxlint-config/src/index.ts index bd3cbc43..0c696ac2 100644 --- a/packages/oxlint-config/src/index.ts +++ b/packages/oxlint-config/src/index.ts @@ -72,6 +72,27 @@ export const libraryRules = { "no-loop-func": "error", "no-nested-ternary": "error", "no-void": ["error", { allowAsStatement: true }], + // `== null` stays allowed as the one idiomatic nullish check. + eqeqeq: ["error", "always", { null: "ignore" }], + "no-var": "error", + "prefer-const": "error", + // Bug-shaped constructs outside oxlint's default correctness category. + // Switch fallthrough is left to `noFallthroughCasesInSwitch` in the shared + // tsconfig; oxlint's rule misreads commented empty cases. + "no-case-declarations": "error", + "default-case-last": "error", + "array-callback-return": "error", + "no-self-compare": "error", + "no-unneeded-ternary": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + // Rethrowing a new error must keep the original as `cause`. + "preserve-caught-error": "error", + // String-evaluated code and prototype tampering. + "no-implied-eval": "error", + "no-new-func": "error", + "no-extend-native": "error", + "no-proto": "error", // Callback expressions should stay lexically scoped; avoids accidental `this` rebinding. "prefer-arrow-callback": "error", "stella-lowercase/stella-lowercase": "error", @@ -117,7 +138,28 @@ export const libraryRules = { { ignorePrimitives: { string: true, boolean: true } }, ], "typescript/only-throw-error": "error", + // Rejections follow the same Error-only contract as `throw`. + "typescript/prefer-promise-reject-errors": "error", + "typescript/use-unknown-in-catch-callback-variable": "error", "typescript/return-await": ["error", "error-handling-correctness-only"], + // Bare `@ts-ignore`/`@ts-nocheck` hide type errors; `@ts-expect-error` + // needs a description and fails once the error it covers is gone. + "typescript/ban-ts-comment": "error", + "typescript/no-require-imports": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-confusing-non-null-assertion": "error", + "typescript/no-deprecated": "error", + "typescript/no-unnecessary-type-assertion": "error", + "typescript/no-unnecessary-template-expression": "error", + "typescript/no-unnecessary-boolean-literal-compare": "error", + "typescript/restrict-plus-operands": "error", + "typescript/no-unsafe-enum-comparison": "error", + "typescript/no-mixed-enums": "error", + "typescript/prefer-includes": "error", + "typescript/prefer-string-starts-ends-with": "error", + "typescript/prefer-find": "error", "unicorn/no-useless-undefined": "off", "unicorn/prefer-array-find": "error", @@ -130,18 +172,35 @@ export const libraryRules = { "unicorn/no-nested-ternary": "off", // `Array.from({ length }).fill(obj)` shares one reference across every slot. "unicorn/no-array-fill-with-reference-type": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-array-flat-map": "error", + // `instanceof Array`/`String` fails across realms and for primitives. + "unicorn/no-instanceof-builtins": "error", + "unicorn/no-accessor-recursion": "error", + "unicorn/require-post-message-target-origin": "error", + + "oxc/misrefactored-assign-op": "error", "import/no-cycle": "error", + "import/no-duplicates": "error", + "import/no-self-import": "error", + "import/no-absolute-path": "error", + "import/no-empty-named-blocks": "error", "import/consistent-type-specifier-style": "off", // Keep a blank line after the import block so it stays visually separate from module body. "import/newline-after-import": "error", "promise/always-return": "error", "promise/no-return-in-finally": "error", + "promise/no-multiple-resolved": "error", "promise/prefer-await-to-then": "off", "promise/prefer-await-to-callbacks": "off", "promise/avoid-new": "off", + // None of these are on in `library()` itself (oxlint enables only the + // correctness category). They stay listed because consumers spread + // `libraryRules` over broader presets such as Ultracite, where these + // opinionated rules are on and would otherwise apply. "sort-keys": "off", "no-plusplus": "off", "no-inline-comments": "off", @@ -163,36 +222,42 @@ export const libraryRules = { // Applied only to the consumer-selected React files. export const reactRules = { - // Enabling the `react` plugin turns on its whole correctness-category - // rule set at "warn" by default, not just the React Compiler categories. - // Only the compiler categories are deliberate additions here, so the rest - // are turned off to keep this package's blast radius scoped; lift any of - // these independently if a future change wants them. + // Enabling the `react` plugin turns on its whole correctness category. + // These two duplicate other checks: the compiler's dependency rules + // replace `exhaustive-deps`, and TypeScript reports undefined JSX names. "react/exhaustive-deps": "off", - "react/forward-ref-uses-ref": "off", - "react/jsx-key": "off", - "react/jsx-no-duplicate-props": "off", "react/jsx-no-undef": "off", - "react/jsx-props-no-spread-multi": "off", - "react/no-children-prop": "off", - "react/no-danger-with-children": "off", - "react/no-did-mount-set-state": "off", - "react/no-did-update-set-state": "off", - "react/no-direct-mutation-state": "off", - "react/no-find-dom-node": "off", - "react/no-is-mounted": "off", - "react/no-render-return-value": "off", - "react/no-string-refs": "off", - "react/no-this-in-sfc": "off", - "react/no-unsafe": "off", - "react/no-will-update-set-state": "off", - "react/void-dom-elements-no-children": "off", + // The rest of the category reports real bugs (missing keys, duplicate + // props, mutated state, removed React APIs), so it stays on as errors. + "react/forward-ref-uses-ref": "error", + "react/jsx-key": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-props-no-spread-multi": "error", + "react/no-children-prop": "error", + "react/no-danger-with-children": "error", + "react/no-did-mount-set-state": "error", + "react/no-did-update-set-state": "error", + "react/no-direct-mutation-state": "error", + "react/no-find-dom-node": "error", + "react/no-is-mounted": "error", + "react/no-render-return-value": "error", + "react/no-string-refs": "error", + "react/no-this-in-sfc": "error", + "react/no-unsafe": "error", + "react/no-will-update-set-state": "error", + "react/void-dom-elements-no-children": "error", ...reactCompilerRules, } satisfies Rules; export const libraryOverrides = [ { - files: ["**/*.{test,spec}.{ts,tsx,js,jsx}", "__test__/**", "__tests__/**"], + // Override globs are anchored at the config directory, so test folders + // need a leading `**/` to match inside packages. + files: [ + "**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", + "**/__test__/**", + "**/__tests__/**", + ], rules: { "no-console": "off", "require-await": "off", diff --git a/packages/oxlint-config/src/library-overrides.test.ts b/packages/oxlint-config/src/library-overrides.test.ts new file mode 100644 index 00000000..6e8aea4e --- /dev/null +++ b/packages/oxlint-config/src/library-overrides.test.ts @@ -0,0 +1,71 @@ +/// + +import { expect, test } from "bun:test"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { library } from "./index"; + +const oxlintBin = fileURLToPath( + new URL("./bin/oxlint", import.meta.resolve("oxlint/package.json")), +); + +test("test overrides reach nested test folders and every module extension", async () => { + const directory = await mkdtemp(join(tmpdir(), "library-overrides-")); + try { + const source = "export const size = (value?: string) => value!.length;\n"; + const files = [ + "packages/core/src/size.ts", + "packages/core/__tests__/size.ts", + "packages/core/__test__/size.ts", + "packages/core/src/size.test.mts", + "packages/core/src/size.spec.cts", + ]; + for (const file of files) { + await mkdir(join(directory, file, ".."), { recursive: true }); + await writeFile(join(directory, file), source); + } + const config = library({ + options: { typeAware: false, denyWarnings: false }, + }); + config.jsPlugins = []; + const rules = { ...config.rules }; + delete rules["stella-lowercase/stella-lowercase"]; + delete rules["no-raw-colors/no-raw-colors"]; + config.rules = rules; + await writeFile(join(directory, "oxlint.json"), JSON.stringify(config)); + + const lint = Bun.spawn( + [oxlintBin, "-c", "oxlint.json", "--format", "json", "."], + { cwd: directory, stdout: "pipe", stderr: "pipe" }, + ); + const [stdout, exitCode] = await Promise.all([ + new Response(lint.stdout).text(), + lint.exited, + ]); + const result: unknown = JSON.parse(stdout); + if ( + typeof result !== "object" || + result === null || + !("diagnostics" in result) || + !Array.isArray(result.diagnostics) + ) + throw new Error(`Unexpected oxlint output: ${stdout}`); + const flagged: unknown[] = result.diagnostics.map((diagnostic: unknown) => + typeof diagnostic === "object" && + diagnostic !== null && + "code" in diagnostic && + "filename" in diagnostic + ? `${String(diagnostic.code)} ${String(diagnostic.filename)}` + : diagnostic, + ); + expect(flagged).toEqual([ + "typescript(no-non-null-assertion) packages/core/src/size.ts", + ]); + expect(exitCode).toBe(1); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}, 15_000); diff --git a/packages/oxlint-config/src/plugin.ts b/packages/oxlint-config/src/plugin.ts index fe719853..e2bc87fd 100644 --- a/packages/oxlint-config/src/plugin.ts +++ b/packages/oxlint-config/src/plugin.ts @@ -20,7 +20,7 @@ const isAstNode = (node: unknown): node is AstNode => typeof node === "object" && node !== null && "type" in node && - typeof (node as { type: unknown }).type === "string"; + typeof node.type === "string"; const isStringLiteral = (node: unknown): node is AstNode & { value: string } => isAstNode(node) && diff --git a/packages/oxlint-config/src/publish-artifact-archive.test.ts b/packages/oxlint-config/src/publish-artifact-archive.test.ts index 6d53c328..a9bb8a74 100644 --- a/packages/oxlint-config/src/publish-artifact-archive.test.ts +++ b/packages/oxlint-config/src/publish-artifact-archive.test.ts @@ -151,8 +151,8 @@ test("malformed packed JavaScript retains its import parser cause and member pat readPackedSyntaxFiles(archive), (error: unknown) => error instanceof SyntaxError && - /Packed runtime import syntax package\/dist\/chunks\/hidden\.mjs:/.test( - error.message, + error.message.includes( + "Packed runtime import syntax package/dist/chunks/hidden.mjs:", ) && error.cause instanceof SyntaxError, ); diff --git a/packages/oxlint-config/src/publish-artifact-syntax.ts b/packages/oxlint-config/src/publish-artifact-syntax.ts index 4dfcdd49..c8e29df1 100644 --- a/packages/oxlint-config/src/publish-artifact-syntax.ts +++ b/packages/oxlint-config/src/publish-artifact-syntax.ts @@ -160,6 +160,7 @@ export const assertPackedArtifactSyntax = ({ } catch (error) { throw new Error( `Packed JavaScript ${file} exceeds its declared syntax target: ${String(error)}`, + { cause: error }, ); } } @@ -215,6 +216,7 @@ export const checkPackedJavaScriptWithNode = async ({ } catch (error) { throw new Error( `Packed JavaScript ${file} is incompatible with consumer Node ${node}: ${String(error)}`, + { cause: error }, ); } } diff --git a/packages/oxlint-config/src/publish-build-target-vite-dts-helper.ts b/packages/oxlint-config/src/publish-build-target-vite-dts-helper.ts index 78ddd48c..0d39263a 100644 --- a/packages/oxlint-config/src/publish-build-target-vite-dts-helper.ts +++ b/packages/oxlint-config/src/publish-build-target-vite-dts-helper.ts @@ -124,7 +124,7 @@ export const declarationOnlyDts = ({ async configResolved(config) { if ( realpathSync(config.root) !== root || - config.build.write === false || + !config.build.write || config.build.watch ) throw new Error( diff --git a/packages/oxlint-config/src/toolchain-changed.ts b/packages/oxlint-config/src/toolchain-changed.ts index fbcfb0f1..398ca636 100644 --- a/packages/oxlint-config/src/toolchain-changed.ts +++ b/packages/oxlint-config/src/toolchain-changed.ts @@ -1130,7 +1130,7 @@ const parseSnapshot = (snapshot: GitSnapshot): ParsedSnapshot => { } else if (name === ".npmrc") { for (const line of text.split(/\r?\n/)) { if (!/^\s*(?:use-node-version|node-version)\s*=/i.test(line)) continue; - if (/\$\{/.test(line)) + if (line.includes("${")) throw new Error("Unclassifiable npm Node selector"); const selector = /^\s*(?:use-node-version|node-version)\s*=\s*(.*?)\s*$/i.exec( diff --git a/packages/oxlint-config/src/toolchain-rust.test.ts b/packages/oxlint-config/src/toolchain-rust.test.ts index 09b52924..741c6586 100644 --- a/packages/oxlint-config/src/toolchain-rust.test.ts +++ b/packages/oxlint-config/src/toolchain-rust.test.ts @@ -562,7 +562,7 @@ test("Docker RUN and ENV instructions retain Rust pin enforcement", () => { const instructions = [ (pin: string) => `RUN rustup default ${pin}`, (pin: string) => `RUN --mount=type=cache,target=/cache cargo +${pin} test`, - (pin: string) => ["RUN rustup toolchain add " + "\\", pin].join("\n"), + (pin: string) => ["RUN rustup toolchain add \\", pin].join("\n"), (pin: string) => `RUN ["rustup", "default", "${pin}"]`, (pin: string) => `RUN ["sh", "-c", "rustup default ${pin}"]`, (pin: string) => `RUN [ -f Cargo.toml ] && cargo +${pin} test`, diff --git a/packages/oxlint-config/src/toolchain-workflow-trust.test.ts b/packages/oxlint-config/src/toolchain-workflow-trust.test.ts index 909a93e5..53a6a9cb 100644 --- a/packages/oxlint-config/src/toolchain-workflow-trust.test.ts +++ b/packages/oxlint-config/src/toolchain-workflow-trust.test.ts @@ -104,7 +104,7 @@ const interpreterCommands = [ `${interpreter} --import=./scripts/register.mjs --test`, `${interpreter} --loader=./scripts/loader.mjs --test`, `${interpreter} --input-type=module`, - `${interpreter}`, + interpreter, `${interpreter} --version --require=./scripts/register.cjs`, ]), ...["python", "python2", "python3", "python3.13"].flatMap((interpreter) => [ @@ -112,7 +112,7 @@ const interpreterCommands = [ `${interpreter} -m pytest`, `${interpreter} -c 'import unittest; unittest.main(module=None)'`, `${interpreter} -v`, - `${interpreter}`, + interpreter, `${interpreter} -`, `${interpreter} --version -m pytest`, ]), diff --git a/packages/oxlint-config/src/typecheck-probe.ts b/packages/oxlint-config/src/typecheck-probe.ts index 58fc8ec9..c40b1280 100644 --- a/packages/oxlint-config/src/typecheck-probe.ts +++ b/packages/oxlint-config/src/typecheck-probe.ts @@ -201,6 +201,7 @@ export const runTypecheckProbe = async ({ controller.signal.throwIfAborted(); let onAbort: (() => void) | undefined; const aborted = new Promise((_done, reject) => { + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- the abort reason is forwarded as given onAbort = () => reject(controller.signal.reason); controller.signal.addEventListener("abort", onAbort, { once: true }); }); diff --git a/packages/oxlint-plugin/src/no-raw-route-query-client.ts b/packages/oxlint-plugin/src/no-raw-route-query-client.ts index e03594f0..63540baa 100644 --- a/packages/oxlint-plugin/src/no-raw-route-query-client.ts +++ b/packages/oxlint-plugin/src/no-raw-route-query-client.ts @@ -1,5 +1,4 @@ -import { eslintCompatPlugin, type Plugin } from "@oxlint/plugins"; -import type { Ranged } from "@oxlint/plugins"; +import { eslintCompatPlugin, type Plugin, type Ranged } from "@oxlint/plugins"; import { getImportedName, getPropertyName, isIdentifier } from "./utils.ts"; @@ -236,7 +235,7 @@ const plugin: Plugin = eslintCompatPlugin({ if ( callee.type === "MemberExpression" && - callee.computed === false && + !callee.computed && isIdentifier(callee.object) && routeQueryHelperNamespaces.has(callee.object.name) ) { diff --git a/packages/oxlint-plugin/src/no-unsafe-inner-html.ts b/packages/oxlint-plugin/src/no-unsafe-inner-html.ts index e6037309..a007562c 100644 --- a/packages/oxlint-plugin/src/no-unsafe-inner-html.ts +++ b/packages/oxlint-plugin/src/no-unsafe-inner-html.ts @@ -34,8 +34,12 @@ // function can be named `sanitizeHtml`. Add explicit provenance at the sink // until the codebase has a branded SafeHtml boundary. -import type { Scope, Variable } from "@oxlint/plugins"; -import { eslintCompatPlugin, type Plugin } from "@oxlint/plugins"; +import { + eslintCompatPlugin, + type Plugin, + type Scope, + type Variable, +} from "@oxlint/plugins"; import type { AstNode, LiteralArgumentResolution } from "./utils.ts"; import { diff --git a/packages/oxlint-plugin/src/utils.ts b/packages/oxlint-plugin/src/utils.ts index 0ba8a20d..371dd83a 100644 --- a/packages/oxlint-plugin/src/utils.ts +++ b/packages/oxlint-plugin/src/utils.ts @@ -20,7 +20,7 @@ export const isAstNode = (node: unknown): node is AstNode => typeof node === "object" && node !== null && "type" in node && - typeof (node as { type: unknown }).type === "string" && + typeof node.type === "string" && "range" in node; export const isIdentifier = ( diff --git a/packages/test-kit/src/rejection.test.ts b/packages/test-kit/src/rejection.test.ts index c36dd548..a4afd2b5 100644 --- a/packages/test-kit/src/rejection.test.ts +++ b/packages/test-kit/src/rejection.test.ts @@ -7,6 +7,7 @@ describe("rejectionOf", () => { test("returns the exact reason a promise rejects with", async () => { const reason = new TypeError("bad input"); expect(await rejectionOf(Promise.reject(reason))).toBe(reason); + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- non-Error reasons must pass through unchanged expect(await rejectionOf(Promise.reject("plain string"))).toBe( "plain string", );