From 9e0b7180a9b4e9ff0527052af24b8fb97bfa31cb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:24:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=85=20reduce=20redundant=20eslint=20h?= =?UTF-8?q?arness=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint-rules/harness.test.mjs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 8eca8dc87..07a4348a0 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -176,20 +176,22 @@ describe("harness lint 规则", () => { it("放行同名普通函数和被局部变量遮蔽的导入名", () => { expect( ruleIdsAt( - `function waitFor(callback) { callback(); } const fireEvent = { click() {} }; waitFor(() => fireEvent.click(button));`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect( - ruleIdsAt( - `import { fireEvent as fe, waitFor } from "@testing-library/react"; function run(fe) { waitFor(() => fe.click(button)); }`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - - expect( - ruleIdsAt( - `import { fireEvent, waitFor } from "@testing-library/react"; fireEvent.click(button); waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, + ` + import { fireEvent, fireEvent as fe, waitFor } from "@testing-library/react"; + + function localCase() { + function waitFor(callback) { callback(); } + const fireEvent = { click() {} }; + waitFor(() => fireEvent.click(button)); + } + + function run(fe) { + waitFor(() => fe.click(button)); + } + + fireEvent.click(button); + waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); + `, "src/pages/example.test.tsx" ) ).not.toContain(RULE); From 5b8ed8968385ae2d7219e3afb492adcfe9f4e290 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:40:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20merge=20redundant=20ES?= =?UTF-8?q?Lint=20harness=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint-rules/harness.test.mjs | 144 +++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 63 deletions(-) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 07a4348a0..f43238234 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -40,13 +40,17 @@ describe("harness lint 规则", () => { }); it("拦截 i18n.t / i18next.t 的 defaultValue", () => { - expect(ruleIdsAt(`i18n.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`i18next.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + const ids = ruleIdsAt( + `i18n.t("k", { defaultValue: "x" }); i18next.t("k", { defaultValue: "x" });`, + "src/pages/foo.tsx" + ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("放行不带 defaultValue 的 t() 调用", () => { - expect(ruleIdsAt(`const x = t("ns:a.b");`, "src/pages/foo.tsx")).not.toContain(RULE); - expect(ruleIdsAt(`const x = t("ns:a.b", { count: 1 });`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleIdsAt(`const x = t("ns:a.b"); const y = t("ns:a.b", { count: 1 });`, "src/pages/foo.tsx") + ).not.toContain(RULE); }); it("不误伤组件的 defaultValue 属性(如 )", () => { @@ -58,8 +62,11 @@ describe("harness lint 规则", () => { }); it("拦截可选链 / computed 成员调用中的 defaultValue", () => { - expect(ruleIdsAt(`i18n?.t("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`i18next["t"]("k", { defaultValue: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + const ids = ruleIdsAt( + `i18n?.t("k", { defaultValue: "x" }); i18next["t"]("k", { defaultValue: "x" });`, + "src/pages/foo.tsx" + ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("拦截 computed defaultValue key", () => { @@ -106,10 +113,10 @@ describe("harness lint 规则", () => { it("放行设计令牌 bg-background / text-foreground / bg-card", () => { expect( - ruleIdsAt(`const e =
;`, "src/pages/foo.tsx") - ).not.toContain(RULE); - expect( - ruleIdsAt(`const e =
;`, "src/pages/foo.tsx") + ruleIdsAt( + `const e =
; const f =
;`, + "src/pages/foo.tsx" + ) ).not.toContain(RULE); }); @@ -118,15 +125,19 @@ describe("harness lint 规则", () => { }); it("拦截非灰阶 Tailwind 色板(text-red-500 / bg-blue-600)", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + const ids = ruleIdsAt( + `const e =
; const f =
;`, + "src/pages/foo.tsx" + ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("拦截带变体和 opacity 后缀的原始色板", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain( - RULE + const ids = ruleIdsAt( + `const e =
; const f =
;`, + "src/pages/foo.tsx" ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("拦截 cn() 内的非灰阶原始颜色", () => { @@ -211,30 +222,31 @@ describe("harness lint 规则", () => { }); it("拦截 waitFor 导入别名并放行同名普通函数", () => { - expect( - ruleIdsAt( - `import { waitFor as until } from "@testing-library/react"; until(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); - expect( - ruleIdsAt( - `function waitFor(callback) { callback(); } waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + const ids = ruleIdsAt( + ` + import { waitFor, waitFor as until } from "@testing-library/react"; + until(() => expect(screen.getByText("done")).toBeInTheDocument()); + + function localCase() { + function waitFor(callback) { callback(); } + waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); + } + `, + "src/pages/example.test.tsx" + ); + expect(ids.filter((id) => id === RULE)).toHaveLength(1); }); it("放行多断言和非存在性断言", () => { expect( ruleIdsAt( - `waitFor(() => { expect(screen.getByText("done")).toBeInTheDocument(); expect(api).toHaveBeenCalled(); });`, + ` + waitFor(() => { expect(screen.getByText("done")).toBeInTheDocument(); expect(api).toHaveBeenCalled(); }); + waitFor(() => expect(screen.getByRole("button")).toBeEnabled()); + `, "src/pages/example.test.tsx" ) ).not.toContain(RULE); - expect( - ruleIdsAt(`waitFor(() => expect(screen.getByRole("button")).toBeEnabled());`, "src/pages/example.test.tsx") - ).not.toContain(RULE); }); }); @@ -243,12 +255,11 @@ describe("harness lint 规则", () => { it("拦截 Playwright waitForTimeout 和 timer Promise", () => { expect(ruleIdsAt(`await page.waitForTimeout(500);`, "e2e/example.spec.ts")).toContain(RULE); - expect( - ruleIdsAt(`await new Promise((resolve) => setTimeout(resolve, 0));`, "src/pages/example.test.tsx") - ).toContain(RULE); - expect( - ruleIdsAt(`await new Promise((resolve) => setTimeout(() => resolve(), 0));`, "src/pages/example.test.tsx") - ).toContain(RULE); + const ids = ruleIdsAt( + `await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(() => resolve(), 0));`, + "src/pages/example.test.tsx" + ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("放行带有逐处豁免注释的时序让步和其他 setTimeout", () => { @@ -266,37 +277,44 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-test-large-boundary-fixture"; it("拦截显式的一页以上分页边界写法及其 const 别名", () => { - expect( - ruleIdsAt( - `const total = NETWORK_RULES_PAGE_SIZE + 1; const rows = Array.from({ length: total }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); - expect(ruleIdsAt(`Array.from({ length: PAGE_ROWS + 1 }, makeRow);`, "src/pages/example.test.tsx")).toContain( - RULE + const ids = ruleIdsAt( + ` + const total = NETWORK_RULES_PAGE_SIZE + 1; + const rows = Array.from({ length: total }, makeRow); + Array.from({ length: PAGE_ROWS + 1 }, makeRow); + `, + "src/pages/example.test.tsx" ); + expect(ids.filter((id) => id === RULE)).toHaveLength(2); }); it("放行非边界夹具和非页面测试", () => { - expect(ruleIdsAt(`Array.from({ length: 20 }, makeRow);`, "src/pages/example.test.tsx")).not.toContain(RULE); - expect(ruleIdsAt(`Array.from({ length: PAGE_SIZE + 2 }, makeRow);`, "src/pages/example.test.tsx")).not.toContain( - RULE - ); - expect(ruleIdsAt(`Array.from({ length: itemCount + 1 }, makeItem);`, "src/pages/example.test.tsx")).not.toContain( - RULE - ); + expect( + ruleIdsAt( + ` + Array.from({ length: 20 }, makeRow); + Array.from({ length: PAGE_SIZE + 2 }, makeRow); + Array.from({ length: itemCount + 1 }, makeItem); + `, + "src/pages/example.test.tsx" + ) + ).not.toContain(RULE); expect(ruleIdsAt(`Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, "src/pkg/example.test.ts")).not.toContain( RULE ); }); it("只追踪 const 的单级别名", () => { - expect( - ruleIdsAt(`let total = PAGE_SIZE + 1; Array.from({ length: total }, makeRow);`, "src/pages/example.test.tsx") - ).not.toContain(RULE); expect( ruleIdsAt( - `const total = PAGE_SIZE + 1; const count = total; Array.from({ length: count }, makeRow);`, + ` + let total = PAGE_SIZE + 1; + Array.from({ length: total }, makeRow); + + const aliasedTotal = PAGE_SIZE + 1; + const count = aliasedTotal; + Array.from({ length: count }, makeRow); + `, "src/pages/example.test.tsx" ) ).not.toContain(RULE); @@ -305,13 +323,13 @@ describe("harness lint 规则", () => { it("放行词法遮蔽和逐处说明的边界夹具", () => { expect( ruleIdsAt( - `const Array = { from() {} }; Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect( - ruleIdsAt( - `// eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary\nArray.from({ length: PAGE_SIZE + 1 }, makeRow);`, + ` + const Array = { from() {} }; + Array.from({ length: PAGE_SIZE + 1 }, makeRow); + + // eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary + Array.from({ length: PAGE_SIZE + 1 }, makeRow); + `, "src/pages/example.test.tsx" ) ).not.toContain(RULE); From e254334639065fb53acde6e45edfc57b4e6f488e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:07:13 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E7=BB=9F=E4=B8=80=20E?= =?UTF-8?q?SLint=20harness=20=E8=A7=84=E5=88=99=E8=AF=8A=E6=96=AD=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint-rules/harness.test.mjs | 392 +++++++++++++++++++++++----------- 1 file changed, 268 insertions(+), 124 deletions(-) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index f43238234..14c359002 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -15,7 +15,7 @@ const syntaxOnlyConfig = config.filter((entry) => !entry?.languageOptions?.parse const linter = new Linter({ configType: "flat" }); /** 在指定文件路径下 lint 一段代码,返回非致命诊断的 ruleId 列表;解析失败直接抛出。 */ -function ruleIdsAt(code, filename) { +function ruleIdsAt({ code, filename }) { const messages = linter.verify(code, syntaxOnlyConfig, { filename }); const fatal = messages.find((m) => m.fatal); if (fatal) { @@ -24,57 +24,99 @@ function ruleIdsAt(code, filename) { return messages.map((m) => m.ruleId); } +/** 返回指定 ruleId 的诊断数量。 */ +function ruleCountAt({ code, filename, ruleId }) { + return ruleIdsAt({ code, filename }).filter((id) => id === ruleId).length; +} + describe("harness lint 规则", () => { // 首次 linter.verify 需解析整套 flat config(typescript-eslint 解析器 + react 插件),冷启动开销大; // 在 beforeAll 预热一次(走 hook 超时而非 850ms 测试超时),避免这次一次性成本偶发落到某个 it 里超时。 beforeAll(() => { - ruleIdsAt(`const x = 1;`, "src/pages/foo.tsx"); + ruleIdsAt({ code: `const x = 1;`, filename: "src/pages/foo.tsx" }); }); describe("① scriptcat/no-i18n-default-value:禁止 t() 内联 defaultValue 兜底", () => { const RULE = "scriptcat/no-i18n-default-value"; it("拦截 t(key, { defaultValue })", () => { - const ids = ruleIdsAt(`const x = t("ns:a.b", { defaultValue: "中文兜底" });`, "src/pages/foo.tsx"); - expect(ids).toContain(RULE); + expect( + ruleCountAt({ + code: `const x = t("ns:a.b", { defaultValue: "中文兜底" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 i18n.t / i18next.t 的 defaultValue", () => { - const ids = ruleIdsAt( - `i18n.t("k", { defaultValue: "x" }); i18next.t("k", { defaultValue: "x" });`, - "src/pages/foo.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + expect( + ruleCountAt({ + code: `i18n.t("k", { defaultValue: "x" }); i18next.t("k", { defaultValue: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行不带 defaultValue 的 t() 调用", () => { expect( - ruleIdsAt(`const x = t("ns:a.b"); const y = t("ns:a.b", { count: 1 });`, "src/pages/foo.tsx") - ).not.toContain(RULE); + ruleCountAt({ + code: `const x = t("ns:a.b"); const y = t("ns:a.b", { count: 1 });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("不误伤组件的 defaultValue 属性(如 )", () => { - expect(ruleIdsAt(`const e = ;`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `const e = ;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("不误伤非 i18n 的成员调用(如 GM_getValue 第二参对象)", () => { - expect(ruleIdsAt(`store.getValue("k", { defaultValue: 1 });`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `store.getValue("k", { defaultValue: 1 });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("拦截可选链 / computed 成员调用中的 defaultValue", () => { - const ids = ruleIdsAt( - `i18n?.t("k", { defaultValue: "x" }); i18next["t"]("k", { defaultValue: "x" });`, - "src/pages/foo.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + expect( + ruleCountAt({ + code: `i18n?.t("k", { defaultValue: "x" }); i18next["t"]("k", { defaultValue: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截 computed defaultValue key", () => { - expect(ruleIdsAt(`t("k", { ["defaultValue"]: "x" });`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `t("k", { ["defaultValue"]: "x" });`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("明确只禁止 defaultValue 对象兜底,不拦截非对象第二参", () => { - expect(ruleIdsAt(`t("k", "fallback text");`, "src/pages/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `t("k", "fallback text");`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -82,13 +124,23 @@ describe("harness lint 规则", () => { const RULE = "no-restricted-imports"; it("拦截从 @radix-ui/react-* 单包导入", () => { - const ids = ruleIdsAt(`import { Dialog } from "@radix-ui/react-dialog";`, "src/pages/components/ui/dialog.tsx"); - expect(ids).toContain(RULE); + expect( + ruleCountAt({ + code: `import { Dialog } from "@radix-ui/react-dialog";`, + filename: "src/pages/components/ui/dialog.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行从合并包 radix-ui 导入", () => { - const ids = ruleIdsAt(`import { Dialog } from "radix-ui";`, "src/pages/components/ui/dialog.tsx"); - expect(ids).not.toContain(RULE); + expect( + ruleCountAt({ + code: `import { Dialog } from "radix-ui";`, + filename: "src/pages/components/ui/dialog.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -96,52 +148,93 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-raw-color-classname"; it("拦截 bg-white 等原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截带变体前缀的原始颜色(dark:bg-gray-800)", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截任意值十六进制颜色 bg-[#fff]", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 cn() 内的原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行设计令牌 bg-background / text-foreground / bg-card", () => { expect( - ruleIdsAt( - `const e =
; const f =
;`, - "src/pages/foo.tsx" - ) - ).not.toContain(RULE); + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("仅作用于 src/pages(src/app 不受限)", () => { - expect(ruleIdsAt(`const e =
;`, "src/app/foo.tsx")).not.toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/app/foo.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("拦截非灰阶 Tailwind 色板(text-red-500 / bg-blue-600)", () => { - const ids = ruleIdsAt( - `const e =
; const f =
;`, - "src/pages/foo.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + expect( + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截带变体和 opacity 后缀的原始色板", () => { - const ids = ruleIdsAt( - `const e =
; const f =
;`, - "src/pages/foo.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + expect( + ruleCountAt({ + code: `const e =
; const f =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("拦截 cn() 内的非灰阶原始颜色", () => { - expect(ruleIdsAt(`const e =
;`, "src/pages/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const e =
;`, + filename: "src/pages/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); }); @@ -150,44 +243,48 @@ describe("harness lint 规则", () => { it("拦截 fireEvent 及其导入别名", () => { expect( - ruleIdsAt( - `import { fireEvent as fe, waitFor } from "@testing-library/react"; waitFor(() => fe.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { fireEvent as fe, waitFor } from "@testing-library/react"; waitFor(() => fe.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 userEvent 实例交互", () => { expect( - ruleIdsAt( - `import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const user = userEvent.setup(); waitFor(() => user.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const user = userEvent.setup(); waitFor(() => user.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 userEvent 导入别名实例交互", () => { expect( - ruleIdsAt( - `import { userEvent as ue } from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const u = ue.setup(); waitFor(() => u.type(input, "x"));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { userEvent as ue } from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; const u = ue.setup(); waitFor(() => u.type(input, "x"));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 waitFor 导入别名", () => { expect( - ruleIdsAt( - `import { fireEvent, waitFor as until } from "@testing-library/react"; until(() => fireEvent.click(button));`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { fireEvent, waitFor as until } from "@testing-library/react"; until(() => fireEvent.click(button));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行同名普通函数和被局部变量遮蔽的导入名", () => { expect( - ruleIdsAt( - ` + ruleCountAt({ + code: ` import { fireEvent, fireEvent as fe, waitFor } from "@testing-library/react"; function localCase() { @@ -203,9 +300,10 @@ describe("harness lint 规则", () => { fireEvent.click(button); waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); `, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -214,16 +312,18 @@ describe("harness lint 规则", () => { it("拦截仅包装 getBy 存在性断言的 waitFor", () => { expect( - ruleIdsAt( - `import { waitFor } from "@testing-library/react"; waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, - "src/pages/example.test.tsx" - ) - ).toContain(RULE); + ruleCountAt({ + code: `import { waitFor } from "@testing-library/react"; waitFor(() => expect(screen.getByText("done")).toBeInTheDocument());`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 waitFor 导入别名并放行同名普通函数", () => { - const ids = ruleIdsAt( - ` + expect( + ruleCountAt({ + code: ` import { waitFor, waitFor as until } from "@testing-library/react"; until(() => expect(screen.getByText("done")).toBeInTheDocument()); @@ -232,21 +332,23 @@ describe("harness lint 规则", () => { waitFor(() => expect(screen.getByText("done")).toBeInTheDocument()); } `, - "src/pages/example.test.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(1); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行多断言和非存在性断言", () => { expect( - ruleIdsAt( - ` + ruleCountAt({ + code: ` waitFor(() => { expect(screen.getByText("done")).toBeInTheDocument(); expect(api).toHaveBeenCalled(); }); waitFor(() => expect(screen.getByRole("button")).toBeEnabled()); `, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -254,22 +356,37 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-test-fixed-sleep"; it("拦截 Playwright waitForTimeout 和 timer Promise", () => { - expect(ruleIdsAt(`await page.waitForTimeout(500);`, "e2e/example.spec.ts")).toContain(RULE); - const ids = ruleIdsAt( - `await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(() => resolve(), 0));`, - "src/pages/example.test.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + expect( + ruleCountAt({ + code: `await page.waitForTimeout(500);`, + filename: "e2e/example.spec.ts", + ruleId: RULE, + }) + ).toBe(1); + expect( + ruleCountAt({ + code: `await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(() => resolve(), 0));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行带有逐处豁免注释的时序让步和其他 setTimeout", () => { expect( - ruleIdsAt( - `// eslint-disable-next-line scriptcat/no-test-fixed-sleep -- listener registration contract\nawait new Promise((resolve) => setTimeout(resolve, 0));`, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect(ruleIdsAt(`setTimeout(tick, 10);`, "e2e/example.spec.ts")).not.toContain(RULE); + ruleCountAt({ + code: `// eslint-disable-next-line scriptcat/no-test-fixed-sleep -- listener registration contract\nawait new Promise((resolve) => setTimeout(resolve, 0));`, + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); + expect( + ruleCountAt({ + code: `setTimeout(tick, 10);`, + filename: "e2e/example.spec.ts", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -277,37 +394,44 @@ describe("harness lint 规则", () => { const RULE = "scriptcat/no-test-large-boundary-fixture"; it("拦截显式的一页以上分页边界写法及其 const 别名", () => { - const ids = ruleIdsAt( - ` + expect( + ruleCountAt({ + code: ` const total = NETWORK_RULES_PAGE_SIZE + 1; const rows = Array.from({ length: total }, makeRow); Array.from({ length: PAGE_ROWS + 1 }, makeRow); `, - "src/pages/example.test.tsx" - ); - expect(ids.filter((id) => id === RULE)).toHaveLength(2); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(2); }); it("放行非边界夹具和非页面测试", () => { expect( - ruleIdsAt( - ` + ruleCountAt({ + code: ` Array.from({ length: 20 }, makeRow); Array.from({ length: PAGE_SIZE + 2 }, makeRow); Array.from({ length: itemCount + 1 }, makeItem); `, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); - expect(ruleIdsAt(`Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, "src/pkg/example.test.ts")).not.toContain( - RULE - ); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); + expect( + ruleCountAt({ + code: `Array.from({ length: PAGE_SIZE + 1 }, makeRow);`, + filename: "src/pkg/example.test.ts", + ruleId: RULE, + }) + ).toBe(0); }); it("只追踪 const 的单级别名", () => { expect( - ruleIdsAt( - ` + ruleCountAt({ + code: ` let total = PAGE_SIZE + 1; Array.from({ length: total }, makeRow); @@ -315,24 +439,26 @@ describe("harness lint 规则", () => { const count = aliasedTotal; Array.from({ length: count }, makeRow); `, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); it("放行词法遮蔽和逐处说明的边界夹具", () => { expect( - ruleIdsAt( - ` + ruleCountAt({ + code: ` const Array = { from() {} }; Array.from({ length: PAGE_SIZE + 1 }, makeRow); // eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary Array.from({ length: PAGE_SIZE + 1 }, makeRow); `, - "src/pages/example.test.tsx" - ) - ).not.toContain(RULE); + filename: "src/pages/example.test.tsx", + ruleId: RULE, + }) + ).toBe(0); }); }); @@ -341,21 +467,39 @@ describe("harness lint 规则", () => { it("拦截 ui 组件里的 forwardRef(...)", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/pages/components/ui/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code, + filename: "src/pages/components/ui/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 ui 组件里的 React.forwardRef(...)", () => { - expect(ruleIdsAt(`const C = React.forwardRef(() => null);`, "src/pages/components/ui/foo.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code: `const C = React.forwardRef(() => null);`, + filename: "src/pages/components/ui/foo.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("拦截 ui 目录之外的页面级组件使用 forwardRef", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/pages/options/routes/ScriptList/components.tsx")).toContain(RULE); + expect( + ruleCountAt({ + code, + filename: "src/pages/options/routes/ScriptList/components.tsx", + ruleId: RULE, + }) + ).toBe(1); }); it("放行 src/pages 之外的组件使用 forwardRef", () => { const code = `import { forwardRef } from "react"; const C = forwardRef(() => null);`; - expect(ruleIdsAt(code, "src/app/foo.tsx")).not.toContain(RULE); + expect(ruleCountAt({ code, filename: "src/app/foo.tsx", ruleId: RULE })).toBe(0); }); }); }); From aaaadb8b977aa4db305602c3af8d009f5cb5bb6a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:12:21 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=84=20=E8=A1=A5=E5=85=85=20ESLint?= =?UTF-8?q?=20harness=20=E8=BE=85=E5=8A=A9=E5=87=BD=E6=95=B0=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint-rules/harness.test.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 14c359002..1ccf7889d 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -14,7 +14,11 @@ const syntaxOnlyConfig = config.filter((entry) => !entry?.languageOptions?.parse const linter = new Linter({ configType: "flat" }); -/** 在指定文件路径下 lint 一段代码,返回非致命诊断的 ruleId 列表;解析失败直接抛出。 */ +/** + * 用真实 ESLint flat config 检查虚拟文件,返回非致命诊断的 ruleId 列表。 + * `filename` 决定配置作用域;解析失败会抛出异常,避免无效夹具伪装成规则放行。 + * Vitest 用例用 `ruleCountAt({ code, filename, ruleId })` 断言命中数量,并先合并同名虚拟文件的代码片段。 + */ function ruleIdsAt({ code, filename }) { const messages = linter.verify(code, syntaxOnlyConfig, { filename }); const fatal = messages.find((m) => m.fatal); From f6981483cc9981454e45e01c07c6010e1122ba49 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:44:32 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E9=9D=9E=E6=B3=95=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint-rules/harness.test.mjs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 1ccf7889d..0f437d0fb 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -14,12 +14,45 @@ const syntaxOnlyConfig = config.filter((entry) => !entry?.languageOptions?.parse const linter = new Linter({ configType: "flat" }); +const sourceFilenamePattern = /\.(?:[cm]?js|[jt]sx?)$/; + +const configuredRuleIds = new Set(syntaxOnlyConfig.flatMap((entry) => Object.keys(entry?.rules ?? {}))); + +function assertFixtureCode(code) { + if (typeof code !== "string") { + throw new TypeError("code must be a string"); + } +} + +function assertFixtureFilename(filename) { + if (typeof filename !== "string") { + throw new TypeError("filename must be a string"); + } + + const trimmed = filename.trim(); + if (trimmed !== filename || trimmed === "" || !sourceFilenamePattern.test(filename)) { + throw new TypeError(`filename must be a non-empty source filename: ${filename}`); + } +} + +function assertRuleId(ruleId) { + if (typeof ruleId !== "string" || ruleId.trim() === "") { + throw new TypeError("ruleId must be a non-empty string"); + } + if (!configuredRuleIds.has(ruleId)) { + throw new Error(`Unknown configured ruleId: ${ruleId}`); + } +} + /** * 用真实 ESLint flat config 检查虚拟文件,返回非致命诊断的 ruleId 列表。 * `filename` 决定配置作用域;解析失败会抛出异常,避免无效夹具伪装成规则放行。 * Vitest 用例用 `ruleCountAt({ code, filename, ruleId })` 断言命中数量,并先合并同名虚拟文件的代码片段。 */ function ruleIdsAt({ code, filename }) { + // 非法参数直接报错 + assertFixtureCode(code); + assertFixtureFilename(filename); const messages = linter.verify(code, syntaxOnlyConfig, { filename }); const fatal = messages.find((m) => m.fatal); if (fatal) { @@ -30,6 +63,8 @@ function ruleIdsAt({ code, filename }) { /** 返回指定 ruleId 的诊断数量。 */ function ruleCountAt({ code, filename, ruleId }) { + // 非法参数直接报错 + assertRuleId(ruleId); return ruleIdsAt({ code, filename }).filter((id) => id === ruleId).length; } From 1334177be3682d2f509d7ce712a4fa6d5bca7c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 28 Sep 2026 16:44:27 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=85=20=E4=BF=AE=E6=AD=A3=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E5=A4=B9=E5=85=B7=E5=90=88=E5=B9=B6=E5=90=8E=E8=B1=81?= =?UTF-8?q?=E5=85=8D=E6=B3=A8=E9=87=8A=E7=94=A8=E4=BE=8B=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并夹具时顶层 const Array 遮蔽了下方带 eslint-disable 注释的 Array.from,导致删掉豁免注释用例仍通过;把遮蔽用例移入独立函数作用域,两个分支各自可被变异检出。同时删去复述代码的注释。 --- eslint-rules/harness.test.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint-rules/harness.test.mjs b/eslint-rules/harness.test.mjs index 0f437d0fb..9aeadfe47 100644 --- a/eslint-rules/harness.test.mjs +++ b/eslint-rules/harness.test.mjs @@ -50,7 +50,6 @@ function assertRuleId(ruleId) { * Vitest 用例用 `ruleCountAt({ code, filename, ruleId })` 断言命中数量,并先合并同名虚拟文件的代码片段。 */ function ruleIdsAt({ code, filename }) { - // 非法参数直接报错 assertFixtureCode(code); assertFixtureFilename(filename); const messages = linter.verify(code, syntaxOnlyConfig, { filename }); @@ -63,7 +62,6 @@ function ruleIdsAt({ code, filename }) { /** 返回指定 ruleId 的诊断数量。 */ function ruleCountAt({ code, filename, ruleId }) { - // 非法参数直接报错 assertRuleId(ruleId); return ruleIdsAt({ code, filename }).filter((id) => id === ruleId).length; } @@ -488,8 +486,10 @@ describe("harness lint 规则", () => { expect( ruleCountAt({ code: ` - const Array = { from() {} }; - Array.from({ length: PAGE_SIZE + 1 }, makeRow); + function shadowedArrayCase() { + const Array = { from() {} }; + Array.from({ length: PAGE_SIZE + 1 }, makeRow); + } // eslint-disable-next-line scriptcat/no-test-large-boundary-fixture -- pagination boundary Array.from({ length: PAGE_SIZE + 1 }, makeRow);