空体 async 函数扫描 — 防迁移空壳静默丢数据¶
类型:rule(操作指南) 描述:防
async (...) => {}空壳静默丢数据;scan:empty-async-fn用法 + baseline + 豁免注释 最后更新:2026-05-31 触发场景:迁移整库 / 整模块;写或改 async 函数;pre-commit 报「新增空体 async 函数」 来源:v0.10.124 ISSUE-0086(insertOne从母产品迁移过来是空函数体async (table, value) => {},调用即静默返回 undefined)
什么时候用¶
- 场景 1:从母产品 / 其它仓库迁移整库或整模块进来时 —— 先跑一遍
pnpm scan:empty-async-fn,揪出搬过来没填实现的占位空壳。 - 场景 2:写 / 改任意 async 函数后 —— pre-commit 自动
--diff拦新增。 - 场景 3:pre-commit 报「❌ scan:empty-async-fn 发现新增空体 async 函数」时 —— 按下方决策树处理。
为什么空体 async 是 footgun¶
async (...) => {} 函数体为空时:
- 调用
await emptyFn(...)静默返回 undefined —— 不插数据、不抛错、不打日志。 - TypeScript 不报错(推断返回
Promise<void>,类型完全合法)。 - 调用方拿到「成功」假象,数据 / 副作用悄无声息地丢掉。
ISSUE-0086 的 insertOne 就是这样:母产品里插入走 insertMany,insertOne 是没填实现的空壳,迁移时一并搬来、漏清理。当时 src/ 下零调用没出事,但留着就是地雷。
静默失败比吵闹失败危险:宁可抛错,也别返回 undefined 假装成功。
标准操作步骤¶
检测什么(仅 src/ 的 .ts / .tsx)¶
花括号内只有空白 / 换行 / 注释的 async 函数,三形态 + 一附带:
- 箭头:
async (...) => {}(含多行、参数类型注解、返回类型注解) - function:
async function foo(...) {}/async function* g() {} - 方法:
async foo(...) {}/async *gen() {}(含 static / public / private 前缀) - 附带:单参无括号箭头
async x => {}
三模式(同 scan:mv3 / scan:react / scan:error-handling)¶
pnpm scan:empty-async-fn # 完整扫描(人工看清单)
pnpm scan:empty-async-fn -- --save-baseline # 当前命中存为基线
pnpm scan:empty-async-fn -- --diff # 只显示比基线新增(pre-commit 用)
pnpm scan:empty-async-fn -- --strict # 有命中即 exit 1(CI 用)
命中后怎么决策¶
flowchart TD
A[扫到空体 async 函数] --> B{该函数本应做事吗}
B -->|是, 如 insertOne 写库| C[补函数体, 别让调用静默拿 undefined]
B -->|否, 有意留空| D[体内加豁免注释 intentional noop]
D --> E[或一类合规模式 用 --save-baseline 重存]
豁免注释关键字(必须写在花括号体内才生效 —— 扫描器只读体内文本):
// intentional noop(自然语言)// SAFE: empty-async-ok — <理由>(本项目// SAFE:约定,对齐 count-window / nested-scroll)- 其它可识别词:
intentionally empty/no-op/empty on purpose
必备前置¶
python3(脚本无三方依赖)- 跑在仓库根(pre-commit 已自动
cd到根) - baseline 文件
.empty-async-fn-baseline.json提交到 git(全员共享同一基线)
易错点¶
- ⚠️ 豁免注释放错地方:必须在
{ ... }体内。放在const f = async () => {}的上一行无效(扫描器只读体内文本)。 - ⚠️ baseline 越攒越多:理想长期保持
hits: []。能就地加豁免注释的就别往 baseline 里塞 —— inline 注释自文档,baseline 是黑盒。 - ⚠️ 对象类型返回注解漏报:
async f(): { a: number } {}会被当作非空漏掉(fail-safe 偏漏不偏误报)。极罕见,不影响主用途。 - ⚠️ 正则字面量里的伪签名可能误报一次 → baseline 收编或就地加豁免注释即可。
完成后必做¶
- 更新
docs/rules/INDEX.md(本规则已登记) - 新增空体若属合规模式 → 跑
--save-baseline并提交 baseline - 真 footgun → 补实现 + 落 ISSUE
已知豁免清单(baseline 之外的 inline 标注)¶
| 文件 | 函数 | 为什么留空 |
|---|---|---|
src/sections/settings/view/settings-view.tsx |
onSubmit = handleSubmit(async () => {}) |
设置项已自动保存,FormProvider 表单提交本身无动作;体内 // SAFE: empty-async-ok 标注。注意:旧注释写「供重置按钮调用」实为误导,重置走的是 onReset |
相关¶
- ISSUE-0086(insertOne 空壳)—— 本扫描的起因
- fix-regression-defense —— 同库其它错误契约是 load-bearing,别「统一」(
countByQuery抛异常被去重逻辑依赖)
示例:端到端¶
# 1) 迁移完一个库,先全扫
pnpm scan:empty-async-fn
# → 列出所有 async (...) => {} 空壳
# 2) 逐个判定:
# - 该写实现的(如 insertOne)→ 补函数体
# - 有意留空的 → 体内加 // SAFE: empty-async-ok — <理由>
# 3) 全部处理完,存基线(理想是 0 处)
pnpm scan:empty-async-fn -- --save-baseline
git add .empty-async-fn-baseline.json
# 4) 此后 pre-commit 自动 --diff,只拦新引入的空壳