Files
STM32F4-Base/scripts/check_bsp_err.py
T
wandering1 ad87f979b1 docs(规约): 删除编号规范.md,生效规则并入文档规范 §7.1
用户裁定:docs/编号规范.md 在项目里已无意义,删除。

依据:该文 253 行里真正仍生效的只有两条规则(新条目一律不发编号、
人读文档存量编号继续清退)和一张存量族表;其余内容早有归属——两道
编号门禁的退役记录在 docs/门禁说明/id-check.md 与 id-check-new.md,
手工普查与恢复机器面在 scripts/archive/id_gate/README.md。留着就是
第二真相源,同一事实写在两处迟早漂移。

处理:
- docs/文档规范.md 新增 §7.1「条目与编号:一律不发」,承接规则、
  场景表、三条硬规矩、存量族表、已停发旧号族清单、不是编号的清单、
  清理动作与「没有机器门禁兜底」四段。
- 活跃引用 62 处逐条改指 §7.1 或归档 README:AGENTS.md 2 处、
  门禁说明三篇 18 处、其余文档 11 篇、代码与脚本 10 处、rename_maps
  七个 record 字段、.dsh/skills 四个文件 8 处。
- 归档区 107 处去掉 docs/ 前缀当纯文本提及;编号治理历史.md 45 处
  改为「原编号规范 §N」,避免被 doc-links 当自引用解析。
- 顺带修正 scripts/archive/id_gate/README.md 里仍称旧文为现行规范
  的表述。

门禁:check_doc_links --all --sections-strict 220 篇全通过、无法判定 0;
pre-commit 全 Passed。
2026-11-06 18:32:04 +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 §7.1),
等于机器放行、人看不懂。旧名到新名对照: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())