Files
STM32F4-Base/docs/门禁说明/bom-guard.md
T
wandering1 fa80b17d5f docs(门禁说明): 补 bom-guard 与 setview-check 两篇详解
票面:task-20 合入后 README 索引上挂着我名的两篇(Lead 指定挂批 4 MR)。按本目录八段结构写,
「拦下过什么真事」一栏用同日实测,作者自己的也算。

- bom-guard.md:立项依据是 2026-10-25 一天内 5 次(道E 4 次 + 道B 1 次);症状 #870-D / #8 /
  #274 一类**假语法错**,位置不指向真因;MSVC 与既有钩子全照不出来;"人工逐文件比 HEAD"只防得
  住记得做的人。两条刻意不报的边界(新文件无 BOM、反向加 BOM)+ 一条刻意不做(不自动补,整文件
  重写会把可修的丢 BOM 变成不可修的 U+FFFD)+ 三处互指防漂移,各写清理由。也记了本门禁自己的
  立项提交被 gitlint 拦过(正文一行 92>80,量出来仍按了提交)——这一栏只挑别人的事写就等于暗示
  写门禁的人不需要门禁。
- setview-check.md:立论点是"分层边界制造的物理盲区"——BSP 不得 include App 头,行表 id 只能是
  裸数字 + 注名,编译期无法自证;像素断言又只看可见 8 行,40 项分 5 页,错位一行时第 1 页可能
  看着完全正常。另收两条脚本自身实测坑:\w+ 吃不下带引号的单位导致**整行静默跳过**(故加行数
  硬闸)、打印中文时 CP936 半路崩(门禁脚本崩在半路 = 一条都没检查)。退出码 1 与 2 分开:把
  "我没读懂"报成"你写错了"是检查类脚本最坏的一种失败。
- README.md:索引加两行;"那 46 个钩子" → 48、"40 个 hook → 23 篇" → 42 → 25 篇(不同步就是在
  让索引说谎)。AGENTS 里"两篇待补"那句已过期,但 AGENTS 是单写者文件,留给 Lead 收。

跳过的门禁(三条同族):mem-numbers / mem-budget / size-gate(L0c),与本笔无关,等台账回填。
2026-10-25 15:33:46 +08:00

5.9 KiB
Raw Blame History

bom-guard — 带 BOM 文件被剥掉即拦(防"假语法错")

一句话:编辑工具会静默剥掉文件原有的 UTF-8 BOM,ARMCC 于是换了一套解码假设,报出来的 却是 #870-D / #8 / #274 这类语法错——错的位置和内容都不指向真因。这条门禁让"补回 3 个字节"这件事在提交时就必须处理,而不是等下一个人构建失败再去猜。

1. 它在查什么

对 pre-commit 传来的、落在名单内的文件,比较 HEAD 版本与工作区版本的前 3 字节: HEAD 是 EF BB BF 而工作区不是 → FAIL 并列出文件与补救动作。只此一条判定,没有存量豁免。

