Files
STM32F4-Base/scripts/check_bsp_err.py
T
wandering1 cc2966319a docs(S-5): 补齐 16 条返回值约定,bsp-err 基线 19 → 1,并修门禁自身误判
A(还债)先行,B(GD5F 的 SPI 失败被吞成成功)另波处理。本笔不改行为:@build 的
Program Size 与上一笔逐字段相同(Code 213,648、RO 18,860、RW 1,188、ZI 131,500)。

**补齐 16 条「返回值:」约定,bsp-err 存量基线 19 → 1**:
· gd5f2gq5ue.h 九个 SPI 原语(wait_busy / write_enable / read_status / page_read /
  read_from_cache / program_load / program_exec / block_erase / check_ecc)
  写全规范五要素,
  并把**每个函数能返回哪些码**写死:哪一个是"可重试"的 GD5F_WAIT_EXPIRED、哪一个才是
  器件自认坏块的 P_FAIL/E_FAIL、失败时出参为什么不可信(E-42 同族)。
· nand_ftl.h 从零散三行补成带模块头,并明写 **nand_ftl_init() 的 −1 抹掉了失败类**:
  分类目前只在失败点进日志,要按类分支需要 C4 快照(现无消费者故未做);同时警告"不得
  把 −1 解读成卷坏了可以重格式化"(E-31 的教训正在这条上)。
