Files
STM32F4-Base/scripts/check_bsp_err.py
T
wandering1 2575177e81 refactor(gate): bsp-err 四项检查改用自描述标签,废掉 R1~R4 这组撞车代号
纯改名,检查行为一个字没动:规则逻辑、阈值、豁免形状全部原样。

改名原因(用户一句「R4 到底是什么」问出来的):R1~R4 是我在 check_bsp_err.py 注释里
自造的序号,而这个裸形在本仓另有含义——R1~R8 已在 scripts/id_registry.json 登记为
"专项风险"的文档内编号(保护规划与看门狗的风险表都在用),带连字符的那一族又是审查
报告的可靠性 / 内存优化项编号(编号规范 §1 的"连字符承载语义"一行正是这么写的)。
**同一个裸形四种意思**。最刺人的一点:编号门禁**不会报**这种撞法,因为编号规范 §4.3
已把这些裸形声明为"同号不同义"、走 collides_with 白名单,校验 4 不计事故——同一天里
我起的另一组自造号被当场拦下、这组代号却一路绿灯,差别不在我做对了什么,而在白名单
挡的是"事故"、不是"可读性"。

新名同时是基线键的第二字段(格式 文件|标签|名字):
· err-type        错误通道不外泄(对外接口不拿 HAL_StatusTypeDef / bool 当成败)
· timeout-name    超时宏不取负值
· return-doc      公开函数必须写「返回值:」
· consume-result  返回值不得裸丢弃(须接住或显式 (void))
老名对照:R1=err-type、R2=timeout-name、R3=return-doc、R4=consume-result。

同步面:脚本里的规则码与 38 条基线键的字段**一起换**(中途不跑 --prune,否则改名会被
读成"38 条全还清 + 38 条新账"),换完 --emit-baseline 复核仍是 38/38/0、RC=0。现状类
文本全部换新名:AGENTS.md、pre-commit使用说明.md §2.27(新增"检查项叫什么"一行含老新
对照与撞号说明)、bsp_err.h 边界纪律、报告 §0.4 / §9 / 附录 A,以及代码注释里的 5 处
(gd5f2gq5ue.c/.h、nand_ftl.h、ui_menu_tab.h)。已提交的变更记录行里的老名按"冻结历史
不回改"处理,以 §2.27 对照行为准。ui_menu_tab.h 带 BOM,Edit 又剥掉、已按原样补回。

写说明文字时又被编号门禁拦了一次:我为了举例"老名撞车",在那段话里写了两个编号字面,
被判"未紧邻文档名 / 未登记前缀"共 3 类新账——连批评自造编号的段落本身都在自造编号。
改法是不写号、只描述形态并指向权威条目。

验证:@build **0 错 0 警告**,Program Size 与上一笔**逐字段相同**(Code 213,676、RO
18,860、RW 1,188、ZI 131,500)→ 证明确为注释与文案,故不跑 @test(无指令变化);
pre-commit run 全绿 **17 项 Passed**;check_doc_links --all --sections-strict 147
  篇 0
问题(中途我自己写的 §4.3 没紧邻文档名,被判成本文自引用,已补全路径);9 个文件字节
卫生 0 异常(UI 头 BOM 仍在、无 CR、无 U+FFFD、均有尾换行)。
2026-10-23 09:36:01 +08:00