名单(三处互指,见 §6.2):Drivers/BSP/UI/**、App/task/ui_task.c、App/cfg/cfg_file.c、 Lib/lv_conf.h。

2. 为什么要它(拦下过什么真事)

  • 立项直接来自一天内 5 次踩坑:2026-10-25 道E 在 task-15(UI 页面批次)里连踩 4 次, 道B 在批 1 踩过 1 次。每次的症状都一样:改完 UI 源文件、@build 突然报一串语法错,而改动 本身是加注释或改常量;MDK-ARM/build_log.txt 里的行号指向中文注释所在的上一行, 照着报错去查会查不出任何东西。真因只是文件开头少了 3 个字节。
  • 别的门禁和编译器都不会响:MSVC(模拟器侧)带不带 BOM 都编得过、0 警告;no-mojibake 查的是 U+FFFD(已经损坏的痕迹),BOM 刚丢时文件字节完全合法,它看不见;coding-standard 也不看编码。也就是说,这条坑当时唯一的暴露点是"重编该文件",而历史 0 警告不代表安全。
  • 人工审计只防得住记得做的人:立项前道E 的做法是每次提交前逐文件比 HEAD 查 BOM, 四次里有一次是靠"看图发现满屏横杠"顺手查的,不是靠纪律——纪律型检查不成立(同类口径见 docs/代码规范/pre-commit使用说明.md §5.5)。
  • 顺带一条同族事实(写在这里而不是别处):本门禁自己的立项提交被 gitlint 拦过一次—— 正文有一行 92 > 80,作者量出来了仍然按了提交。「拦下过什么真事」这一栏如果只挑别人的事写, 就等于在暗示写门禁的人不需要门禁,那是不成立的。

3. 触发面

.pre-commit-config.yaml 里 bom-guard 的 files 正则(等价于 §1 名单)。不在名单内的文件 不报也不提示——比如有人在 App/log/ 新建带 BOM 的文件,本门禁不管(那种文件按约定就不该 带 BOM;真带上了,编译器会替我们响)。

⚠ files 不命中时钩子是 Skipped,Skipped ≠ Passed:一次只改文档的提交里看不到它变绿 是正常的,不代表它检查过了。

4. 报错怎么读 / 单独跑

# 常规由 pre-commit 自动跑;人工核全仓(含没改过的文件):
& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --all
# 自证判定(临时 git 仓库,含两条边界用例):
& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --selftest

报错会点名文件并给补救动作:只把 3 个字节 EF BB BF 补回文件最前面。不要整文件重写 (Set-Content / 脚本按系统默认编码重打中文会把"可修的丢 BOM"变成"不可修的 U+FFFD", 那是 no-mojibake 都救不回来的损坏)。

5. 好处

  1. 把一类"只有重编译才暴露、且报错完全误导人"的缺陷挡在提交点,且提示直接给出正确修法;
  2. 逼着 BOM 名单保持单一口径(§6.2 的三处互指)——名单一旦散着改,本门禁立刻红;
  3. 判定极简(两个 3 字节比较),几乎不会有误报,也就不容易被 --no-verify 搪塞过去。

6. 限制、盲区与刻意的不判定

6.1 只查"有 → 无"这一个方向

"HEAD 无 BOM、工作区加了 BOM" 不报。那是另一个方向的改动,可能是有意的(把某个文件纳入 BOM 名单),也可能是无意的;报红只会逼人 --no-verify,而养成绕门禁的习惯比漏检一次贵得多。 "新增文件没有 BOM" 同样不报:名单里的新文件(如新建 ui_page_xxx.c)由创建者负责加 BOM, 漏加会在 @build 时以真错误暴露——本门禁不假装自己能替代那一步。

6.2 名单三处互指(防漂移)

AGENTS 铁律第 2 条的名单、钩子 files 正则、脚本内 BOM_LIST 必须一致;脚本每次运行都核一次, 不一致即 FAIL。比较时会先脱掉正则里的反斜杠与锚点——核的是意图一致,不是字节相同,否则 App/task/ui_task\.c$ 会被误判成"没覆盖 App/task/ui_task.c"。

6.3 不校验文件是否真的含非 ASCII

判定只看编码标记,不看内容。一个纯 ASCII 的名单内文件带不带 BOM 都不影响编译,本门禁不替 编译器做这种优化判断——它宁可少管,也不要给出"可以不带 BOM"的例外口径(例外一旦被记住, 就会被套用到带中文的文件上)。

6.4 绕过与提交后不可见

--no-verify 可以直接跳过;合入主干之后再手工删 BOM,要等下一次有人改这个文件才会被比出来。 这两条是所有 pre-commit 门禁的公共限度,不在本篇展开(→ docs/代码规范/pre-commit使用说明.md)。

7. 豁免与基线

无基线、无豁免、无 --write:要么 BOM 在,要么别提交。

8. 改这个门禁本身

  • 改名单必须同时改三处(§6.2 会拦着);
  • 新增判定分支必须先在 --selftest 里加用例(含正反两向),它是这篇文档里唯一可执行的部分;
  • 别在这里加"自动修复":见 §4 的编码风险。

9. 变更记录

日期 变更
2026-10-25 首版(道E task-20)。立项依据:同日 task-15 内 4 次 + 批 1 一次 BOM 剥离事故;端到端实测(真剥掉 ui_w_records.c 的 BOM → 钩子 rc=1)随 task-20 提交正文留存