· ui_menu_tab.h 五个表查询 API 由行尾短注释升级为完整块(含"越界返回 false 是保守取
  只读"、"宁可按一张错表导航失败也不要按错表导航成功"这类判断依据)。

**修掉门禁自己的一条误判口径**(本笔最该记的):R3 原本要求紧邻注释块里出现字面"返回值",
而规范 V1.0 允许列对齐写法「返 回 值:」。后果有两层:① 合规写法被判违规;② 反向更糟——
gd5f_write_enable / gd5f_read_status 两条**早就"通过"了**,原因竟是它们的**函数说明**里顺口
写了"返回值"三个字,而不是真有一行返回值说明。**一条会因为散文里出现关键词而放行的规则,
等于在奖励运气**。修法是在块内忽略空格后再匹配关键词(排版选择不是违规),并在注释里写明
为什么不许把"必须写成不空格的形式"写进规则。

剩余 1 条刻意保留:bool ui_content_page_open()。它与"补注释"不是一档事——要改签名、要处理
3 个调用点(ui_menu.c:138/192/196 **今天三处全部丢弃返回值**,即"按 ENTER 开页失败是静默的"
这件事现在没人知道)、且 UI 源码双端编译(模拟器同一份),验证面是"重建模拟器 + 像素回归 +
真机逐页进菜单"。这不是还债,是独立一波改动,留档待决策。UI 三个文件带 BOM,Edit 会静默
剥掉 → 已按原样补回 3 字节(AGENTS 铁律:丢 BOM 后 ARMCC 按 CP936 解码,中文报 #870-D)。

**更正我自己写进文档的一条错话**:S-5 各条里我把 GD5F_SPI_TIMEOUT 100 记成"自旋圈数(改
分频时长会变)"。这次逐行核它 12 处用法,**全部是 HAL_* 超时参数或 HAL_GetTick() 差值 →
它也是毫秒**。真问题降级为"单位没写进名字(缺 _MS 后缀),读代码必须回实现里查单位"。
osWaitForever 与 HAL_MAX_DELAY 同为 0xFFFFFFFFU、以及 *_BUSY_TIMEOUT = -2 其实是返回码这两条
仍然成立(后者已改)。现状类条目(报告 §0.4、bsp_err.h、pre-commit使用说明.md §2.27、脚本
文件头)已全部改准;**已提交的变更记录行仍写着旧说法——按 §8.1 冻结历史不回改,以本笔新增
的更正行为准**。

验证:@build 0 错 0 警告(尺寸同上,故**不跑 @test**——无指令变化,跑它没有新信息);受影响
文件的 17 个钩子全 Passed(含 ui-golden 像素回归、cppcheck、编码规范、bsp-err 1/1/0 且
--prune 干净);check_doc_links --all --sections-strict 147 篇 0 问题;id-check
  --strict 通过;
9 个文件字节卫生 0 异常(UI 头 BOM 仍在、无 CR、无 U+FFFD、无行尾空格、均有尾换行)。
口径同步:AGENTS.md、.pre-commit-config.yaml、pre-commit使用说明.md §2.27 的基线计数 19 → 1;
报告 §0.4 / §9 / 附录 A 与变更记录两条(还债一条、更正一条)。
2026-10-23 08:25:58 +08:00

162 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""BSP 错误码与超时命名约定检查(报告 S-5 的机械检查那一半)。
为什么要有这个脚本:约定写在 docs 里不会自己生效。同一层的驱动今天并存六套返回约定,
而"能不能重试"只写在个别注释里;叫 *_TIMEOUT 的东西里既有真毫秒(单位靠不统一的 _MS 后缀表达)、
也有**返回码**。没有机械检查,新代码只会继续按各自旧习惯长。
三条规则(只看 Drivers/BSP 下的公开头文件 = 对外 API 面;lftpd 按第三方排除):
R1 公开 API 不得返回 HAL_StatusTypeDef / bool
bool 把"为什么失败"整条丢掉;HAL 枚举把 HAL 焊进调用方的 include 链。
R2 *_TIMEOUT 宏不得取负值
名字叫 TIMEOUT 却是返回码,读代码的人会当成时长;超时应返回 BSP_ERR_TIMEOUT。
R3 非 void 的公开 API 必须在紧邻注释块里写「返回值:」
约定要能被读到才算约定,也才谈得上审查。
增量拦截(与 layer-deps / id-check 同构):BASELINE 钉住存量,只减不增,没有 --write。
修完一条跑 --prune,它会点名"这条已消失,请从 BASELINE 删除"。
退出码:0 = 通过;1 = 有新增违规;2 = 用法或扫描错误。
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
EXCLUDE = ("lftpd/",)
# 存量基线(2026-10-23 清点):"文件|规则|名字"。修掉一条删一行,不要往里加。
BASELINE = frozenset(
line.strip() for line in """
Drivers/BSP/UI/ui_content_page.h|R1|ui_content_page_open
""".splitlines() if line.strip()
)
DEFINE_RE = re.compile(r"^\s*#define\s+([A-Z][A-Z_0-9]*TIMEOUT[A-Z_0-9]*)\s+(-[0-9]+)",
re.MULTILINE);
# bool 用在"提问"上是合法的(is_editable / has_children / is_active…);只有拿 bool 当
# **错误通道**才是问题。用名字形状区分,避免把 7 个正当谓词误判成违规、再拿基线去洗规则。
PREDICATE_RE = re.compile(r"(^|_)(is|has|can|should|need|valid|active|open|empty)_|" +
r"_(is|has|can|valid)$")
DECL_RE = re.compile(r"^\s*(?:extern\s+)?([A-Za-z_][A-Za-z_0-9_ ]*[ ]+\**)"
r"([a-z_][a-z_0-9]*)\s*\([^;{]*\)\s*;", re.MULTILINE);
def tracked_headers():
"""git 跟踪的 BSP 公开头文件(排除第三方 lftpd)。"""
out = subprocess.run(["git", "ls-files", "--", "Drivers/BSP"],
cwd=str(ROOT), capture_output=True, text=True)
if out.returncode != 0:
print("git ls-files 失败:", out.stderr.strip())
raise SystemExit(2)
files = []
for rel in out.stdout.split():
rel = rel.replace("\\", "/")
if not rel.endswith(".h"):
continue
if any(part in rel for part in EXCLUDE):
continue
files.append(rel)
return sorted(files)
def doc_block_before(lines, idx):
"""取声明上方紧邻的注释块文本(向上直到 /* 或空行为止)。"""
buf = []
j = idx - 1
while j >= 0:
t = lines[j].strip()
if t.endswith("*/") or t.startswith("*") or t.startswith("/*"):
buf.append(t)
if t.startswith("/*"):
break
j -= 1
continue
break
return "\n".join(buf)
def scan():
hits = []
for rel in tracked_headers():
try:
text = (ROOT / rel).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", rel, exc)
raise SystemExit(2)
lines = text.splitlines()
for m in DEFINE_RE.finditer(text):
hits.append((rel, "R2", m.group(1)))
for m in DECL_RE.finditer(text):
ret = m.group(1).replace("const", "").strip()
name = m.group(2)
flat = ret.replace(" ", "");
if name in ("void", "return", "if", "while", "sizeof", "typedef", "static"):
continue
is_bool = flat.endswith("bool") or flat.endswith("_Bool")
if flat.endswith("HAL_StatusTypeDef") or (is_bool and not PREDICATE_RE.search(name)):
hits.append((rel, "R1", name))
if flat == "void":
continue
idx = text[:m.start()].count("\n")
# 忽略空格:规范 V1.0 允许列对齐写法「返 回 值:」,那只是一种排版选择,
# 要求"必须写成不空格的形式"等于让门禁去规定排版,会把合规注释判成违规。
marker = doc_block_before(lines, idx).replace(" ", "").replace("\u3000", "")
if "返回值" not in marker:
hits.append((rel, "R3", name))
return hits
def main():
ap = argparse.ArgumentParser(description="BSP 错误码/超时命名约定(增量拦截)")
ap.add_argument("--verbose", action="store_true", help="列出全部命中与归类")
ap.add_argument("--prune", action="store_true", help="只报已消失、可从 BASELINE 删除的条目")
ap.add_argument("--emit-baseline", action="store_true",
help="打印当前全部命中的键(建基线用)")
args = ap.parse_args()
keys = {"%s|%s|%s" % h for h in scan()}
if args.emit_baseline:
for key in sorted(keys):
print(key)
print("# 共 %d 条" % len(keys))
return 0
gone = sorted(set(BASELINE) - keys)
fresh = sorted(keys - set(BASELINE))
if args.prune:
if not gone:
print("[prune] 基线 %d 条全部仍在" % len(BASELINE))
for key in gone:
print("[prune] 这条已消失,请从 BASELINE 删除:", key)
return 0
if args.verbose:
for key in sorted(keys):
print(" [%s] %s" % ("基线" if key in BASELINE else "新增", key))
print("BSP 约定:命中 %d(在册 %d / 新增 %d),基线共 %d"
% (len(keys), len(keys & set(BASELINE)), len(fresh), len(BASELINE)))
if fresh:
print("[FAIL] 新增违反 BSP 约定的条目:")
for key in fresh:
print(" -", key)
print(' 出路:① 返回 bsp_err_t(Drivers/BSP/bsp_err.h,0=成功、负值=失败,')
print(' 可重试判定用 BSP_ERR_CAN_RETRY);② 时长宏一律 *_MS 且必须是时间,')
print(' 若将来出现按循环次数计时的等待,改名 *_LOOP 并注释写明非时间量;')
print(' ③ 返回码不许叫 *_TIMEOUT。')
return 1
if gone:
print("[提醒] 有 %d 条基线已还清,请用 --prune 列出并从 BASELINE 删除" % len(gone))
return 0
if __name__ == "__main__":
raise SystemExit(main())