diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30957fec..ba8ef779 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -569,12 +569,13 @@ repos: pass_filenames: false # 15) BSP 错误码与超时命名约定(报告 S-5 的机械检查那一半)。 - # 三条规则:公开 API 不得用 HAL_StatusTypeDef/bool 当错误通道(谓词 is_/has_/can_/valid - # 不在此列);*_TIMEOUT 宏不得取负值(名字叫 TIMEOUT 其实是返回码,读代码的人会当成 - # 时长);非 void 的公开 API 必须有「返回值:」注释。存量钉 BASELINE、只减不增(2026-10-23 首清点 19 条,已还 18 → **1 条**:`bool ui_content_page_open()`), + # 四项检查:公开 API 不得用 HAL_StatusTypeDef/bool 当错误通道(谓词 is_/has_/can_/valid + # 不在此列);*_TIMEOUT 宏不得取负值(名字叫 TIMEOUT 其实是返回码);非 void 的公开 API + # 必须有「返回值:」注释;**返回错误码的函数不得裸丢弃返回值**(接住或显式 (void)——实测 + # ARMCC v5 与 cppcheck 都不报这类,只有这条会响)。存量钉 BASELINE、只减不增(2026-10-23 + # 首清点 19 条 → 已还 18;新增一项又暴露 37 条 → **现 38 条**:37 丢弃 + 1 个 bool 待改签名), # 修完跑 --prune 点名收紧基线。约定正文 = Drivers/BSP/bsp_err.h,规则说明 = - # scripts/check_bsp_err.py 文件头。注:ARMCC v5 对 warn_unused_result 不生效(实测), - # 所以"丢弃返回值必须写 (void)"这条只能靠书写约定与评审兜着。 + # scripts/check_bsp_err.py 文件头,逐项详解(含限制与盲区)= docs/门禁说明/bsp-err.md。 # 恒扫全库(要按 git 清单建公开头索引),故 pass_filenames: false。 - id: bsp-err name: BSP 错误码与超时命名约定 diff --git a/AGENTS.md b/AGENTS.md index 3f5491e2..29f89058 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **39 个钩子**(逐个详解 → `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` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 6 条基线只减不增,报告 S-1)+ `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 条**),报告 S-5) +- 任一失败即拦截,共 **39 个钩子**(**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `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` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 6 条基线只减不增,报告 S-1)+ `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 条**),报告 S-5) - **范围 = 项目代码**:第三方(`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/` diff --git a/docs/README.md b/docs/README.md index 9c796324..3c8c9316 100644 --- a/docs/README.md +++ b/docs/README.md @@ -167,6 +167,7 @@ docs/ | [`嵌入式C语言代码规范_V1.0.md`](代码规范/嵌入式C语言代码规范_V1.0.md) | 命名/格式/头文件/注释规范(含机械检查项) | | [`pre-commit使用说明.md`](代码规范/pre-commit使用说明.md) | 提交门禁配置、检查范围与排除项 | | [`cppcheck使用说明.md`](代码规范/cppcheck使用说明.md) | cppcheck 配置与告警处置 | +| [`../门禁说明/README.md`](门禁说明/README.md) | **每个提交门禁一篇文档**:查什么、拦下过什么真事、**限制与盲区**、豁免与基线口径 | ### 7.1b 文档规范(写/改文档前必读) diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md new file mode 100644 index 00000000..f924677c --- /dev/null +++ b/docs/门禁说明/README.md @@ -0,0 +1,66 @@ +# 门禁逐项说明(每种门禁一篇) + +> 这一目录回答一个问题:**提交时那 39 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。** +> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / +> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**—— +> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。 + +- 配置事实源:`.pre-commit-config.yaml`(39 个 `- id:`,2026-10-23 清点)。 +- 通用机制与整体口径:`docs/代码规范/pre-commit使用说明.md`(stages、范围排除、stash 陷阱等)。 +- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)。 + +## 1. 三种拦截姿态(先分清这个,报错时才知道该干什么) + +| 姿态 | 含义 | 有哪些 | 失败时该做什么 | +|---|---|---|---| +| **改写型** | 不报错,直接改你的文件 | `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`(6 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 | + +⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。 +每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。 + +## 2. 索引(39 个 hook → 23 篇) + +| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 状态 | +|---|---|---|---|---| +| [`bsp-err.md`](bsp-err.md) | `bsp-err` | BSP 的对外 API 面:错误通道类型、超时宏命名、必须写返回值、返回值不得裸丢弃 | `Drivers/BSP/**` 或门禁自身 | 已写 | +| `layer-deps.md` | `layer-deps` | 分层方向:`Drivers/BSP/**` 不得 include `App/**` | `Drivers/BSP/**` | 待写 | +| `id-check.md` | `id-check` | 实体编号的八项校验(未登记前缀、裸形、重复定义、孤儿、悬空引用…) | 几乎所有文本文件 | 待写 | +| `doc-links.md` | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 待写 | +| `cppcheck.md` | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 待写 | +| `coding-standard.md` | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 待写 | +| `no-mojibake.md` | `no-mojibake` | 禁 `U+FFFD`——中文被按错误编码重解码后不可逆损坏的痕迹 | 全部文本 | 待写 | +| `mem-numbers.md` | `mem-numbers` | 文档里引用的 RAM/Flash 数字与最新 `.map` 报告是否一致(体积档) | `docs/内存管理/**`、相关脚本 | 待写 | +| `mem-budget.md` | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 待写 | +| `task-table.md` | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 待写 | +| `registration-check.md` | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 待写 | +| `lv-conf-sync.md` | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 待写 | +| `cubemx-survival.md` | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 待写 | +| `test-suite-switch.md` | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 待写 | +| `font-charset.md` | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 待写 | +| `ui-golden.md` | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 11 页黄金参考图比对 | UI 源码、模拟器 | 待写 | +| `host-unit-tests.md` | 16 个 `*-unit-test` | 主机侧单测(同一机制 16 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 待写 | +| `trailing-whitespace.md` | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 待写 | +| `end-of-file-fixer.md` | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 待写 | +| `check-added-large-files.md` | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 待写 | +| `check-merge-conflict.md` | `check-merge-conflict` | 别把没解完的冲突标记(`<<<<<<<`)提上去 | 全部 | 待写 | +| `check-yaml.md` | `check-yaml` | YAML 语法是否可解析(门禁配置自己坏了会静默不跑) | `.yml`/`.yaml` | 待写 | +| `gitlint.md` | `gitlint` | 提交信息规则:标题长度、**正文每行 ≤80**、空行要求 | 提交信息(commit-msg 阶段) | 待写 | + +## 3. 每篇文档的固定八段 + +1. **它在查什么**——逐条判据,能对着代码复核的粒度; +2. **为什么要它**——它拦下过什么真事,没有它会怎样(没有真事的门禁要老实写"预防性"); +3. **触发面**——`files:` / `exclude:` / `pass_filenames` / 阶段,以及**看不见哪些文件**; +4. **报错怎么读**——样例输出、怎么单独跑、常见"假报警"(含 stash 陷阱); +5. **好处**——它把哪件事从"口头约定"变成了机器判定; +6. **限制与盲区**——什么写法能骗过它、什么错它照不到、判据的启发式在哪里; +7. **豁免与基线**——白名单形状、在册数量、还债口径(只减不增); +8. **改门禁本身**——自测怎么做、改名会波及什么(例如基线键)、为什么不许拿基线洗假阳性。 + +## 4. 迁移状态 + +`docs/代码规范/pre-commit使用说明.md` §2 目前仍是多数门禁的详解所在地。本目录逐篇写完后, +那节收敛为"通用机制 + 总览 + 指向本目录",同一事实只留一处(`docs/文档规范.md` 的要求)。 +上表「状态」列就是迁移进度;改完一篇就把该行改成"已写",并在 §2 对应小节留一行指针。 diff --git a/docs/门禁说明/bsp-err.md b/docs/门禁说明/bsp-err.md new file mode 100644 index 00000000..f99801a6 --- /dev/null +++ b/docs/门禁说明/bsp-err.md @@ -0,0 +1,151 @@ +# `bsp-err` — BSP 错误码与超时命名约定 + +> 一句话:它管 **`Drivers/BSP/**` 对外 API 的四件事**——用什么类型表达失败、超时宏叫什么、 +> 有没有把返回值写进注释、调用点有没有真的去看返回值。**它不管错误码用得对不对**(见 §6)。 + +- 配置:`.pre-commit-config.yaml` 第 579 行起(`entry: python scripts/check_bsp_err.py`、 + `language: system`、`pass_filenames: false`)。 +- 规则脚本:`scripts/check_bsp_err.py`(纯标准库;文件头就是规则正文)。 +- 约定正文:`Drivers/BSP/bsp_err.h`(**header-only,无 `.c`** → 不占体积、不需双端注册)。 +- 背景与整改史:`docs/问题记录/项目审查与优化建议报告.md` §0.4 与 §9 的 S-5 行。 + +## 1. 它在查什么 + +四项检查,每项一个自描述标签(就是基线键的第二字段,键格式 `文件|标签|名字`): + +| 标签 | 判据 | 拿它挡的具体写法 | +|---|---|---| +| `err-type` | 公开 API 的返回类型不得是 `HAL_StatusTypeDef`;也不得是 `bool`(除非是谓词形状) | 用 `bool` 表达成败——"为什么失败"整条丢掉;用 HAL 枚举——把 HAL 焊进调用方的 include 链 | +| `timeout-name` | `#define` 出来的 `*TIMEOUT*` 宏不得取负值 | 名字叫 TIMEOUT 实际是返回码(本仓曾有 `GD5F_BUSY_TIMEOUT = -2`、`TPAFE5160_BUSY_TIMEOUT = -2`,已改) | +| `return-doc` | 非 `void` 的公开 API,声明上方紧邻注释块里必须出现"返回值"(**匹配时忽略空格**) | 只写函数功能不写成败含义 | +| `consume-result` | 返回错误码的函数,调用点必须**接住返回值**或**显式 `(void)` 丢弃**;裸调用即违规 | `HAL_SPI_Transmit(&hspi1, cmd, 3, T);` 后面照旧 `return OK` | + +`consume-result` 的"哪些函数算返回错误码"是**自动取名单**,不手写(手写名单必然与代码脱节): + +1. 头文件里返回 `bsp_err_t` 的公开 API(契约规定 `bsp_err_t` 就是错误码); +2. 返回 `int`/`int32_t` 且其文档块出现"错误码 / 失败 / BSP_ERR / `_FAIL`"的函数; +3. 已知返回 `HAL_StatusTypeDef` 的 HAL 族(`HAL_{SPI,I2C,UART}_{Transmit,Receive,TransmitReceive}` + 及其 `_DMA`/`_IT` 变体、`HAL_I2C_Mem_{Read,Write}`); +4. 与 2 同一套启发式,再扫一遍 `.c` 里的 **static 定义**——只扫头文件等于只守跨编译单元的边界。 + +## 2. 为什么要它 + +- 这一层今天**六套约定并存**:0/−1、自研枚举 `CH395F_STATUS_*`、芯片寄存器原生码 + `CH395F_ERR_*`(与前一套同模块并存)、`HAL_StatusTypeDef`、`bool`,外加第三方 dhara 的 + `dhara_error_t` 与 FatFs 的 `FRESULT`。真正值钱的不是码名好看,而是"**能不能重试**" + 以前只写在个别注释里——现在 `BSP_ERR_CAN_RETRY(e)` 把它变成跨模块可判定。 +- **它上线当天就照出真窟窿**:`consume-result` 落地即暴露 37 条存量丢弃,其中一处是 + `bbt_rebuild` 重新打开 ECC 的那两行返回值全丢——SPI 一抖 ECC 就保持关闭,而 ECC 关闭时 + ECC 检查恒读"无错",整条链再没有任何信号。修法与运维判据见 + `docs/测试说明/存储维护与容量恢复.md` §4.3。 +- **没有工具会替我们做这件事**,两条实测负结果(本门禁存在的正当性全在这两条上): + ① `__attribute__((warn_unused_result))` 在 ARMCC v5 上**完全不生效**——属性挂上、调用点 + 裸调不接返回值,构建仍 0 错 0 警告(在 `rs485_transmit` 上做过实验); + ② **cppcheck 也照不出来**——按本仓自己的 `--enable=warning,style,performance,portability` + 跑 `gd5f2gq5ue.c`,对 14 处丢弃 `HAL_SPI_*` 返回值、以及"未初始化局部量直接进 `P_FAIL` + 判定"**一条不报**(它假定被调函数会写出参,而失败路径根本不写)。 + +## 3. 触发面 + +``` +files: ^Drivers/BSP/|^scripts/check_bsp_err\.py$|^\.pre-commit-config\.yaml$ +pass_filenames: false # 恒扫全库:要按 git 清单建公开头索引,不能只看暂存的那几个文件 +``` + +- 扫描对象来自 `git ls-files Drivers/BSP`:`err-type`/`timeout-name`/`return-doc` 看 **`.h`** + (对外 API 面),`consume-result` 扫 **`.c`**(33 个文件)。 +- **`lftpd/` 排除**:它的 `226/425/450/550` 是 FTP 协议码,不是内部错误码。 +- `net_*` BSD 兼容层**不纳入统一码**(用户裁定 2026-10-23,见 `Drivers/BSP/bsp_err.h` 边界纪律 3): + 调用方要的就是"返回字节数 + 每任务 errno 槽"那个形状,模拟器的 Winsock 后端照同一形状实现。 + ⚠ 但**"不纳入码"不等于"可以丢弃返回值"**——`consume-result` 照样管 `net_close` 这类调用。 +- 因为 `files:` 覆盖到 `.pre-commit-config.yaml` 与脚本自身,改门禁必然触发它自己。 + +## 4. 报错怎么读 + +``` +$ python scripts/check_bsp_err.py +BSP 约定:命中 38(在册 38 / 新增 0),基线共 38 +``` + +| 输出 | 意思 | 做什么 | +|---|---|---| +| `新增 N>0` + `[FAIL]` | 出现了基线之外的新违规 | 按提示改代码(返回 `bsp_err_t` / 时长宏带 `_MS` / 补「返回值:」/ 接住返回值或写 `(void)`) | +| `[提醒] 有 N 条基线已还清` | 你把存量修掉了 | 跑 `--prune` 拿到清单,**从脚本的 `BASELINE` 里删掉那些行**,再跑一次确认 0 提醒 | +| `命中 0` | 干净 | — | +| 本地跑出**与暂存无关的假报警** | pre-commit 会 stash 未暂存改动,钩子看到的是"没有你改动"的树 | 先 `git add` 再跑钩子(本仓踩过:rs485 试点期被假报一批) | + +三个只读旗标:`--verbose`(列出全部命中并标注在册/新增)、`--prune`(只报已消失、可删的基线)、 +`--emit-baseline`(打印当前全部键,重建基线用)。**脚本刻意没有 `--write`**:基线是人审后手改的, +不提供"一条命令把现状洗成合法"的入口。退出码 0 通过 / 1 有新增 / 2 用法或扫描失败。 + +## 5. 好处 + +- 把"约定"从口头搬到机器上:新代码想沿用旧习惯,提交就被拦,不需要人在评审里记着。 +- 存量与新增分得开:基线让历史债务不阻塞日常工作,"只减不增 + `--prune` 验收"保证债只会变小。 +- 可审计:`--verbose` 就是债务清单;每条基线是 `文件|标签|名字`,能对着代码定位。 +- 零运行时成本:header-only 契约 + 纯标准库脚本,不进固件、不需要构建开关。 + +## 6. 限制与盲区(读这篇最重要的部分:绿灯不等于没问题) + +1. **不管错误码用得对不对**。它只判通道类型、命名、有没有文档、有没有被消费。把 + `BSP_ERR_TIMEOUT` 用在"参数非法"上,它一声不响——那是语义问题,机器判不了。 +2. **范围只有 `Drivers/BSP`**。`App/**`、`simulator/**`、`Src/**` 不在面内;应用层的同类问题 + 今天仍然只能靠评审。 +3. `consume-result` **只认单行整句的裸调用**(`foo(...);` 一行写完,行尾注释允许)。跨行写的调用 + ——参数换行、或 `;` 在下一行——按正则形状测不到。这是判据的物理限度,不是漏配。 +4. **名单是启发式**:靠"返回类型 + 注释关键词"认函数。**注释里不写"错误码/失败"的函数会漏** + (假阴性);反过来,把谓词写成 `foo_ok()` 这种不在豁免形状里的名字就会被判违规(假阳性, + 见 §7 的两次收窄)。**判据依赖命名形状,改名就能绕过规则**——这是所有基于正则的门禁共同的洞。 +5. HAL 族是**固定清单**(§1 第 3 条)。清单外的返回 `HAL_StatusTypeDef` 的函数(如 FLASH、SD 卡 + 那批)不在名单里;将来用到就得多补一行正则,属于**人工维护的盲区**。 +6. `err-type`/`return-doc` 只看**头文件里的声明形状**:用宏包出来的 API、函数指针表里的 API、 + 通过 `typedef` 起的函数别名,都不在它的视野里。 +7. `return-doc` 的判据是"紧邻注释块里出现『返回值』字样",**散文里出现这三个字也会放行**。 + 这不是假想:本仓 `gd5f_write_enable`、`gd5f_read_status` 曾**长期"通过"**,原因竟是它们的 + *函数说明*里顺口写了"返回值"三个字,而不是真有一行返回值说明。**一条会因为散文里出现关键词 + 而放行的规则,等于在奖励运气**——已改为忽略空格后匹配(规范 V1.0 允许列对齐写法「返 回 值:」, + 门禁不规定排版),但"关键词出现即放行"的性质没有变,只能靠评审看内容。 +8. **基线键带出现序号**(`gd5f_read_status#2`)。含义是"该文件里该函数的第 2 处丢弃",不是位置。 + 于是:删掉靠前的丢弃会让后面的序号整体前移,`--prune` 只报"消失的键",**不会告诉你某处只是 + 搬了家**;反过来,同函数新增一处丢弃会多出一个新序号 → 会被判新增(这点是好的,实测过)。 +9. **改标签名 = 改基线键**。今天把 `R1~R4` 换成现名时,必须脚本与 38 条基线一起换,中途跑 + `--prune` 会把改名读成"38 条全还清 + 38 条新账"。同理,今后任何标签改名都是**一次原子改动**, + 不能分两个提交。 +10. 不提供重试 helper,也不检查重试策略(有上限吗、喂看门狗了吗、跨就绪屏障等待了吗)—— + 见 `Drivers/BSP/bsp_err.h` 边界纪律 5:一个通用 `while` 包装太容易被拿去包住无限等待。 + +## 7. 豁免与基线 + +- 基线在脚本里的 `BASELINE` 常量(不是外部台账文件),**只减不增**,没有 `--write`。 +- **当前在册 38 条**(2026-10-23): + · `consume-result` 37 条 —— `gd5f2gq5ue.c` 28(**14 处丢弃 `HAL_SPI_*` 状态**:`HAL_SPI_Transmit`×11、 + `HAL_SPI_Receive`×3;**14 处丢弃自家原语**:`gd5f_write_enable`×4、`gd5f_read_status`×3、 + `gd5f_bbt_save`×1、`gd5f_block_erase`×1、`gd5f_page_read`×1、`gd5f_program_exec`×1、 + `gd5f_program_load`×1、`gd5f_read_from_cache`×1、`gd5f_wait_busy`×1)、`ui_net_sink.c` 6(全是 + `net_close`)、`rs485.c` 2(`rs485_receive_start`)、`nand_ftl.c` 1; + · `err-type` 1 条 —— `bool ui_content_page_open()`:要改签名并处理 3 个调用点(三处今天全部 + 丢弃返回值,即"按 ENTER 开页失败是静默的"),与"补注释"不是一档事,见报告变更记录。 +- 形状白名单(**写进规则,不塞基线**): + · 谓词 `is_/has_/can_/should_/need_/valid_`——返回值是"提问"而不是"办成没有"; + · setter `(?:^|_)set_...$`——返回的整数是状态回声而不是调用成败(实跑误判出 `net_set_errno` 八处); + · `mark_block_bad`——今天返回 `void`,预先豁免,防它将来加错误码返回时把整批调用点变成违规。 +- **原则:规则按真实反例收窄,不拿基线洗假阳性。**本仓第一版 `err-type` 误判 7 个正当谓词, + 改窄而不是入基线;`consume-result` 两次同类处理见上面两条。 + +## 8. 改这个门禁本身 + +- 改正则**先做正反例自测**:写一段一次性 python,把"应当命中"和"不应命中"的样例各列几条, + `exec` 脚本头部拿到正则再断言(今天三次修正则都是这么验的,6 条样例级别即可)。 +- 假阳性只能改规则;**把它加进 `BASELINE` 是洗规则**,会把真问题一起豁免掉。 +- 加新的 must-use HAL 族:改 `HAL_STATUS_RE`,同时补一条正反例自测。 +- 改标签名:脚本与基线一次改完(§6 第 9 条),随后 `--emit-baseline` 复核数量不变。 +- 门禁自己的注释里不要写只有本次会话懂的代号;要引用规范就写`文件.md` + 紧邻编号。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首版三项检查(错误通道 / 超时宏命名 / 必须写返回值)随 S-5 契约落地,存量基线 19 条 | +| 2026-10-23 | 还掉 18 条缺「返回值:」;`return-doc` 改为忽略空格匹配(此前两条因散文里出现"返回值"而误判通过) | +| 2026-10-23 | 新增第四项 `consume-result`,上线暴露 37 条存量丢弃;名单扩到 `.c` 内 static 定义;修两次判据(行尾注释漏判、setter 误判);同日修掉 `bbt_rebuild` 的 ECC 开关丢弃 | +| 2026-10-23 | 四项由 `R1~R4` 改名为语义标签(该裸形在本仓与"专项风险"编号撞车,且编号门禁因同号不同义白名单不会报);基线 38 条随改名一起换 |