342 lines
16 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 后缀表达)、
也有**返回码**。没有机械检查,新代码只会继续按各自旧习惯长。
四条检查(每项一个自描述标签 = 基线键的第二字段,格式是「文件|标签|名字」):
err-type 公开 API 不得返回 HAL_StatusTypeDef / bool
bool 把"为什么失败"整条丢掉;HAL 枚举把 HAL 焊进调用方的 include 链。
timeout-name *_TIMEOUT 宏不得取负值
名字叫 TIMEOUT 却是返回码,读代码的人会当成时长;超时应返回 BSP_ERR_TIMEOUT。
return-doc 非 void 的公开 API 必须在紧邻注释块里写「返回值:」
约定要能被读到,才算约定(也才谈得上审查)。匹配时忽略空格:规范 V1.0 允许列对齐写法
「返 回 值:」,门禁不去规定排版。
consume-result 返回错误码的函数,调用点必须**接住返回值**或**显式 (void) 丢弃**
名单自动取,不手写:① 返回 bsp_err_t 的公开 API(契约规定它就是错误码);
② 返回 int/int32_t 且文档块出现"错误码 / 失败 / BSP_ERR"的函数(覆盖 GD5F 私有码族);
③ 已知返回 HAL_StatusTypeDef 的 HAL 传输/读写族;④ 上面 ①② 同一套启发式再扫一遍 .c
里的 static 定义(只扫头文件等于只守跨编译单元的边界)。为什么单列:实测 ARMCC v5 不认
warn_unused_result、cppcheck 也不报这类丢弃(都在本仓实跑验证过)——**丢弃底层传输的
返回值,今天没有任何工具会响**。已知限度:只认单行整句的裸调用(行尾注释允许),跨行写的暂测不到。
术语迁移(2026-10-23):这四项原先叫 R1/R2/R3/R4,已整体改名。原因是 R1~R8 在本仓是"专项
风险"的裸形编号(scripts/id_registry.json 已登记那一族,保护规划与看门狗的风险表都在用)、
带连字符的那一族又是审查报告的可靠性 / 内存优化项编号——**同一个裸形四种意思**,而编号门禁
因为 docs/编号规范.md §4.3 的同号不同义白名单不会报,
等于机器放行、人看不懂。旧名到新名对照:R1=err-type、R2=timeout-name、R3=return-doc、
R4=consume-result;已提交的文档与变更记录里出现的老名按"冻结历史不回改"处理,以本对照为准。
增量拦截(与 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/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Receive#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Receive#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Receive#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#10
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#11
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#4
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#5
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#6
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#7
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#8
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|HAL_SPI_Transmit#9
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_bbt_save#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_block_erase#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_page_read#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_program_exec#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_program_load#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_read_from_cache#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_read_status#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_read_status#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_read_status#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_wait_busy#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_write_enable#1
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_write_enable#2
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_write_enable#3
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c|consume-result|gd5f_write_enable#4
Drivers/BSP/GD5F2GQ5UE/nand_ftl.c|consume-result|gd5f_write_enable#1
Drivers/BSP/RS485/rs485.c|consume-result|rs485_receive_start#1
Drivers/BSP/RS485/rs485.c|consume-result|rs485_receive_start#2
Drivers/BSP/UI/ui_content_page.h|err-type|ui_content_page_open
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#1
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#2
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#3
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#4
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#5
Drivers/BSP/UI/ui_net_sink.c|consume-result|net_close#6
""".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)
HAL_STATUS_RE = re.compile(r"^HAL_(?:SPI|I2C|UART)_(?:Transmit|Receive|TransmitReceive)"
r"(?:_DMA|_IT)?$|^HAL_I2C_Mem_(?:Read|Write)$")
BARE_CALL_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z_0-9]*)\s*\(.*\);"
r"(?:\s*/\*.*?\*/)?\s*$") # 行尾注释不影响判定
# 两类形状不算"丢弃错误码",也不许塞进基线——同 err-type 谓词白名单的理由:**规则该按真实
# 反例收窄,而不是拿基线去洗假阳性**:
# · 谓词:返回值是"提问"而不是"办成没有"(is_/has_/can_/should_/need_/valid_);
# · setter:形如 xxx_set_yyy(...) 的写入口,它返回的 int 是状态回声而不是调用成败
# (实跑误判出 net_set_errno 八处——它只是把值写进调用方自己的 errno 槽)。
ALLOW_BARE_RE = re.compile(
r"(?:(?:^|_)(?:is|has|can|should|need|valid)_)|(?:^|_)set_[a-z0-9_]+$"
r"|(?:^|_)mark_block_bad$")
def must_use_names():
"""自动取"返回错误码"的函数名(不手写名单,避免名单与代码脱节)。
两条来源用**同一套启发式**:① 头文件里的公开声明;② .c 里的 static 定义(只从头文件
取会漏掉 static——实测 gd5f_set_feature 等就是 static 且返回错误码,上面提到的 ECC 那两行丢弃
正是这类,靠人工才发现)。
"""
names = {}
for hdr in tracked_headers():
try:
htext = (ROOT / hdr).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", hdr, exc)
raise SystemExit(2)
hlines = htext.splitlines()
for m in DECL_RE.finditer(htext):
ret = m.group(1).replace("const", "").strip()
hname = m.group(2)
flat = ret.replace(" ", "")
hdoc = doc_block_before(hlines, htext[:m.start()].count(chr(10)))
if flat.endswith("bsp_err_t"):
names.setdefault(hname, "返回 bsp_err_t")
elif flat in ("int", "int32_t") and re.search("错误码|失败|BSP_ERR|_FAIL", hdoc):
names.setdefault(hname, "返回 int 且文档声明会失败")
for src_c in tracked_sources():
try:
ctext = (ROOT / src_c).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", src_c, exc)
raise SystemExit(2)
clines = ctext.splitlines()
for cname, cidx in static_int_defs(ctext):
cdoc = doc_block_before(clines, cidx)
if re.search("错误码|失败|BSP_ERR|_FAIL", cdoc):
names.setdefault(cname, ".c 内 static 且文档声明会失败")
return names
def tracked_sources():
"""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(chr(92), "/")
if not rel.endswith(".c"):
continue
if "lftpd/" in rel:
continue
files.append(rel)
return sorted(files)
def static_int_defs(text):
"""从 .c 里挑出"返回 int/int32_t 的 static 函数定义"(不引正则,按形状扫)。
为什么需要:只从**头文件**取 must-use 名单会漏掉 static 函数——实测
gd5f_set_feature / gd5f_private_get_feature / gd5f_private_bbt_find_best 都是 static,
它们同样返回错误码,却在名单之外,等于门禁只守跨编译单元的边界、看不见编译
单元内部的丢弃(ECC 那两行就是靠人工发现的,不是靠这条规则)。
"""
hits = []
lines = text.splitlines()
brace = chr(123)
for idx, line in enumerate(lines):
t = line.strip()
if not t.endswith(brace):
continue
if "(" not in t or t.startswith("if") or t.startswith("for"):
continue
if not (t.startswith("static ") or " static " in t):
continue
head = t.split("(")[0]
words = head.replace("static", " ").split()
if len(words) < 2 or words[0] not in ("int", "int32_t"):
continue
hits.append((words[-1], idx))
return hits
def scan_r4(must_use):
"""consume-result:Drivers/BSP 下的 .c 里,对 must-use 函数的"裸调用"(不接、也不显式 (void))。"""
files = tracked_sources()
hits = []
for rel in files:
try:
text = (ROOT / rel).read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError) as exc:
print("读不了", rel, exc)
raise SystemExit(2)
seen = {}
for line in text.splitlines():
t = line.strip()
if t.startswith("*") or t.startswith("//") or t.startswith("/*"):
continue
m = BARE_CALL_RE.match(line)
if not m:
continue
name = m.group(1)
if name in ("if", "while", "for", "switch", "return", "sizeof", "else"):
continue
if name not in must_use and not HAL_STATUS_RE.match(name):
continue
if ALLOW_BARE_RE.search(name):
continue
seen[name] = seen.get(name, 0) + 1
hits.append((rel, "consume-result", "%s#%d" % (name, seen[name])))
return hits
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, "timeout-name", 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, "err-type", 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, "return-doc", name))
hits.extend(scan_r4(must_use_names()))
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())