跳转至

空体 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 就是这样:母产品里插入走 insertManyinsertOne 是没填实现的空壳,迁移时一并搬来、漏清理。当时 src/ 下零调用没出事,但留着就是地雷。

静默失败比吵闹失败危险:宁可抛错,也别返回 undefined 假装成功。

标准操作步骤

检测什么(仅 src/ 的 .ts / .tsx)

花括号内只有空白 / 换行 / 注释的 async 函数,三形态 + 一附带:

  1. 箭头:async (...) => {}(含多行、参数类型注解、返回类型注解)
  2. function:async function foo(...) {} / async function* g() {}
  3. 方法:async foo(...) {} / async *gen() {}(含 static / public / private 前缀)
  4. 附带:单参无括号箭头 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,只拦新引入的空壳