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",
);