Files
STM32F4-Base/scripts/check_bsp_err.py
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +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 错误码与超时命名约定检查(机械检查那一半)。
为什么要有这个脚本:约定写在 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())