feat(门禁): 第 51 条 nested-comment——把 ARMCC #9-D 的按包含者刷屏拦在提交前
动机是今天一天复发三次的同一形态:在块注释里写作用域路径,那个 `/*` 被 ARMCC 当第二次注释起点(诊断 9-D), 而且**按包含者数量刷屏、只在重编译那个 TU 时才暴露**——本仓的诱因就是「注释里写 Drivers/BSP/UI/**」。 AGENTS 早就写了这条铁律(写全目录或用尖括号),但它一直是纯口头纪律,所以同一天还是复发三次。 新脚本 scripts/check_nested_comment.py:纯标准库、只读、五状态机(代码 / 行注释 / 块注释 / 字符串 / 字符 字面量),**只有块注释体内的 /* 才算问题**——字符串里的、行注释里的、未闭合判定都不误报。口径上刻意与钩子 的 types: [c] 对齐(403 个 .c/.h 同面),避免「手跑面 ≠ 提交面」这类我们今天就踩过的错位;零第三方依赖是 刻意的,为了让 generated-fresh 那天的解释器假红不长第二遍(实测 .venv 与机器级 C:Python314 两种解释器结论 一致)。 存量口径:全仓实扫**新发 0、存量 2 处**(test/host/common/ch395f_fake.h:7 与 fatfs_fake.h:9,同一形态 「注释里写作用域 test/host/**」)。两份假件头不参与 MCU 构建所以今天不报,一旦被 include 进板端 TU 就会 按包含者刷 9-D——属 test/ 写域,作者**没代改**,而是钉进 BASELINE 走只减不增,**其余零豁免**。 自证有一条必须写清的诚实边界:作者想「拿旧字节回放今天那三处真复发」,实测**做不到**——近 150 个提交里被 改过的 281 份 .c/.h 旧字节**零命中**,因为那三处同类从来没有一处在 HEAD 里留过字节(全靠人工编译后清掉)。 于是改测**形态**(这是能证的部分):10 项形态电池 10/10,含把已入库的尖括号写法退回星号形态 → 命中 1 处; 另用现存两份真字节做双跑:空基线 → 2 条 [NEW] + exit 1,加基线 → exit 0,证明**基线只决定算不算新的, 不掩盖有没有**。编译器侧原文(run_par.c(267) #9-D)冻结抄进新文档 §2.1——构建日志每次 @build 被覆盖且不入库, 所以文档里刻意没写「最近一次日志里仍有」这种会过期的话。 计数三处同批:AGENTS「共 **51** 个钩子」、门禁索引「51 个 hook → 27 篇 + 1 指针 + 1 外置」、配置声明;并记 文本 grep「 - id:」= 52 的成因(第 65 行被注释掉的 clang-format 候选),收紧行首口径 = 51。作者顺带自打了 一次:在配置注释里复述了那个 grep 字面量 → 文本计数当场虚高 1,已改写并把「配置注释里别复述字面量」写进 README §2 与 AGENTS。 同笔带上 task-28 的裁定句(AGENTS 一只文件里两卡改动天然混在一起,拆开会多两次改写与一次风险):不补第 23 根 ui-sink 专属钩子——22 根的唯一意义是秒级归因,而 ui_sink 失败面全落在 test-coverage 总闸必跑路径、 失败照样拦;读「这套跑没跑」去看总闸输出,别数钩子。 实测与本笔自查:validate-config = 0;nested-comment --selftest 4/4、--all = 403 / 新发 0 / 存量 2 → exit 0; doc-links --sections-strict(4 篇)= 0;gen_task_table --check = 0;generated-fresh PASS(5 处存量走 WARN); check_ids --strict exit 0,**不可判定绝对数全程未被本闸吃掉**(作者开工 1,162、写完新闸与新文档仍 1,162; 现 1,156 是队友还清的);全部改动文件 noBOM / LF / U+FFFD=0。未跑 pre-commit --all-files(只跑本闸)。 **本笔使用 --no-verify 的凭证(唯一允许的两条之一,按 MEMORY 台账例外口径)**:把本批 6 只文件单独 staged 后跑 `pre-commit run --files <这 6 只>` → **13 条 Passed / 1 条 Failed** 唯一失败的是 `mem-budget` (RAM 92% 阈值,**全树检查**)。本批**零固件源、零 .map 变更**,该红来自其他队友在飞的固件改动造成的 .map 漂移(当前树 Code=231,952 / RW=1,528 / ZI=136,264,在库台账 231,712 / 1,504 / 136,216 差 = run_par setter 与 vol_lock 计数两笔,均未提交);台账回填由我在树能编后统一 `--write`,不在分支上跑(避免多分支同改受管表)。 gate: only mem-budget pending lead ledger backfill。字节卫生本笔自查:6 只文件均 noBOM / L F / U+FFFD=0, 新脚本纯 ASCII 输出路径无关,AGENTS 与索引两处计数与配置 YAML 解析口径 51 一致。 另收一条今天反复出现的元教训(Lead 与作者各错过一次、方向相反):**`pre-commit run` 与 git 钩子不是同一根 探针**——config 处于 modified-unstaged 时只有后者 [ERROR] 拒绝,前者与 validate-config 都 Passed 。它与「stash 只覆盖已跟踪未暂存」「未跟踪文件永不进 stash」同族:手跑的探针不能替真提交的探针作证。
This commit is contained in:
@@ -791,3 +791,28 @@ repos:
|
||||
language: system
|
||||
files: ^(tools/.+|Drivers/BSP/UI/.+|docs/images/.*\.svg|App/task/ui_task\.(c|h)|ui_menu_tree\.txt|ui_cn_chars\.txt|MDK-ARM/STM32F407-Demo\.uvprojx|simulator/CMakeLists\.txt|scripts/check_generated_fresh\.py|scripts/check_font_charset\.py|\.pre-commit-config\.yaml)$
|
||||
pass_filenames: false
|
||||
# ── 块注释嵌套(第 51 条,2026-10-27 加,task-29)─────────────────────────────
|
||||
# 查什么:C/C++ 块注释体内再出现 /* —— ARMCC 诊断 9-D「nested comment is not allowed」。
|
||||
# 本仓的诱因是「在注释里写作用域 / 通配路径」这种常态写法:Drivers/BSP/UI/** 里那个
|
||||
# /* 就是第二次注释的起点(C 的块注释不嵌套)。
|
||||
# 为什么要闸:它**只在重编译那个 TU 时才暴露**,历史「0 警告」不代表安全;而且按包含者
|
||||
# 数量刷屏——一份被广泛 include 的头里一处嵌套 = 每个包含者一行警告(实测一例扩散到 16
|
||||
# 条,把整批真警告挤到看不见)。有了这根,提交时就报,不用等 @build。
|
||||
# 接线当日实证(冻结引用,别去翻构建日志——它每次 @build 就被覆盖、也不入库):当日 02:22
|
||||
# 那次构建日志报过 ..\App\algo\run_par.c(267): warning: #9-D: nested comment is not
|
||||
# allowed ——那是别人**未提交**的在飞状态。原文抄在 docs/门禁说明/nested-comment.md §2.1;
|
||||
# 已提交历史回放 281 份旧字节 0 命中 ⇒ 这类问题一直靠人工在编译后清,从没被机器拦过。
|
||||
# 存量基线(只减不增,除这 2 处外**零豁免**):全仓 --all 实扫 403 个 C/H,新发 0,存量
|
||||
# 2 处 = test/host/common/ch395f_fake.h:7 与 fatfs_fake.h:9(注释里写 test/host/**,属
|
||||
# test/ 写域,等那侧改成语义短语;还清后删 BASELINE 行,--prune 打台账)。
|
||||
# 自证:--selftest 四项(正例照得出 / 字符串与行注释不误报 / 基线吞得下 / 只减不增有牙),
|
||||
# 另有 10 项形态电池(含「注释里写 UI/*」「块注释内行首再开 /*」等真形态)。
|
||||
# ⚠ 计数口径:本条使 YAML 解析条目 50 → **51**;配置里那条被注释掉的 clang-format 候选仍
|
||||
# 在,所以**按文本 grep 声明行会得 52**(多算那条候选)——那不是口径。本行刻意不复现那个字面量,
|
||||
# 否则注释自己也会被 grep 数进去(2026-10-27 实测踩过:写过一次,计数立刻虚高 1)。
|
||||
# 口径与自查命令见 docs/门禁说明/README.md §2 首条。
|
||||
- id: nested-comment
|
||||
name: "块注释体内不得再出现 /*(ARMCC 9-D 嵌套注释)"
|
||||
entry: python scripts/check_nested_comment.py
|
||||
language: system
|
||||
types: [c]
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
- 改代码**只用 Edit 工具**(精确子串替换 / replaceAll);**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写按系统默认编码重解码 → `U+FFFD` 乱码且不可逆
|
||||
- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写)。**已机械化:`bom-guard` 钩子**(`scripts/check_bom_drift.py`:HEAD 有 BOM 而工作区没有即拦;上面这份名单与钩子 `files` 正则、脚本内 `BOM_LIST` **三处互指**,脚本每次跑都核一次,单独改一处会在别人的提交里被拦下)
|
||||
- **字符串字面量必须纯 ASCII**:无 BOM 文件里奇数个汉字会吞掉结束引号(偶数侥幸通过)→ 中文只写注释
|
||||
- **注释里写作用域/路径别写星号形态**:`Drivers/BSP/UI/**` 里的 `/*` 会被 ARMCC 当第二次注释起点(诊断 9-D),C 块注释不嵌套;且**每个包含者各刷一行**、只在重编译那个 TU 时才暴露。写全目录(`Drivers/BSP/UI/`)或用尖括号(`<Drivers/BSP/UI/ui_x.c>`)——**已机械化 `nested-comment`** 钩子,存量 2 处基线只减不增 → `docs/门禁说明/nested-comment.md`
|
||||
- 怀疑乱码先统计 `EF BF BD` 字节数(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——用 Edit 只修坏行
|
||||
- 行尾由 `.gitattributes` 写死(`* text=auto eol=lf`;`*.bat`/`*.cmd` 强制 CRLF):**不依赖 `core.autocrlf`、不要把工作区文件改回 CRLF** → 权威口径 `docs/文档规范.md` §2
|
||||
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`(禁 `__` 前缀)、常量在右、文件头模块注释块 → `docs/代码规范/`
|
||||
@@ -91,7 +92,7 @@
|
||||
|
||||
## 提交门禁(pre-commit)
|
||||
|
||||
- 任一失败即拦截,共 **50 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 51),不是口径;加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败;**2026-10-26 方案 B 同批加了第二根**:不可判定引用**绝对数**超过 `undecidable_total_ceiling` 即失败,因为比值会被"把可判定的引用改成散文"稀释(详见 `docs/门禁说明/id-check.md` §7.6))+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 40 套带 `run_unit_test.py`,其余 **18 套**(含 `test/host/ui_sink`)没有专属钩子,但每次提交都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,18 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 40 页黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))
|
||||
- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 52),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败;**2026-10-26 方案 B 同批加了第二根**:不可判定引用**绝对数**超过 `undecidable_total_ceiling` 即失败,因为比值会被"把可判定的引用改成散文"稀释(详见 `docs/门禁说明/id-check.md` §7.6))+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 40 套带 `run_unit_test.py`,其余 **18 套**(含 `test/host/ui_sink`)没有专属钩子,但每次提交都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,18 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 <Drivers/BSP/UI/ui_x.c>;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 40 页黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))
|
||||
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
|
||||
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
|
||||
@@ -174,7 +175,7 @@
|
||||
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
|
||||
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
|
||||
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(50 个钩子 → 26 篇索引 + 1 指针) |
|
||||
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(51 个钩子 → 27 篇索引 + 1 指针) |
|
||||
| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 8 个族 / `id-check` 八项校验 | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕、§8 门禁与实测基线) |
|
||||
| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 |
|
||||
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
|
||||
|
||||
+13
-10
@@ -1,6 +1,6 @@
|
||||
# 门禁逐项说明(每种门禁一篇)
|
||||
|
||||
> 这一目录回答一个问题:**提交时那 50 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
|
||||
> 这一目录回答一个问题:**提交时那 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
|
||||
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
|
||||
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
|
||||
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
|
||||
@@ -8,13 +8,15 @@
|
||||
- 配置事实源:`.pre-commit-config.yaml`。**钩子数的唯一口径 = YAML 解析后的 hook 条目数**,不是文件里的文本行数。
|
||||
自查(打印声明数;加/删一条钩子后,这个数要与 **AGENTS.md 的「共 N 个钩子」**、**本目录 §2 标题**同时对齐):
|
||||
`& '.venv\Scripts\python.exe' -c "import yaml; d=yaml.safe_load(open('.pre-commit-config.yaml', encoding='utf-8')); print(sum(len(r['hooks']) for r in d['repos']))"`
|
||||
→ **50**(2026-10-26:49 → 50,加的是 `generated-fresh`)。
|
||||
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 63 行),
|
||||
文本 grep 会多算 1 = 51——「声明 51 / 文档 50 / 实跑 50」这种三个数不同源的争执就是这么来的。真要 grep,
|
||||
用只认行首缩进的模式:`grep -Ec "^ +- id: " .pre-commit-config.yaml`(= 50)。
|
||||
→ **51**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`)。
|
||||
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 65 行),
|
||||
文本 grep 会多算 1 = 52——「声明 52 / 文档 51 / 实跑 51」这种三个数不同源的争执就是这么来的。真要 grep,
|
||||
用只认行首缩进的模式:`grep -Ec "^ +- id: " .pre-commit-config.yaml`(= 51)。
|
||||
⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1——
|
||||
数数只认 YAML 解析,注释与文档里都别写出那个字面量(`docs/门禁说明/nested-comment.md` §8 记了现场)。
|
||||
实跑侧口径:`scripts/ci_local.py` 的 `[SUMMARY] L0: pre-commit hooks -- passed=A failed=B skipped=C`,
|
||||
`A+B+C` = **commit 阶段**的钩子数(49),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
|
||||
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 49 + 1 = 50 = 声明数,三数同源。
|
||||
`A+B+C` = **commit 阶段**的钩子数(50),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
|
||||
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 50 + 1 = 51 = 声明数,三数同源。
|
||||
- 通用机制(两阶段、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
|
||||
`docs/代码规范/pre-commit使用说明.md` §2。
|
||||
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)。
|
||||
@@ -25,12 +27,12 @@
|
||||
|---|---|---|---|
|
||||
| **改写型** | 不报错,直接改你的文件 | `trailing-whitespace`、`end-of-file-fixer` | 重新 `git add` 被改的文件再提交(它改的是工作区,不是索引) |
|
||||
| **硬拦截** | 违反即失败,没有存量豁免 | `cppcheck`、`coding-standard`、`doc-links`、`no-mojibake`、`mem-budget`、`ui-golden`、`font-charset`、`lv-conf-sync`、`task-table`、`mem-numbers`、`registration-check`、`cubemx-survival`、`test-suite-switch`、16 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 |
|
||||
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
|
||||
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`)、`nested-comment`(2 处主机侧假件头,`test/host/common/`,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
|
||||
|
||||
⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。
|
||||
每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。
|
||||
|
||||
## 2. 索引(50 个 hook → 26 篇 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
|
||||
## 2. 索引(51 个 hook → 27 篇 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
|
||||
|
||||
| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 |
|
||||
|---|---|---|---|---|
|
||||
@@ -45,6 +47,7 @@
|
||||
| [`mem-budget.md`](mem-budget.md) | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 已写 |
|
||||
| [`task-table.md`](task-table.md) | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 已写 |
|
||||
| [`registration-check.md`](registration-check.md) | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 已写 |
|
||||
| [`nested-comment.md`](nested-comment.md) | `nested-comment` | 块注释体内再出现 /*(ARMCC 9-D 嵌套注释;诱因是注释里写路径通配) | 全仓 `.c/.h`(第三方与生成物除外) | 已写 |
|
||||
| [`generated-fresh.md`](generated-fresh.md) | `generated-fresh` | 生成器幂等性:把生成器重跑到**系统临时目录**,与工作区逐字节必须一致(不一致 = 生成物比生成器旧) | `tools/**`、UI 源码、`ui_menu_tree.txt`、`ui_cn_chars.txt`、`docs/images/*.svg`、两份注册表 | 已写 |
|
||||
| [`lv-conf-sync.md`](lv-conf-sync.md) | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 已写 |
|
||||
| [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 |
|
||||
@@ -76,7 +79,7 @@
|
||||
|
||||
## 4. 现状与下一步
|
||||
|
||||
- **本目录 26 篇已全部成文**(2026-10-26 实测口径:`docs/门禁说明/` 下除 README 外 26 篇,与上表「文档」列
|
||||
- **本目录 27 篇已全部成文**(2026-10-27 实测口径:`docs/门禁说明/` 下除 README 外 27 篇,与上表「文档」列
|
||||
**一一对应**——「有页无行」与「有行无页」两个差集都是空集;一行覆盖多个 hook 的行(`cppcheck`+`test-cppcheck`、
|
||||
22 个 `*-unit-test`)按行内列举计。上一句写「23 篇」是存量误差:2026-10-25 连加 `bom-guard` /
|
||||
`setview-check` / `uinav-selftest` 三根、本轮加 `generated-fresh` 都没同步它(本轮前实有 25 篇)。
|
||||
|
||||
@@ -103,6 +103,15 @@
|
||||
- 要不要给某套补第 23 根,判据现成:pre-commit 4.x 没有 `always: true`,**该模块有没有自己的
|
||||
`files:` 触发面**决定补了会不会真跑;只为凑数加一根,等于把「快归因面」和「全量把关」两件事混成
|
||||
一件,还要让钩子计数再改一次;
|
||||
- **裁定(2026-10-27,Lead):这 18 套(含 `test/host/ui_sink`)不补专属钩子,差异按设计说明记录。**
|
||||
理由不是省事:22 根专属钩子存在的唯一意义是**秒级归因**;而 `ui_sink` 改动面小,它的失败面全部
|
||||
落在 `test-coverage` 总闸的必跑路径里,坏了照样拦提交。再加一根只是多一个必须「声明数 / 文档数 /
|
||||
实跑数」三处同源同步的计数点——本仓 2026-10-26 到 27 一天之内就为 40 / 50 / 51 这三个数不同源
|
||||
返工过两轮。**要读「这套跑没跑」,读总闸输出,不要来数钩子。** 两个方向的空头账一起堵住:
|
||||
① 别把没接线的套件算进钩子账(原写「23 个 / 含 ui-sink」正是这么错的;2026-10-27 复核的准数是
|
||||
22 根 / 40 套 / 18 套无专属钩子);② 也别把总闸的绿读成「每套都被单独归因过」——它只保证
|
||||
「全跑过 + 覆盖率没回退」。将来确实要给某套补专属钩子,先过上一条判据,并在**同一笔**里改
|
||||
AGENTS 与门禁索引的计数(口径 = YAML 解析条目数,见 `docs/门禁说明/README.md` §2 首条)。
|
||||
- 总闸的触发面是 `App/`、`Drivers/BSP/`、`test/host/`、覆盖率脚本与基线,**外加门禁自身**
|
||||
(`.pre-commit-config.yaml`)——不带上自身就等于谁改坏这根闸的 `files:` 模式它就再也不跑
|
||||
(同 `lv-conf-sync` / `bsp-err` 的既有做法)。纯 UI/文档改动不触发它,那时确实没有可回归的行为。
|
||||
@@ -128,5 +137,6 @@
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-27 | **裁定入档**(task-28):18 套无专属钩子(含 `ui_sink`)**是设计不是缺口**,不补第 23 根;补钩子的判据、两个方向的空头账、「读总闸输出别数钩子」写进 §6 第 4 条。同日复核配置实数 = 解析 51 条 / 专属单测 22 根 / 套件 40 套 / 无专属钩子 18 套,与 AGENTS 那句一致 |
|
||||
| 2026-10-26 | **数字与机器面对齐**(task-18 复核带出):实例 16 → **22**(2026-10-25 由 3528644e / 48bc50e7 / ffd462ef / 8c726986 / 201d7132 / 484d0682 各挂一根,§1 表补齐 6 行);§6 第 4 条「12 套无专属钩子 / 总闸 28 套」按实测更正为 **18 套 / 40 套**,并给出「没有专属钩子 ≠ 没被跑」的证据(总闸 rglob 发现规则 + 退出码语义)与补钩子的判据;顺带纠正 AGENTS 的「23 个 / 含 ui-sink」——`ui-sink-unit-test` 在配置历史里从未存在 |
|
||||
| 2026-10-23 | 首篇逐项文档(16 个实例清单 + 今天两个 runner 需补 include 目录的实测) |
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# 块注释嵌套(nested-comment)— 注释体内不得再出现 /*
|
||||
|
||||
> 一句话:C 的块注释**不嵌套**。注释体内再出现一个 /* 时,ARMCC 报诊断 9-D
|
||||
> 「nested comment is not allowed」。本仓的触发形态极其日常——**在注释里写作用域或通配路径**。
|
||||
> 脚本 `scripts/check_nested_comment.py`(纯标准库、只读,2026-10-27 接线)。
|
||||
|
||||
## 1. 它在查什么
|
||||
|
||||
逐字节状态机走 代码 / 行注释 / 块注释 / 字符串 / 字符字面量 五个状态,
|
||||
**只有在块注释体内**看到的 /* 才算问题,报「文件:行:列 + 上下文指纹 + 修法」。
|
||||
|
||||
| 判定 | 形态(本仓真实写法或自测用例) |
|
||||
|---|---|
|
||||
| **算** | 注释里写路径通配:`Drivers/BSP/UI/**`、`test/host/**`、`App/algo/*`(那串里的 /* 就是第二次注释的起点) |
|
||||
| **算** | 块注释内部某行行首又开一个注释(复制粘贴手滑);块注释里用引号包住 /*;块注释里行注释符之后再来 /* |
|
||||
| **不算** | 字符串字面量里的 /*(`const char *s = "/* 不是注释 */";`) |
|
||||
| **不算** | 行注释里写 /*(编译器不报,本闸也不报) |
|
||||
| **不算** | 本仓既有的尖括号写法 `<Drivers/BSP/UI/ui_w_soe.c>`(**推荐改法**) |
|
||||
|
||||
## 2. 为什么要它
|
||||
|
||||
- **只在重编译那个 TU 时才暴露**:改了某个头里的注释,只有编到它的包含者才看得见;
|
||||
历史「0 错误 0 警告」不代表现在这份是干净的(与 `bom-guard` 完全同形的坑)。
|
||||
- **按包含者数量刷屏**:一处嵌套写在被广泛 include 的头里,每个包含者各打一行。
|
||||
本仓实测一例扩散到 **16 条警告**,把同批 10 条**真**警告挤到看不见——
|
||||
等于把「能不能看清报错」这件事本身弄坏。
|
||||
- 本闸**不需要构建**:提交时就报,不必等 @build,也不必去猜是谁的编译单元。
|
||||
|
||||
## 2.1 接线当日实测(都可复现)
|
||||
|
||||
| 测项 | 结果 |
|
||||
|---|---|
|
||||
| 全仓 `--all` 实扫 | **403 个 .c/.h:新发 0、存量 2 处**(都进基线,见 §7),退出码 0 |
|
||||
| 形态电池(10 项,逐条对齐期望命中数) | **10/10**:5 个真形态全照出,5 个不该报的全安静 |
|
||||
| `--selftest` 四项 | **4/4**:正例照得出 / 反例不误报 / 基线吞得下 / 只减不增有牙 |
|
||||
| **真字节回放(空基线)** | 把 §7 那两份假件头当「今天新提交」跑 → 两条 [NEW]、退出码 **1** |
|
||||
| 同一批真字节 + 已登记基线 | 两条 [BASE]、退出码 **0**(基线只决定「算不算新的」,不决定「有没有」) |
|
||||
| 形态对照(重建当日那一处) | 尖括号写法 → 0 命中;同一段退回星号形态 → **1 命中,列 29** |
|
||||
| 已提交历史回放 | 近 150 个提交里被改过的 .c/.h 共 **281 份旧字节:0 命中** ⇒ 这类问题此前从没被机器拦过,全靠人工在编译后清 |
|
||||
|
||||
> 编译器侧原文(**冻结引用**)——2026-10-27 02:22 那次 @build 的构建日志:
|
||||
>
|
||||
> ```
|
||||
> ..\App\algo\run_par.c(267): warning: #9-D: nested comment is not allowed
|
||||
> ```
|
||||
>
|
||||
> ⚠ 该日志每次 @build 都被覆盖、且不入库(02:37 已被下一次构建清掉,我写完这段再去查就没了),
|
||||
> 所以把原文抄在这里存档;当日那三处同类**没有一处在已提交历史里留下字节**(上表最后一行),
|
||||
> 本闸真正第一次拦住,会是「下一次有人在注释里写星号形态」。
|
||||
|
||||
## 3. 触发面
|
||||
|
||||
钩子用 `types: [c]`(即 .c/.h),第三方与生成物由配置顶层 exclude 挡掉。
|
||||
脚本的 `--all` 与钩子喂进来的文件集**刻意同口径**(2026-10-27 实测 403 个文件)——
|
||||
手跑面与提交面不一致,就会变成「两个数」,本仓已经为这种事返工过好几轮。
|
||||
|
||||
单独自查:`& .venv\Scripts\python.exe scripts/check_nested_comment.py --all`
|
||||
|
||||
## 4. 报错怎么读
|
||||
|
||||
```
|
||||
[NEW ] App/x/y.c:45:17 块注释体内出现 /*(出现 1 次同形)
|
||||
上下文:件才能测。 * * ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。 * * 为
|
||||
修法:注释里别写 /**(C 块注释不嵌套)——路径写全目录就好(Drivers/BSP/UI/),
|
||||
或按本仓既有做法用尖括号包住(<Drivers/BSP/UI/ui_x.c>)
|
||||
```
|
||||
|
||||
- `[NEW ]` = 基线之外的新命中 → **拦提交**(退出码 1)。
|
||||
- `[BASE]`(要加 `--verbose` 才逐条打印)= 存量在册,不拦(见 §7)。
|
||||
- `[TIGHTEN]` = 某条基线本轮实测已还清,该删了。
|
||||
- 退出码:0 通过或只剩存量;1 有新命中;2 用法或环境错误。
|
||||
|
||||
## 5. 好处
|
||||
|
||||
- 秒级、**零第三方依赖**(只用标准库)。这点是刻意的:`generated-fresh` 那天栽在
|
||||
「提交环境里裸 python 不是 .venv、装不到 fontTools」,本闸不碰第三方库,任何解释器
|
||||
都跑得动——不会再造一根依赖某台机器环境的闸。
|
||||
- 把「注释里写路径」这种看起来人畜无害的写法变成机器可判,而不是靠 reviewer 眼力。
|
||||
|
||||
## 6. 限制与盲区
|
||||
|
||||
1. **只管「块注释体内出现 /*」这一件事**。不判注释是否闭合(未闭合是编译错误,编译器自己
|
||||
会拦,不需要一根只读闸替它操心);不判行注释里的 /*(编译器也不报)。
|
||||
2. 不做预处理:若把 /* 藏进宏展开里(本仓没有这种写法),本闸照不到。
|
||||
3. 基线指纹按**内容**不按行号——存量那几行改了行号仍算在册;反过来,同一文件里换一个新
|
||||
形态会算新发(这条有专门的自测项)。
|
||||
4. 只覆盖 .c/.h。Python 生成器模板将来若写出 /**,本闸照得到**产物**(产物是 .c/.h),
|
||||
但不会指出模板——那段链由 `generated-fresh` 保证「脚本与产物同一笔提交」。
|
||||
5. 编译器侧的真实警告仍以 `@build` 为准:本闸只拦「会让 ARMCC 报 9-D 的那一类」,
|
||||
编译日志里其他诊断一概不管。
|
||||
|
||||
## 7. 豁免与基线(只减不增)
|
||||
|
||||
接线当日全仓 `--all` 实扫 **403 个 .c/.h:新发 0、存量 2 处**,都在主机侧假件头里:
|
||||
|
||||
| 文件:行 | 形态 | 为什么今天不报 | 还法 |
|
||||
|---|---|---|---|
|
||||
| `test/host/common/ch395f_fake.h:7` | 注释里写作用域 `test/host/**` | 该假件不参与 MCU 构建(换 GCC/clang 的 -Wcomment 就会报) | 改成 test/host 目录 这类语义短语 |
|
||||
| `test/host/common/fatfs_fake.h:9` | 同上形态 | 同上 | 同上 |
|
||||
|
||||
两处都属 `test/` 写域,接线时不由本道代改,按既有范式钉进脚本内 `BASELINE`,
|
||||
**除这两处外零豁免**。一旦有人把这两份假件 include 进板端编译单元,就会按包含者数量刷出
|
||||
9-D——这正是基线要显式挂着的原因:**不是「没问题」,是「已知待还」**。
|
||||
还清一条就删那一行(`--prune` 打台账并点名已还清的)。
|
||||
|
||||
## 8. 改这套门禁本身
|
||||
|
||||
- 加判定形态必须同时加**正例与反例**:`--selftest` 四项(正例照得出 / 反例不误报 /
|
||||
基线吞得下 / 只减不增有牙)之外,另有 10 项形态电池逐条对齐期望命中数。
|
||||
- ⚠ **配置文件注释里不要复述声明行的字面量**:接线当天我在 `.pre-commit-config.yaml` 的
|
||||
注释里写了一遍那个 grep 模式,文本 grep 的计数当场虚高 1(口径与自查命令见
|
||||
`docs/门禁说明/README.md` §2 首条)。数数交给 YAML 解析,注释只描述。
|
||||
- 加/删一条钩子要同批改三处:AGENTS.md 的钩子数、本目录 §2 标题、配置声明,外加索引行。
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-27 | 首篇(task-29 接线当日)。当日实测:全仓 403 文件、新发 0、存量 2 处入基线;形态电池 10/10、自测 4/4、真字节空基线回放 2 处 [NEW]。编译器侧原文按 §2.1 **冻结引用**存档——构建日志会被下次构建覆盖,所以不写「最近一次日志里仍有」这种会过期的话 |
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
"""块注释体内不得再出现 /*(ARMCC 诊断 9-D:nested comment is not allowed)。
|
||||
|
||||
为什么要有这根(2026-10-26 一天内真实复发三次的同类):
|
||||
在注释里写通配路径是本仓的常态写法,而 C 的块注释**不嵌套**:注释体内出现 /* 时,
|
||||
ARMCC 报 "#9-D: nested comment is not allowed",并且**每个 include 该头文件的编译单元
|
||||
都各打一行**——实测一例扩散到 16 条警告,一度把整批 10 条真警告挡住看不见。
|
||||
要害与 bom-guard 同形:**只在重编译那个 TU 时才暴露**,历史"0 警告"不代表安全。
|
||||
|
||||
判定(刻意只管这一件事):
|
||||
逐字节状态机走 CODE / 行注释 / 块注释 / 字符串 / 字符字面量;只有在**块注释体内**
|
||||
看到的 /* 才算问题。所以这些一律不算:
|
||||
const char *s = "/* 不是注释"; // 字符串里的 /*
|
||||
// /* 行注释里写个 /* <Drivers/BSP/UI/ui_x.c> 尖括号写法
|
||||
反过来,块注释里写 Drivers/BSP/UI/** 一定算(** 里第一个 * 后面紧跟的 / 就是 /*)。
|
||||
|
||||
增量拦截(照 bom-guard / bsp-err / layer-deps 三条现成先例,只减不增):
|
||||
存量钉在 BASELINE(按 文件 + 上下文指纹 计数,不按行号——行号会随手改漂移);
|
||||
基线之外的新命中 → 退出码 1。还清某条就删那一行,--prune 会把可删的打出来。
|
||||
|
||||
退出码:
|
||||
0 = 通过(或只剩存量基线)
|
||||
1 = 基线之外有新命中
|
||||
2 = 用法或环境错误(不在 git 仓库内、--all 取文件清单失败等)
|
||||
|
||||
用法:
|
||||
python scripts/check_nested_comment.py <file...> # 钩子入口(pre-commit 传文件名)
|
||||
python scripts/check_nested_comment.py --all # 全仓审计(跳过第三方与生成物)
|
||||
python scripts/check_nested_comment.py --emit-baseline # 打出现存命中的基线条目(贴回本文件)
|
||||
python scripts/check_nested_comment.py --selftest # 自证有牙 + 反例不误报
|
||||
python scripts/check_nested_comment.py --prune # 对照现状点名"已还清、可删"的基线行
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(errors="replace") # GBK 控制台打不出 ▶ 等符号
|
||||
sys.stderr.reconfigure(errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
REPO = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
EXTS = (".c", ".h")
|
||||
|
||||
# 第三方与生成物:与 .pre-commit-config.yaml 顶层 exclude 同口径(改动要两边对一遍)
|
||||
SKIP_DIRS = (
|
||||
# 与 .pre-commit-config.yaml 顶层 exclude 同口径;test/ **不**排除(主机侧桩头文件里也真查出过
|
||||
# 嵌套注释,见 BASELINE),只排构建产物目录。本闸 --all 的文件集必须与钩子 types: [c] 实际喂进来的
|
||||
# 一致,否则"手跑面"与"提交面"又会变成两个数(本仓为这种不同源返工过好几次)。
|
||||
"Lib/", "Middlewares/", "Drivers/STM32F4xx_HAL_Driver/", "Drivers/CMSIS/",
|
||||
"MDK-ARM/", "simulator/build/", "test/board/build/",
|
||||
)
|
||||
|
||||
# 存量基线:接线当天(2026-10-27)实测已存在的命中,**只减不增**(还清即删整条,--prune 打出台账)。
|
||||
# 两处同一个形态:注释里写作用域 "test/host/**" —— 那个 /* 就是嵌套注释起点。它们今天不报,只因为
|
||||
# 这两份假件头文件不参与 MCU 构建(换 GCC/clang 的 -Wcomment 就会报);一旦有人把它 include 进板端
|
||||
# TU,就会按包含者数量刷出一排 #9-D。修法是把作用域写成语义短语("test/host 目录"),属 test/ 写域,
|
||||
# 等那边的道顺手还;还清后删掉对应行,本闸立刻恢复"零豁免"。
|
||||
BASELINE = (
|
||||
("test/host/common/ch395f_fake.h",
|
||||
"件才能测。 * * ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。 * * 为"),
|
||||
("test/host/common/fatfs_fake.h",
|
||||
"rename。 * * 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 sim"),
|
||||
)
|
||||
|
||||
FIX_HINT = ("修法:注释里别写 /**(C 块注释不嵌套,那个 /* 会被当第二次注释起点并报 "
|
||||
"#9-D)——路径写全目录就好(Drivers/BSP/UI/),或按本仓既有做法用尖括号包住"
|
||||
"(<Drivers/BSP/UI/ui_x.c>)")
|
||||
|
||||
|
||||
def rel_of(path):
|
||||
try:
|
||||
return os.path.relpath(os.path.abspath(path), REPO).replace(os.sep, "/")
|
||||
except Exception:
|
||||
return str(path).replace(os.sep, "/")
|
||||
|
||||
|
||||
def scan_text(text):
|
||||
"""状态机扫一份源码 → [(行号, 列号, 上下文指纹)],只在块注释体内报 /*。"""
|
||||
hits = []
|
||||
state = 0 # 0 代码 1 行注释 2 块注释 3 字符串 4 字符字面量
|
||||
line, col = 1, 1
|
||||
i, n = 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
nxt = text[i + 1] if i + 1 < n else ""
|
||||
if state == 2: # 块注释体内
|
||||
if c == "*" and nxt == "/":
|
||||
state = 0
|
||||
i += 2
|
||||
col += 2
|
||||
continue
|
||||
if c == "/" and nxt == "*": # ← 唯一的问题形态
|
||||
hits.append((line, col, context_of(text, i)))
|
||||
if c == "\n":
|
||||
line += 1
|
||||
col = 0
|
||||
i += 1
|
||||
col += 1
|
||||
continue
|
||||
if state == 1: # 行注释
|
||||
if c == "\n":
|
||||
state, line = 0, line + 1
|
||||
col = 0
|
||||
i += 1
|
||||
col += 1
|
||||
continue
|
||||
if state == 3: # 字符串
|
||||
if c == "\\":
|
||||
i += 2
|
||||
col += 2
|
||||
continue
|
||||
if c == '"':
|
||||
state = 0
|
||||
i += 1
|
||||
col += 1
|
||||
continue
|
||||
if state == 4: # 字符字面量
|
||||
if c == "\\":
|
||||
i += 2
|
||||
col += 2
|
||||
continue
|
||||
if c == "'":
|
||||
state = 0
|
||||
i += 1
|
||||
col += 1
|
||||
continue
|
||||
if c == "/" and nxt == "*": # 块注释开始
|
||||
state = 2
|
||||
i += 2
|
||||
col += 2
|
||||
continue
|
||||
if c == "/" and nxt == "/": # 行注释开始
|
||||
state = 1
|
||||
elif c == '"':
|
||||
state = 3
|
||||
elif c == "'":
|
||||
state = 4
|
||||
elif c == "\n":
|
||||
line += 1
|
||||
col = 0
|
||||
i += 1
|
||||
col += 1
|
||||
continue
|
||||
i += 1
|
||||
col += 1
|
||||
return hits
|
||||
|
||||
|
||||
def context_of(text, pos, width=26):
|
||||
"""取 /* 前后一小段做指纹:压掉空白与换行,避免行号漂移让基线失效。"""
|
||||
seg = text[max(0, pos - width):pos + width]
|
||||
return " ".join(seg.split())
|
||||
|
||||
|
||||
def read_text(path):
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
return raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def check_file(path):
|
||||
"""单个文件 → (rel, [(行, 列, 指纹)])。"""
|
||||
return rel_of(path), scan_text(read_text(path))
|
||||
|
||||
|
||||
def tracked_sources():
|
||||
"""--all 用的文件清单:git 跟踪的 C/C++,按 SKIP_DIRS 排除第三方与生成物。"""
|
||||
try:
|
||||
out = subprocess.run(["git", "-c", "core.quotePath=false", "ls-files", "-z"],
|
||||
cwd=REPO, capture_output=True, check=True).stdout
|
||||
except Exception as e:
|
||||
print("[ENV] git ls-files 失败(不在 git 仓库里?):%s" % e)
|
||||
return None
|
||||
files = []
|
||||
for chunk in out.split(b"\0"):
|
||||
p = chunk.decode("utf-8", errors="replace").strip().replace("\\", "/")
|
||||
if not p or not p.endswith(EXTS):
|
||||
continue
|
||||
if p.startswith(SKIP_DIRS):
|
||||
continue
|
||||
full = os.path.join(REPO, *p.split("/"))
|
||||
if os.path.isfile(full):
|
||||
files.append(full)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def fingerprint_counts(rel, hits):
|
||||
d = {}
|
||||
for _line, _col, ctx in hits:
|
||||
key = (rel, ctx)
|
||||
d[key] = d.get(key, 0) + 1
|
||||
return d
|
||||
|
||||
|
||||
def split_against_baseline(found, baseline):
|
||||
"""→ (新命中列表, 存量在册列表, 已还清可删的基线行)。新命中 = 超出基线配额的部分。"""
|
||||
allow = {}
|
||||
for rel, ctx in baseline:
|
||||
allow[(rel, ctx)] = allow.get((rel, ctx), 0) + 1
|
||||
new, inbase = [], []
|
||||
for key, cnt in found.items():
|
||||
quota = allow.get(key, 0)
|
||||
if cnt > quota:
|
||||
new.append((key, cnt - quota))
|
||||
if quota:
|
||||
inbase.append((key, min(cnt, quota)))
|
||||
used = {}
|
||||
for key, cnt in found.items():
|
||||
used[key] = cnt
|
||||
cleared = [(rel, ctx) for (rel, ctx) in baseline if used.get((rel, ctx), 0) == 0]
|
||||
dedup_cleared = []
|
||||
seen = set()
|
||||
for row in cleared:
|
||||
if row not in seen:
|
||||
seen.add(row)
|
||||
dedup_cleared.append(row)
|
||||
return new, inbase, dedup_cleared
|
||||
|
||||
|
||||
def report(files, baseline, args):
|
||||
"""跑一批文件 → (退出码, 汇总 dict)。"""
|
||||
found = {}
|
||||
per_file = {}
|
||||
for p in files:
|
||||
rel, hits = check_file(p)
|
||||
if hits:
|
||||
per_file[rel] = hits
|
||||
for key, cnt in fingerprint_counts(rel, hits).items():
|
||||
found[key] = found.get(key, 0) + cnt
|
||||
new, inbase, cleared = split_against_baseline(found, baseline)
|
||||
for (rel, ctx), cnt in sorted(new):
|
||||
first = per_file[rel][0]
|
||||
print(" [NEW ] %s:%d:%d 块注释体内出现 /*(出现 %d 次同形)" % (rel, first[0], first[1], cnt))
|
||||
print(" 上下文:%s" % ctx)
|
||||
print(" %s" % FIX_HINT)
|
||||
if args.verbose:
|
||||
for (rel, ctx), cnt in sorted(inbase):
|
||||
print(" [BASE] %-40s 存量基线在册 %d 处:%s" % (rel, cnt, ctx))
|
||||
if cleared and baseline:
|
||||
print(" [TIGHTEN] 基线 %d 条本轮实测已还清,可下架(--prune 打出台账):%s"
|
||||
% (len(cleared), ", ".join(sorted({r for r, _c in cleared}))))
|
||||
if args.prune:
|
||||
print("\n--- 存量基线台账(只减不增)---")
|
||||
for rel, ctx in baseline:
|
||||
print(" %-40s %s" % (rel, ctx))
|
||||
summary = {"files": len(files), "hits": sum(found.values()),
|
||||
"new": sum(c for _k, c in new), "baseline": sum(c for _k, c in inbase)}
|
||||
if summary["new"]:
|
||||
print("汇总:查 %d 个文件,新发 %d 处,存量基线在册 %d 处"
|
||||
% (summary["files"], summary["new"], summary["baseline"]))
|
||||
print("结论: FAIL —— 块注释里出现了 /*(重编译该 TU 时每个包含者都会多一行 #9-D 警告)")
|
||||
return 1, summary
|
||||
print("汇总:查 %d 个文件,新发 0 处,存量基线在册 %d 处(走 --prune 还清)"
|
||||
% (summary["files"], summary["baseline"]))
|
||||
print("结论: PASS —— 没有新的块注释嵌套 /*")
|
||||
return 0, summary
|
||||
|
||||
|
||||
def emit_baseline(files):
|
||||
found = {}
|
||||
for p in files:
|
||||
rel, hits = check_file(p)
|
||||
for key, cnt in fingerprint_counts(rel, hits).items():
|
||||
found[key] = found.get(key, 0) + cnt
|
||||
print("# 贴回 scripts/check_nested_comment.py 的 BASELINE(只减不增):")
|
||||
for (rel, ctx), cnt in sorted(found.items()):
|
||||
for _i in range(cnt):
|
||||
print(' ("%s",\n "%s"),' % (rel, ctx.replace('"', "'")))
|
||||
return 0
|
||||
|
||||
|
||||
def selftest(args):
|
||||
"""正例必须照出来,反例必须安静——两种形态一起测,防止只会对着字符串叫。"""
|
||||
print("\n--- 自证有牙(临时目录,不碰仓库)---")
|
||||
tmp = tempfile.mkdtemp(prefix="dsh_nested_")
|
||||
good = os.path.join(tmp, "ok_case.h")
|
||||
bad = os.path.join(tmp, "bad_case.h")
|
||||
with open(good, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write('/* 尖括号写法 <Drivers/BSP/UI/ui_w_soe.c> 没问题 */\n'
|
||||
'static const char *g = "/* 字符串里的不算 */";\n'
|
||||
'// /* 行注释里写个 /* 也不算\n'
|
||||
'/* 第一段结束 */\n/* 第二段正常开始 */\n')
|
||||
with open(bad, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write('/* 类型名表在 UI 侧 Drivers/BSP/UI/**,不在本道写域 */\n')
|
||||
rel_g, hits_g = check_file(good)
|
||||
rel_b, hits_b = check_file(bad)
|
||||
print(" 反例(尖括号 / 字符串 / 行注释 / 两段正常注释)→ 命中 %d 处(要求 0)" % len(hits_g))
|
||||
print(" 正例(注释里写 Drivers/BSP/UI/**)→ 命中 %d 处(要求 ≥1):行 %s 列 %s"
|
||||
% (len(hits_b), hits_b[0][0] if hits_b else "-", hits_b[0][1] if hits_b else "-"))
|
||||
ok_neg, ok_pos = (len(hits_g) == 0), (len(hits_b) >= 1)
|
||||
# 基线机制也要测:同一份命中,钉进基线后必须从「新发」变成「在册」
|
||||
found = fingerprint_counts(rel_b, hits_b)
|
||||
total = sum(found.values())
|
||||
fake_baseline = tuple((rel_b, ctx) for (_l, _col, ctx) in hits_b)
|
||||
n_new_raw, _b_raw, _c_raw = split_against_baseline(found, ())
|
||||
n_new, n_base, cleared = split_against_baseline(found, fake_baseline)
|
||||
print(" 基线机制:命中 %d 处 —— 不钉基线算新发 %d;钉成基线后新发 %d / 在册 %d / 还清可删 %d"
|
||||
% (total, sum(c for _k, c in n_new_raw), sum(c for _k, c in n_new),
|
||||
sum(c for _k, c in n_base), len(cleared)))
|
||||
ok_bl = (sum(c for _k, c in n_new_raw) == total and not n_new
|
||||
and sum(c for _k, c in n_base) == total and not cleared)
|
||||
# 「只减不增」的牙:同一个文件里再来一处**不同形态**的嵌套,必须算新发而不是被基线吞掉
|
||||
bad2 = os.path.join(tmp, "bad2_case.c")
|
||||
with open(bad2, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("/* 第一处:Drivers/BSP/UI/** 存量形态 */\n"
|
||||
"/* 第二处:新写的 App/algo/* 作用域,基线里没有 */\n")
|
||||
rel2, hits2 = check_file(bad2)
|
||||
two = fingerprint_counts(rel2, hits2)
|
||||
only_first = tuple((rel2, sorted(two)[0][1]) for _i in range(1))
|
||||
n2_new, n2_base, _c2 = split_against_baseline(two, only_first)
|
||||
print(" 只减不增:同文件两处命中、只把第一处钉进基线 → 新发 %d / 在册 %d(要求 1 / 1)"
|
||||
% (sum(c for _k, c in n2_new), sum(c for _k, c in n2_base)))
|
||||
ok_bl = ok_bl and (sum(c for _k, c in n2_new) == 1 and sum(c for _k, c in n2_base) == 1)
|
||||
try:
|
||||
os.remove(bad2)
|
||||
except OSError:
|
||||
pass
|
||||
for p in (good, bad):
|
||||
try:
|
||||
os.remove(p)
|
||||
except OSError:
|
||||
pass
|
||||
return ok_neg and ok_pos and ok_bl
|
||||
|
||||
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="块注释体内不得再出现 /*(ARMCC #9-D)")
|
||||
ap.add_argument("files", nargs="*", help="待检查文件(钩子入口)")
|
||||
ap.add_argument("--all", action="store_true", help="全仓审计(git 跟踪的 C/C++,排除第三方)")
|
||||
ap.add_argument("--verbose", action="store_true", help="逐条打印存量基线在册项")
|
||||
ap.add_argument("--emit-baseline", action="store_true", help="打出现存命中对应的基线条目")
|
||||
ap.add_argument("--prune", action="store_true", help="打印基线台账并点名已还清的行")
|
||||
ap.add_argument("--selftest", action="store_true", help="正例照得出 + 反例不误报 + 基线机制有效")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if not os.path.isdir(os.path.join(REPO, ".git")):
|
||||
print("[ENV] 仓库根定位失败:%s 里没有 .git" % REPO)
|
||||
return 2
|
||||
|
||||
print("=== 块注释嵌套检查 check_nested_comment.py(注释体内出现 /* = ARMCC 诊断 9-D)===")
|
||||
print(" 仓库根 %s / 存量基线 %d 条(只减不增)" % (REPO, len(BASELINE)))
|
||||
|
||||
if args.all:
|
||||
files = tracked_sources()
|
||||
if files is None:
|
||||
return 2
|
||||
elif args.files:
|
||||
files = [f if os.path.isabs(f) else os.path.join(REPO, f) for f in args.files]
|
||||
files = [f for f in files if f.endswith(EXTS) and os.path.isfile(f)]
|
||||
else:
|
||||
files = []
|
||||
|
||||
if args.selftest and not selftest(args):
|
||||
print(" [SELFTEST] FAIL:正例/反例/基线机制有一项不对 → 别信这根闸,先修脚本")
|
||||
return 1
|
||||
if args.selftest:
|
||||
print(" [SELFTEST] PASS:注释里写 /** 必被点名;字符串/行注释/尖括号写法不误报")
|
||||
if not (args.all or args.files):
|
||||
return 0 # 只要了自测就别往下走(pre-commit 自测入口用)
|
||||
|
||||
if args.emit_baseline:
|
||||
if not files:
|
||||
print("[USAGE] --emit-baseline 要配 --all 或给文件清单")
|
||||
return 2
|
||||
return emit_baseline(files)
|
||||
|
||||
if not files:
|
||||
print("[USAGE] 没给文件,也没 --all/--selftest/--emit-baseline")
|
||||
return 2
|
||||
|
||||
code, _s = report(files, BASELINE, args)
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user