[P1][CR-T5] 引入 ESLint 最小规则集,用静态检查固化前后端分层、确定性与反静默兜底约束 #537

Open
opened 2026-09-29 06:44:02 +00:00 by troytt · 0 comments
Owner

来源:#516|优先级 P1|评审编号 T5|基线 e475c2e

问题描述

仓库目前只开了 tsc --noEmit,没有任何 Linter(ESLint / Biome)或 Formatter(package.json:54-61)。本次评审发现的多类结构性问题——后端反向 import 前端、模拟层混入 Math.random / Date.now / performance.now、未处理的悬空 Promise、any 泛滥、循环依赖、new Function——都是 TypeScript 编译器管不到、但一条 ESLint 规则就能在提交前拦住的。

如果不把这些边界写成自动化门禁,修完一轮后很快又会在新代码里回潮。

证据

  • package.json:54-61 的 devDependencies 只有 @types/node、@vitest/coverage-v8、tsx、typescript、vite、vitest。
  • ci.yml:22-25 只执行 npm run typecheck 和 npm test。
  • 缺乏静态规则直接对应的现存问题数量:
    • src/ 中显式 any 约 620 处(其中 skill-caster.ts 113 处、item-bridge.ts 71 处,见 #527);
    • 值级循环依赖 6 个强连通分量(最大一个横跨 17 个文件),以及 src/scene/backend/* 反向 import src/ui/*、src/render/*、src/scene/frontend/*(见 #526);
    • 模拟层中的 Math.random、Date.now、performance.now(见 #523);
    • 悬空 Promise 与静默 .catch(() => {})(见 #529、#530);
    • new Function 动态求值(见 #534);
    • 测试中的同名变量遮蔽与 vitestExpect(true).toBe(true)(见 #535)。

根因

项目早期以高吞吐移植功能和通过类型检查为首要目标,没有把 AGENTS.md 中的架构红线(前后端分层、1.13c 确定性、禁止静默兜底)转化为可执行的静态分析规则。

修复指南

使用 ESLint v9 Flat Config(eslint.config.mjs)+ typescript-eslint,只开高信噪比、直接对应本次评审问题的最小规则集,不做纯风格噪音检查:

规则 / 配置 作用域 对应子 issue 说明
no-restricted-imports src/game/**、src/scene/backend/**、src/sim/**、src/net/** #526 禁止后端/模拟层 import src/ui/**、src/render/**、src/scene/frontend/**、src/frontend/**
import/no-cycle(或 eslint-plugin-import-x) src/** #526 禁止新增模块级循环依赖
no-restricted-properties / no-restricted-globals src/game/**、src/scene/backend/**、src/net/** #523 禁止 Math.random、Date.now、performance.now(注:src/sim/loop.ts 的帧率计时属于合法用途,单独放到 src/sim/ 豁免)
@typescript-eslint/no-floating-promises、@typescript-eslint/no-misused-promises src/** #529、#530 强制所有异步加载链显式处理或 void 标注并接入生命周期
@typescript-eslint/no-explicit-any src/** #527 存量多的文件先按目录设为 warn 并逐步清零,已清零目录设为 error
no-new-func、no-eval src/** #534 禁止 new Function 与 eval
@typescript-eslint/no-shadow src/**、tests/** #535 拦截 tests/net.test.ts 里外层/内层同名 problems、checks 遮蔽
no-restricted-syntax(禁止 expect(true).toBe(true) / vitestExpect(true).toBe(true)) tests/** #535 防止空断言兜底再次合入
no-restricted-syntax(审计 ?? <NumericLiteral>) src/game/** #524 先作为 warn / 审计脚本运行,配合 #524 清理数据表缺失字段的静默魔法数字兜底

落地步骤:

  1. 安装 eslint、@eslint/js、typescript-eslint、eslint-plugin-import-x。
  2. 编写 eslint.config.mjs,对已经干净的目录直接开 error,对尚在迁移中的目录(如 #526、#527 还没拆完的文件)用精确到文件列表的 override 暂时标为 warn,每完成一个子 issue 就把对应文件从 override 列表移除。
  3. 在 package.json 中加入 "lint": "eslint src scripts tests",并写入 .gitea/workflows/ci.yml 与 AGENTS.md §5 质量门禁。

验收标准

  • npm run lint 可运行并纳入 package.json 与 .gitea/workflows/ci.yml。
  • 在 src/game/ 或 src/scene/backend/ 中尝试写 import ... from '../ui/...'、Math.random()、Date.now()、new Function(...) 或悬空 Promise 时,npm run lint 立即以 error 退出。
  • 在 tests/ 中尝试写 expect(true).toBe(true) 或遮蔽外层同名变量时,npm run lint 立即以 error 退出。
  • npm run typecheck 0 error;npm run lint 0 error;npx vitest run 全部通过。

相关

  • #526:前后端分层边界与循环依赖。
  • #523:模拟确定性。
  • #524:禁止静默数值兜底。
  • #527:消除 any。
  • #529、#530:悬空 Promise 与静默吞错。
  • #534:禁用 new Function。
  • #535:禁止空断言与变量遮蔽。
> 来源:#516|优先级 P1|评审编号 T5|基线 `e475c2e` ## 问题描述 仓库目前只开了 `tsc --noEmit`,没有任何 Linter(ESLint / Biome)或 Formatter([package.json:54-61](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/package.json#L54-L61))。本次评审发现的多类结构性问题——后端反向 import 前端、模拟层混入 `Math.random` / `Date.now` / `performance.now`、未处理的悬空 Promise、`any` 泛滥、循环依赖、`new Function`——都是 TypeScript 编译器管不到、但一条 ESLint 规则就能在提交前拦住的。 如果不把这些边界写成自动化门禁,修完一轮后很快又会在新代码里回潮。 ## 证据 - [package.json:54-61](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/package.json#L54-L61) 的 `devDependencies` 只有 `@types/node`、`@vitest/coverage-v8`、`tsx`、`typescript`、`vite`、`vitest`。 - [ci.yml:22-25](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/.gitea/workflows/ci.yml#L22-L25) 只执行 `npm run typecheck` 和 `npm test`。 - 缺乏静态规则直接对应的现存问题数量: - `src/` 中显式 `any` 约 **620 处**(其中 `skill-caster.ts` 113 处、`item-bridge.ts` 71 处,见 #527); - 值级循环依赖 **6 个强连通分量**(最大一个横跨 17 个文件),以及 `src/scene/backend/*` 反向 import `src/ui/*`、`src/render/*`、`src/scene/frontend/*`(见 #526); - 模拟层中的 `Math.random`、`Date.now`、`performance.now`(见 #523); - 悬空 Promise 与静默 `.catch(() => {})`(见 #529、#530); - `new Function` 动态求值(见 #534); - 测试中的同名变量遮蔽与 `vitestExpect(true).toBe(true)`(见 #535)。 ## 根因 项目早期以高吞吐移植功能和通过类型检查为首要目标,没有把 `AGENTS.md` 中的架构红线(前后端分层、1.13c 确定性、禁止静默兜底)转化为可执行的静态分析规则。 ## 修复指南 使用 ESLint v9 Flat Config(`eslint.config.mjs`)+ `typescript-eslint`,只开**高信噪比、直接对应本次评审问题**的最小规则集,不做纯风格噪音检查: | 规则 / 配置 | 作用域 | 对应子 issue | 说明 | |---|---|---|---| | `no-restricted-imports` | `src/game/**`、`src/scene/backend/**`、`src/sim/**`、`src/net/**` | #526 | 禁止后端/模拟层 import `src/ui/**`、`src/render/**`、`src/scene/frontend/**`、`src/frontend/**` | | `import/no-cycle`(或 `eslint-plugin-import-x`) | `src/**` | #526 | 禁止新增模块级循环依赖 | | `no-restricted-properties` / `no-restricted-globals` | `src/game/**`、`src/scene/backend/**`、`src/net/**` | #523 | 禁止 `Math.random`、`Date.now`、`performance.now`(注:`src/sim/loop.ts` 的帧率计时属于合法用途,单独放到 `src/sim/` 豁免) | | `@typescript-eslint/no-floating-promises`、`@typescript-eslint/no-misused-promises` | `src/**` | #529、#530 | 强制所有异步加载链显式处理或 `void` 标注并接入生命周期 | | `@typescript-eslint/no-explicit-any` | `src/**` | #527 | 存量多的文件先按目录设为 `warn` 并逐步清零,已清零目录设为 `error` | | `no-new-func`、`no-eval` | `src/**` | #534 | 禁止 `new Function` 与 `eval` | | `@typescript-eslint/no-shadow` | `src/**`、`tests/**` | #535 | 拦截 `tests/net.test.ts` 里外层/内层同名 `problems`、`checks` 遮蔽 | | `no-restricted-syntax`(禁止 `expect(true).toBe(true)` / `vitestExpect(true).toBe(true)`) | `tests/**` | #535 | 防止空断言兜底再次合入 | | `no-restricted-syntax`(审计 `?? <NumericLiteral>`) | `src/game/**` | #524 | 先作为 `warn` / 审计脚本运行,配合 #524 清理数据表缺失字段的静默魔法数字兜底 | 落地步骤: 1. 安装 `eslint`、`@eslint/js`、`typescript-eslint`、`eslint-plugin-import-x`。 2. 编写 `eslint.config.mjs`,对已经干净的目录直接开 `error`,对尚在迁移中的目录(如 #526、#527 还没拆完的文件)用精确到文件列表的 override 暂时标为 `warn`,每完成一个子 issue 就把对应文件从 override 列表移除。 3. 在 `package.json` 中加入 `"lint": "eslint src scripts tests"`,并写入 `.gitea/workflows/ci.yml` 与 `AGENTS.md` §5 质量门禁。 ## 验收标准 - [ ] `npm run lint` 可运行并纳入 `package.json` 与 `.gitea/workflows/ci.yml`。 - [ ] 在 `src/game/` 或 `src/scene/backend/` 中尝试写 `import ... from '../ui/...'`、`Math.random()`、`Date.now()`、`new Function(...)` 或悬空 Promise 时,`npm run lint` 立即以 `error` 退出。 - [ ] 在 `tests/` 中尝试写 `expect(true).toBe(true)` 或遮蔽外层同名变量时,`npm run lint` 立即以 `error` 退出。 - [ ] `npm run typecheck` 0 error;`npm run lint` 0 error;`npx vitest run` 全部通过。 ## 相关 - #526:前后端分层边界与循环依赖。 - #523:模拟确定性。 - #524:禁止静默数值兜底。 - #527:消除 `any`。 - #529、#530:悬空 Promise 与静默吞错。 - #534:禁用 `new Function`。 - #535:禁止空断言与变量遮蔽。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#537
No description provided.