From 1d39571114cfcf495039ab95d5ef038b76641f95 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 27 Oct 2026 03:43:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=97=A8=E7=A6=81):=20=E7=AC=AC=2051=20?= =?UTF-8?q?=E6=9D=A1=20nested-comment=E2=80=94=E2=80=94=E6=8A=8A=20ARMCC?= =?UTF-8?q?=20#9-D=20=E7=9A=84=E6=8C=89=E5=8C=85=E5=90=AB=E8=80=85?= =?UTF-8?q?=E5=88=B7=E5=B1=8F=E6=8B=A6=E5=9C=A8=E6=8F=90=E4=BA=A4=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 动机是今天一天复发三次的同一形态:在块注释里写作用域路径,那个 `/*` 被 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」同族:手跑的探针不能替真提交的探针作证。 --- .pre-commit-config.yaml | 25 ++ AGENTS.md | 5 +- docs/门禁说明/README.md | 23 +- docs/门禁说明/host-unit-tests.md | 10 + docs/门禁说明/nested-comment.md | 119 ++++++++++ scripts/check_nested_comment.py | 382 +++++++++++++++++++++++++++++++ 6 files changed, 552 insertions(+), 12 deletions(-) create mode 100644 docs/门禁说明/nested-comment.md create mode 100644 scripts/check_nested_comment.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 746cc55a..81d73c79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/AGENTS.md b/AGENTS.md index 8b00b936..6ae9310d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/`)或用尖括号(``)——**已机械化 `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 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 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` | diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index e9615b2c..6f1146f5 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -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 篇)。 diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md index ebe60eae..799485a4 100644 --- a/docs/门禁说明/host-unit-tests.md +++ b/docs/门禁说明/host-unit-tests.md @@ -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 目录的实测) | diff --git a/docs/门禁说明/nested-comment.md b/docs/门禁说明/nested-comment.md new file mode 100644 index 00000000..8c104168 --- /dev/null +++ b/docs/门禁说明/nested-comment.md @@ -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 = "/* 不是注释 */";`) | +| **不算** | 行注释里写 /*(编译器不报,本闸也不报) | +| **不算** | 本仓既有的尖括号写法 ``(**推荐改法**) | + +## 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/), + 或按本仓既有做法用尖括号包住() +``` + +- `[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 **冻结引用**存档——构建日志会被下次构建覆盖,所以不写「最近一次日志里仍有」这种会过期的话 | diff --git a/scripts/check_nested_comment.py b/scripts/check_nested_comment.py new file mode 100644 index 00000000..1bf731af --- /dev/null +++ b/scripts/check_nested_comment.py @@ -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/** 一定算(** 里第一个 * 后面紧跟的 / 就是 /*)。 + +增量拦截(照 bom-guard / bsp-err / layer-deps 三条现成先例,只减不增): + 存量钉在 BASELINE(按 文件 + 上下文指纹 计数,不按行号——行号会随手改漂移); + 基线之外的新命中 → 退出码 1。还清某条就删那一行,--prune 会把可删的打出来。 + +退出码: + 0 = 通过(或只剩存量基线) + 1 = 基线之外有新命中 + 2 = 用法或环境错误(不在 git 仓库内、--all 取文件清单失败等) + +用法: + python scripts/check_nested_comment.py # 钩子入口(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/),或按本仓既有做法用尖括号包住" + "()") + + +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('/* 尖括号写法 没问题 */\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())