Files
STM32F4-Base/scripts/check_nested_comment.py
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

383 lines
17 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
"""块注释体内不得再出现 /*(ARMCC 诊断 9-D:nested comment is not allowed)。
为什么要有这根(2026-10-26 一天内真实复发三次的同类):
在注释里写通配路径是本仓的常态写法,而 C 的块注释**不嵌套**:注释体内出现 /* 时,
ARMCC 报 "#9-D: nested comment is not allowed",并且**每个 include 该头文件的编译单元
都各打一行**——实测一例扩散到 16 条警告,一度把整批 10 条真警告挡住看不见。
要害与 bom-guard 同形:**只在重编译那个 TU 时才暴露**,历史"0 警告"不代表安全。
判定(刻意只管这一件事):
逐字节状态机走 CODE / 行注释 / 块注释 / 字符串 / 字符字面量;只有在**块注释体内**
看到的 /* 才算问题。所以这些一律不算:
const char *s = "/* 不是注释"; // 字符串里的 /*
// /* 行注释里写个 /* <Drivers/BSP/UI/ui_x.c> 尖括号写法
反过来,块注释里写 Drivers/BSP/UI/** 一定算(** 里第一个 * 后面紧跟的 / 就是 /*)。
增量拦截(照 bom-guard / bsp-err / layer-deps 三条现成先例,只减不增):
存量钉在 BASELINE(按 文件 + 上下文指纹 计数,不按行号——行号会随手改漂移);
基线之外的新命中 → 退出码 1。还清某条就删那一行,--prune 会把可删的打出来。
退出码:
0 = 通过(或只剩存量基线)
1 = 基线之外有新命中
2 = 用法或环境错误(不在 git 仓库内、--all 取文件清单失败等)
用法:
python scripts/check_nested_comment.py <file...> # 钩子入口(pre-commit 传文件名)
python scripts/check_nested_comment.py --all # 全仓审计(跳过第三方与生成物)
python scripts/check_nested_comment.py --emit-baseline # 打出现存命中的基线条目(贴回本文件)
python scripts/check_nested_comment.py --selftest # 自证有牙 + 反例不误报
python scripts/check_nested_comment.py --prune # 对照现状点名"已还清、可删"的基线行
"""
import argparse
import os
import subprocess
import sys
import tempfile
try:
sys.stdout.reconfigure(errors="replace") # GBK 控制台打不出 ▶ 等符号
sys.stderr.reconfigure(errors="replace")
except Exception:
pass
REPO = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
EXTS = (".c", ".h")
# 第三方与生成物:与 .pre-commit-config.yaml 顶层 exclude 同口径(改动要两边对一遍)
SKIP_DIRS = (
# 与 .pre-commit-config.yaml 顶层 exclude 同口径;test/ **不**排除(主机侧桩头文件里也真查出过
# 嵌套注释,见 BASELINE),只排构建产物目录。本闸 --all 的文件集必须与钩子 types: [c] 实际喂进来的
# 一致,否则"手跑面"与"提交面"又会变成两个数(本仓为这种不同源返工过好几次)。
"Lib/", "Middlewares/", "Drivers/STM32F4xx_HAL_Driver/", "Drivers/CMSIS/",
"MDK-ARM/", "simulator/build/", "test/board/build/",
)
# 存量基线:接线当天(2026-10-27)实测已存在的命中,**只减不增**(还清即删整条,--prune 打出台账)。
# 两处同一个形态:注释里写作用域 "test/host/**" —— 那个 /* 就是嵌套注释起点。它们今天不报,只因为
# 这两份假件头文件不参与 MCU 构建(换 GCC/clang 的 -Wcomment 就会报);一旦有人把它 include 进板端
# TU,就会按包含者数量刷出一排 #9-D。修法是把作用域写成语义短语("test/host 目录"),属 test/ 写域,
# 等那边的道顺手还;还清后删掉对应行,本闸立刻恢复"零豁免"。
BASELINE = (
("test/host/common/ch395f_fake.h",
"件才能测。 * * ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。 * * 为"),
("test/host/common/fatfs_fake.h",
"rename。 * * 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 sim"),
)
FIX_HINT = ("修法:注释里别写 /**(C 块注释不嵌套,那个 /* 会被当第二次注释起点并报 "
"#9-D)——路径写全目录就好(Drivers/BSP/UI/),或按本仓既有做法用尖括号包住"
"(<Drivers/BSP/UI/ui_x.c>)")
def rel_of(path):
try:
return os.path.relpath(os.path.abspath(path), REPO).replace(os.sep, "/")
except Exception:
return str(path).replace(os.sep, "/")
def scan_text(text):
"""状态机扫一份源码 → [(行号, 列号, 上下文指纹)],只在块注释体内报 /*。"""
hits = []
state = 0 # 0 代码 1 行注释 2 块注释 3 字符串 4 字符字面量
line, col = 1, 1
i, n = 0, len(text)
while i < n:
c = text[i]
nxt = text[i + 1] if i + 1 < n else ""
if state == 2: # 块注释体内
if c == "*" and nxt == "/":
state = 0
i += 2
col += 2
continue
if c == "/" and nxt == "*": # ← 唯一的问题形态
hits.append((line, col, context_of(text, i)))
if c == "\n":
line += 1
col = 0
i += 1
col += 1
continue
if state == 1: # 行注释
if c == "\n":
state, line = 0, line + 1
col = 0
i += 1
col += 1
continue
if state == 3: # 字符串
if c == "\\":
i += 2
col += 2
continue
if c == '"':
state = 0
i += 1
col += 1
continue
if state == 4: # 字符字面量
if c == "\\":
i += 2
col += 2
continue
if c == "'":
state = 0
i += 1
col += 1
continue
if c == "/" and nxt == "*": # 块注释开始
state = 2
i += 2
col += 2
continue
if c == "/" and nxt == "/": # 行注释开始
state = 1
elif c == '"':
state = 3
elif c == "'":
state = 4
elif c == "\n":
line += 1
col = 0
i += 1
col += 1
continue
i += 1
col += 1
return hits
def context_of(text, pos, width=26):
"""取 /* 前后一小段做指纹:压掉空白与换行,避免行号漂移让基线失效。"""
seg = text[max(0, pos - width):pos + width]
return " ".join(seg.split())
def read_text(path):
with open(path, "rb") as f:
raw = f.read()
return raw.decode("utf-8", errors="replace")
def check_file(path):
"""单个文件 → (rel, [(行, 列, 指纹)])。"""
return rel_of(path), scan_text(read_text(path))
def tracked_sources():
"""--all 用的文件清单:git 跟踪的 C/C++,按 SKIP_DIRS 排除第三方与生成物。"""
try:
out = subprocess.run(["git", "-c", "core.quotePath=false", "ls-files", "-z"],
cwd=REPO, capture_output=True, check=True).stdout
except Exception as e:
print("[ENV] git ls-files 失败(不在 git 仓库里?):%s" % e)
return None
files = []
for chunk in out.split(b"\0"):
p = chunk.decode("utf-8", errors="replace").strip().replace("\\", "/")
if not p or not p.endswith(EXTS):
continue
if p.startswith(SKIP_DIRS):
continue
full = os.path.join(REPO, *p.split("/"))
if os.path.isfile(full):
files.append(full)
return sorted(files)
def fingerprint_counts(rel, hits):
d = {}
for _line, _col, ctx in hits:
key = (rel, ctx)
d[key] = d.get(key, 0) + 1
return d
def split_against_baseline(found, baseline):
"""→ (新命中列表, 存量在册列表, 已还清可删的基线行)。新命中 = 超出基线配额的部分。"""
allow = {}
for rel, ctx in baseline:
allow[(rel, ctx)] = allow.get((rel, ctx), 0) + 1
new, inbase = [], []
for key, cnt in found.items():
quota = allow.get(key, 0)
if cnt > quota:
new.append((key, cnt - quota))
if quota:
inbase.append((key, min(cnt, quota)))
used = {}
for key, cnt in found.items():
used[key] = cnt
cleared = [(rel, ctx) for (rel, ctx) in baseline if used.get((rel, ctx), 0) == 0]
dedup_cleared = []
seen = set()
for row in cleared:
if row not in seen:
seen.add(row)
dedup_cleared.append(row)
return new, inbase, dedup_cleared
def report(files, baseline, args):
"""跑一批文件 → (退出码, 汇总 dict)。"""
found = {}
per_file = {}
for p in files:
rel, hits = check_file(p)
if hits:
per_file[rel] = hits
for key, cnt in fingerprint_counts(rel, hits).items():
found[key] = found.get(key, 0) + cnt
new, inbase, cleared = split_against_baseline(found, baseline)
for (rel, ctx), cnt in sorted(new):
first = per_file[rel][0]
print(" [NEW ] %s:%d:%d 块注释体内出现 /*(出现 %d 次同形)" % (rel, first[0], first[1], cnt))
print(" 上下文:%s" % ctx)
print(" %s" % FIX_HINT)
if args.verbose:
for (rel, ctx), cnt in sorted(inbase):
print(" [BASE] %-40s 存量基线在册 %d 处:%s" % (rel, cnt, ctx))
if cleared and baseline:
print(" [TIGHTEN] 基线 %d 条本轮实测已还清,可下架(--prune 打出台账):%s"
% (len(cleared), ", ".join(sorted({r for r, _c in cleared}))))
if args.prune:
print("\n--- 存量基线台账(只减不增)---")
for rel, ctx in baseline:
print(" %-40s %s" % (rel, ctx))
summary = {"files": len(files), "hits": sum(found.values()),
"new": sum(c for _k, c in new), "baseline": sum(c for _k, c in inbase)}
if summary["new"]:
print("汇总:查 %d 个文件,新发 %d 处,存量基线在册 %d 处"
% (summary["files"], summary["new"], summary["baseline"]))
print("结论: FAIL —— 块注释里出现了 /*(重编译该 TU 时每个包含者都会多一行 #9-D 警告)")
return 1, summary
print("汇总:查 %d 个文件,新发 0 处,存量基线在册 %d 处(走 --prune 还清)"
% (summary["files"], summary["baseline"]))
print("结论: PASS —— 没有新的块注释嵌套 /*")
return 0, summary
def emit_baseline(files):
found = {}
for p in files:
rel, hits = check_file(p)
for key, cnt in fingerprint_counts(rel, hits).items():
found[key] = found.get(key, 0) + cnt
print("# 贴回 scripts/check_nested_comment.py 的 BASELINE(只减不增):")
for (rel, ctx), cnt in sorted(found.items()):
for _i in range(cnt):
print(' ("%s",\n "%s"),' % (rel, ctx.replace('"', "'")))
return 0
def selftest(args):
"""正例必须照出来,反例必须安静——两种形态一起测,防止只会对着字符串叫。"""
print("\n--- 自证有牙(临时目录,不碰仓库)---")
tmp = tempfile.mkdtemp(prefix="dsh_nested_")
good = os.path.join(tmp, "ok_case.h")
bad = os.path.join(tmp, "bad_case.h")
with open(good, "w", encoding="utf-8", newline="\n") as f:
f.write('/* 尖括号写法 <Drivers/BSP/UI/ui_w_soe.c> 没问题 */\n'
'static const char *g = "/* 字符串里的不算 */";\n'
'// /* 行注释里写个 /* 也不算\n'
'/* 第一段结束 */\n/* 第二段正常开始 */\n')
with open(bad, "w", encoding="utf-8", newline="\n") as f:
f.write('/* 类型名表在 UI 侧 Drivers/BSP/UI/**,不在本道写域 */\n')
rel_g, hits_g = check_file(good)
rel_b, hits_b = check_file(bad)
print(" 反例(尖括号 / 字符串 / 行注释 / 两段正常注释)→ 命中 %d 处(要求 0)" % len(hits_g))
print(" 正例(注释里写 Drivers/BSP/UI/**)→ 命中 %d 处(要求 ≥1):行 %s 列 %s"
% (len(hits_b), hits_b[0][0] if hits_b else "-", hits_b[0][1] if hits_b else "-"))
ok_neg, ok_pos = (len(hits_g) == 0), (len(hits_b) >= 1)
# 基线机制也要测:同一份命中,钉进基线后必须从「新发」变成「在册」
found = fingerprint_counts(rel_b, hits_b)
total = sum(found.values())
fake_baseline = tuple((rel_b, ctx) for (_l, _col, ctx) in hits_b)
n_new_raw, _b_raw, _c_raw = split_against_baseline(found, ())
n_new, n_base, cleared = split_against_baseline(found, fake_baseline)
print(" 基线机制:命中 %d 处 —— 不钉基线算新发 %d;钉成基线后新发 %d / 在册 %d / 还清可删 %d"
% (total, sum(c for _k, c in n_new_raw), sum(c for _k, c in n_new),
sum(c for _k, c in n_base), len(cleared)))
ok_bl = (sum(c for _k, c in n_new_raw) == total and not n_new
and sum(c for _k, c in n_base) == total and not cleared)
# 「只减不增」的牙:同一个文件里再来一处**不同形态**的嵌套,必须算新发而不是被基线吞掉
bad2 = os.path.join(tmp, "bad2_case.c")
with open(bad2, "w", encoding="utf-8", newline="\n") as f:
f.write("/* 第一处:Drivers/BSP/UI/** 存量形态 */\n"
"/* 第二处:新写的 App/algo/* 作用域,基线里没有 */\n")
rel2, hits2 = check_file(bad2)
two = fingerprint_counts(rel2, hits2)
only_first = tuple((rel2, sorted(two)[0][1]) for _i in range(1))
n2_new, n2_base, _c2 = split_against_baseline(two, only_first)
print(" 只减不增:同文件两处命中、只把第一处钉进基线 → 新发 %d / 在册 %d(要求 1 / 1)"
% (sum(c for _k, c in n2_new), sum(c for _k, c in n2_base)))
ok_bl = ok_bl and (sum(c for _k, c in n2_new) == 1 and sum(c for _k, c in n2_base) == 1)
try:
os.remove(bad2)
except OSError:
pass
for p in (good, bad):
try:
os.remove(p)
except OSError:
pass
return ok_neg and ok_pos and ok_bl
def main(argv=None):
ap = argparse.ArgumentParser(description="块注释体内不得再出现 /*(ARMCC #9-D)")
ap.add_argument("files", nargs="*", help="待检查文件(钩子入口)")
ap.add_argument("--all", action="store_true", help="全仓审计(git 跟踪的 C/C++,排除第三方)")
ap.add_argument("--verbose", action="store_true", help="逐条打印存量基线在册项")
ap.add_argument("--emit-baseline", action="store_true", help="打出现存命中对应的基线条目")
ap.add_argument("--prune", action="store_true", help="打印基线台账并点名已还清的行")
ap.add_argument("--selftest", action="store_true", help="正例照得出 + 反例不误报 + 基线机制有效")
args = ap.parse_args(argv)
if not os.path.exists(os.path.join(REPO, ".git")):
print("[ENV] 仓库根定位失败:%s 里没有 .git(目录或 worktree 的 gitdir 文件都算)" % REPO)
return 2
print("=== 块注释嵌套检查 check_nested_comment.py(注释体内出现 /* = ARMCC 诊断 9-D)===")
print(" 仓库根 %s / 存量基线 %d 条(只减不增)" % (REPO, len(BASELINE)))
if args.all:
files = tracked_sources()
if files is None:
return 2
elif args.files:
files = [f if os.path.isabs(f) else os.path.join(REPO, f) for f in args.files]
files = [f for f in files if f.endswith(EXTS) and os.path.isfile(f)]
else:
files = []
if args.selftest and not selftest(args):
print(" [SELFTEST] FAIL:正例/反例/基线机制有一项不对 → 别信这根闸,先修脚本")
return 1
if args.selftest:
print(" [SELFTEST] PASS:注释里写 /** 必被点名;字符串/行注释/尖括号写法不误报")
if not (args.all or args.files):
return 0 # 只要了自测就别往下走(pre-commit 自测入口用)
if args.emit_baseline:
if not files:
print("[USAGE] --emit-baseline 要配 --all 或给文件清单")
return 2
return emit_baseline(files)
if not files:
print("[USAGE] 没给文件,也没 --all/--selftest/--emit-baseline")
return 2
code, _s = report(files, BASELINE, args)
return code
if __name__ == "__main__":
sys.exit(main())