Files
STM32F4-Base/scripts/check_coding_standard.py
T
wandering1 c936441c83 门禁扩容:新增 lv-conf-sync、simulator/ 纳入全部钩子;含 L3 定值/元件框架落地
本轮把"自研代码落在门禁之外"的口子收掉,同时落地 LVGL 特性裁剪与
L3 保护定值/元件框架。

门禁(.pre-commit-config.yaml,26 个钩子)
- 新增 scripts/check_lv_conf_sync.py + 钩子 lv-conf-sync:比对两份
  lv_conf.h 的**生效值**(未定义则取 lv_conf_internal.h 兜底默认),
  抓 MISMATCH / LVGL 不认的宏名 / 同文件重复定义
- 顶层 exclude 删掉 simulator/:其下 21 个 tracked 文件全是本项目自研
  代码,生成物(build/、SDL2/、tests/ref/、shot*)已在 .gitignore;
  doc-links 的 exclude 同步删除
- lv-conf-sync 的 files: 覆盖两端配置 → 只改 simulator/lv_conf.h 也触发
  (pre-commit 4.x 已移除 always: true,实测只告警并跳过,无兜底开关)
- 新增 prot-set-unit-test / prot-elm-unit-test 两个主机侧单测钩子
- S11(2.3.5) 判定细化:禁的是"用路径找文件",尖括号系统头带子目录
  (如 <sys/stat.h>)不再误报;全仓库复核仅 2 处项目代码判定变化

模拟器代码整改(纳入钩子后暴露,全部修代码而非豁免)
- cppcheck 4 处:sim_syslog.c 判空分支内解引用 p_epoch(真实 bug)、
  2 处 struct tm 未初始化、1 处指针可加 const
- coding-standard 61 处:44 处函数左大括号并到右括号同行、8 处补函数
  注释、3 处补文件头注释块、2 处 __ 前缀保护宏改 XXX_H、2 处 sprintf
  改 snprintf(写入字节数与原来完全一致)

LVGL 特性裁剪(F-1/F-2/F-3)
- 两端 lv_conf.h 对齐同一特性子集:删 26 个 LVGL 9.3 不存在的选项名、
  3 处重复定义,10 项靠默认值隐式生效的改为显式固化
- 实测对镜像体积中性;价值在防漂移,且模拟器 golden 回归跑的正是固件
  实际编译的配置

L3 软件侧
- App/algo/prot_set.* Q10 定值模型(范围校验 / 整表 all-or-nothing /
  校验和封存-自检-闭锁)+ prot_elm.* 通用"滞回+定时限"元件框架 7 实例
- test/host/prot_set、test/host/prot_elm 主机侧单测;uvprojx 挂载两处

文档同步
- pre-commit使用说明:§2.8 S11 口径、§2.15 lv-conf-sync 详解、新增 §5.5
  复盘(教训:exclude 只写真正不该进钩子的路径)
- AGENTS.md、UI_设计架构 §1.4 第 10 条、RAM 分析、审查报告刷新与销项

验证
- @build BUILD_OK 0/0(Code=194276 RO=15872 RW=876 ZI=139892)
- 模拟器重建:菜单 8 状态像素逐字节一致(池 5368 B)、11 内容页黄金参考
  全过(往返 5448 B)、remo sink 0/25600 位差异、--logtest/--cfgtest PASS
- 全仓库 cppcheck / coding-standard / no-mojibake --all-files 全过
- check_doc_links --all 129/0;check_mem_numbers --check 内存数字一致
- 端到端:往 simulator/lv_conf.h 注入一行漂移 → 钩子 FAIL(exit 1)
2026-10-11 08:48:49 +08:00

311 lines
13 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 -*-
"""
C 语言代码规范检查(《嵌入式C语言代码规范_V1.0》可机械化校验的规则)
由 pre-commit 调用:python scripts/check_coding_standard.py <file...>,发现违规退出码 1。
检查项(括号内为规范条款号):
S1 (2.3.1) .h 文件必须有 XXX_H 格式的头文件保护宏(禁止 __ 双下划线前缀)
S2 (5.2) 文件开头必须有文件头注释(含 模块名称 / 作者)
S3 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字
S4 (3.5.1) typedef 类型名必须以 _t 结尾
S5 (6.3.2) switch 必须包含 default 分支
S6 (7.5) 禁用不安全字符串/输入函数 strcpy/strcat/sprintf/gets/scanf/strtok
S7 (6.3.3) 禁止使用 goto
S8 (4.1.1) 禁止 Tab 缩进(使用 4 个空格)
S9 (5.3) 函数定义必须有函数注释(含「函数功能」)
S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
S11 (2.3.5) 禁止相对/绝对路径 include
S12 (7.6) 禁止可变参数函数(...)
S13 (7.7) 禁止递归函数
S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
S17 (2.2) 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过)
S18 (6.3.1) 禁止 Yoda 条件(字面量/常量必须在运算符右侧)
S19 (4.4.2) 函数定义左大括号必须与右括号同行
语义类规则(函数长度、单一职责、错误处理等)无法机械可靠判定,不在此脚本范围。
"""
import os
import re
import sys
# 顶层函数定义行:类型 名称 ( 参数 ),可选同行左大括号
FUNC_DEF_RE = re.compile(
r'^[A-Za-z_][A-Za-z0-9_\s\*]*\s+[a-z_][a-z0-9_]*\s*\([^;{]*\)\s*\{?\s*$'
)
CTRL_RE = re.compile(r'^\s*(if|for|while|switch|return|do|else)\b')
C_KEYWORDS = {'if', 'for', 'while', 'switch', 'do', 'else', 'return',
'sizeof', 'struct', 'union', 'enum'}
NAME_PAREN_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\s*\(')
def _mask(text):
"""注释与字符串字面量替换为等长空白(保留换行,行号不变),防止误报。
单次组合正则扫描,避免字符串内的 // 或注释内的引号造成顺序误掩。
"""
def _repl(m):
return ''.join(c if c == '\n' else ' ' for c in m.group(0))
pat = re.compile(r'"(\\.|[^"\\\n])*"|/\*.*?\*/|//[^\n]*', re.S)
return pat.sub(_repl, text)
def _line(text, pos):
return text[:pos].count('\n') + 1
def _fn_defs(masked):
"""线性扫描找函数定义 name(...) {,返回 [(name, name行, body, pre, 大括号行, 右括号行)]。
body 为定义大括号内文本,pre 为名称前 200 字符(用于判定 static)。
"""
out = []
n = len(masked)
for m in NAME_PAREN_RE.finditer(masked):
name = m.group(1)
if name in C_KEYWORDS or name == 'defined':
continue
ls = masked.rfind('\n', 0, m.start()) + 1
if masked[ls:].lstrip()[:1] == '#':
continue
i = m.end() - 1 # '(' 下标
depth, j = 0, i
ok = False
while j < n:
c = masked[j]
if c == '(':
depth += 1
elif c == ')':
depth -= 1
if depth == 0:
ok = True
break
elif c in '{};':
break
j += 1
if not ok:
continue
k = j + 1
while k < n and masked[k] in ' \t\r\n':
k += 1
if k >= n or masked[k] != '{':
continue
depth, b = 0, k
while b < n:
if masked[b] == '{':
depth += 1
elif masked[b] == '}':
depth -= 1
if depth == 0:
break
b += 1
out.append((name, _line(masked, m.start()), masked[k + 1:b],
masked[max(0, m.start() - 200):m.start()],
_line(masked, k), _line(masked, j)))
return out
def check_file(path):
with open(path, 'r', encoding='utf-8', errors='replace') as f:
text = f.read()
lines = text.splitlines()
v = [] # (行号或'-', 规则, 说明)
# S1 (2.3.1) .h 头文件保护宏
if path.endswith('.h') and \
not re.search(r'#\s*ifndef\s+(?<!_)[A-Z][A-Z0-9_]*_H\b', text):
v.append(('-', 'S1 2.3.1', '缺少 XXX_H 格式的头文件保护宏(禁止 __ 前缀)'))
# S2 (5.2) 文件头注释
head = '\n'.join(lines[:60])
if '模块名称' not in head and '作者' not in head:
v.append(('-', 'S2 5.2', '缺少文件头注释(模块名称 / 作者)'))
# S3 (3.4.1) 宏名全大写
for m in re.finditer(r'^\s*#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)', text, re.M):
if re.search(r'[a-z]', m.group(1)):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S3 3.4.1', '宏名须全大写+下划线: %s' % m.group(1)))
# S4 (3.5.1) typedef 类型名 _t 后缀
for m in re.finditer(r'typedef\s+(?:struct|enum|union)\s*\{.*?\}\s'
r'([A-Za-z_][A-Za-z0-9_]*)\s*;', text, re.S):
if not m.group(1).endswith('_t'):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S4 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
for m in re.finditer(r'typedef\s+(?:unsigned\s+|signed\s+)?'
r'(?:char|short|int|long|float|double)\s+\*?\s*'
r'([A-Za-z_][A-Za-z0-9_]*)\s*;', text):
if not m.group(1).endswith('_t'):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S4 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
# S5 (6.3.2) switch 必须有 default
for m in re.finditer(r'\bswitch\s*\(', text):
i = text.find('{', m.start())
if i == -1:
continue
j, d = i, 0
while j < len(text):
if text[j] == '{':
d += 1
elif text[j] == '}':
d -= 1
if d == 0:
break
j += 1
if not re.search(r'\bdefault\s*:', text[i:j + 1]):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S5 6.3.2', 'switch 缺少 default 分支'))
# S6 (7.5) 不安全字符串/输入函数
for m in re.finditer(r'\b(strcpy|strcat|sprintf|gets|scanf|strtok)\s*\(', text):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S6 7.5', '不安全字符串/输入函数: %s (改用安全版本)'
% m.group(1)))
# S7 (6.3.3) 禁止 goto
m = re.search(r'\bgoto\s+[A-Za-z_]\w*', text)
if m:
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S7 6.3.3', '禁止使用 goto'))
# S8 (4.1.1) 禁止 Tab
if '\t' in text:
for idx, line in enumerate(lines, 1):
if '\t' in line:
v.append((idx, 'S8 4.1.1', '禁止 Tab 缩进(使用 4 个空格)'))
break
# S9 (5.3) 函数定义必须有函数注释
for idx, line in enumerate(lines, 1):
if (FUNC_DEF_RE.match(line) and '=' not in line and not CTRL_RE.match(line)):
window = '\n'.join(lines[max(0, idx - 26):idx - 1])
if '函数功能' not in window:
v.append((idx, 'S9 5.3', '函数定义缺少函数注释(函数功能): %s'
% line.strip()[:50]))
masked = _mask(text)
mlines = masked.splitlines()
defs = _fn_defs(masked)
# S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
for name, ln, _body, _pre, _bl, _pl in defs:
if re.search(r'[A-Z]', name) and not name.startswith('HAL_'):
v.append((ln, 'S10 3.3.1', '函数名须全小写: %s' % name))
# S11 (2.3.5) 禁止相对/绝对路径 include(基于原始文本,字符串掩码会吃掉引号)
# 规则本意 = 禁"用路径找文件":`#include "../../x.h"`、`#include "App/foo/x.h"`、
# `#include <C:/x.h>`、`#include <..\..\x.h>`。
# ⚠ 2026-10-11 细化:原实现把**任何含 `/` 的 include** 都判违规,于是
# `<sys/stat.h>` 这类**尖括号系统头**也被误报(模拟器侧两处即中招)。
# 现按形态区分:尖括号 = 系统/依赖头,允许带子目录(由 -I 解析);
# 引号 = 项目头,必须裸文件名(带 `/` 或 `\` 即违规)。`..`/绝对路径两种形态都禁。
for m in re.finditer(r'^\s*#\s*include\s*([<"])([^">]+)[">]', text, re.M):
form, inc = m.group(1), m.group(2)
bad = ('..' in inc or '\\' in inc or inc.startswith('/')
or re.match(r'^[A-Za-z]:', inc) is not None)
if form == '"' and '/' in inc:
bad = True
if bad:
v.append((_line(text, m.start()), 'S11 2.3.5',
'禁止相对/绝对路径 include: %s' % inc))
# S12 (7.6) 禁止可变参数函数
for m in re.finditer(r'\(\s*[^();{}]*\.\.\.[^();{}]*\)\s*[;{]', masked):
v.append((_line(masked, m.start()), 'S12 7.6', '禁止可变参数函数(...)'))
# S13 (7.7) 禁止递归函数
for name, ln, body, _pre, _bl, _pl in defs:
if re.search(r'\b%s\s*\(' % re.escape(name), body):
v.append((ln, 'S13 7.7', '递归函数: %s' % name))
# S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
for idx, line in enumerate(mlines, 1):
if not line or line[0] in ' \t':
continue
if not line.rstrip().endswith(';') or '(' in line or '#' in line:
continue
first = line.split(None, 1)[0]
if first in ('extern', 'typedef', 'struct', 'enum', 'union', 'static',
'return', 'if', 'for', 'while', 'switch', 'do', 'else'):
continue
if '*' not in line or '=' in line:
continue
v.append((idx, 'S15 6.1.4', '文件作用域指针变量须初始化为 NULL: %s'
% line.strip()[:50]))
# S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
for idx, line in enumerate(mlines, 1):
if not line or line[0] in ' \t' or line[0] in '}]':
continue
if not line.rstrip().endswith(';') or '(' in line or '#' in line:
continue
first = line.split(None, 1)[0]
if first in ('extern', 'typedef', 'struct', 'enum', 'union', 'static',
'const', 'volatile', 'return', 'if', 'for', 'while',
'switch', 'do', 'else'):
continue
m2 = re.search(r'([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;\s*$', line)
if not m2:
continue
if not m2.group(1).startswith('g_'):
v.append((idx, 'S16 3.2.2', '非 static 全局变量须 g_ 前缀: %s'
% line.strip()[:50]))
# S17 (2.2) 非 static 函数必须在配对头文件声明(HAL_ 豁免;无配对头则跳过)
if path.endswith('.c'):
hpath = path[:-2] + '.h'
if os.path.exists(hpath):
with open(hpath, 'r', encoding='utf-8', errors='replace') as f:
htext = f.read()
for name, ln, _body, pre, _bl, _pl in defs:
if name.startswith('HAL_'):
continue
seg = pre[pre.rfind(';') + 1:]
seg = seg[seg.rfind('}') + 1:]
if re.search(r'\bstatic\b', seg):
continue
if re.search(r'\b%s\s*\(' % re.escape(name), htext):
continue
v.append((ln, 'S17 2.2', '非 static 函数须在配对头文件声明: %s' % name))
# S18 (6.3.1) 禁止 Yoda 条件(常量在右)
for m in re.finditer(r'\b(0[xX][0-9a-fA-F]+(?:[uUlL]+)?|\d+(?:[uUlL]+)?'
r'|NULL|true|false)\s*(==|!=|<=|>=|<|>)\s+[A-Za-z_]',
masked):
v.append((_line(masked, m.start()), 'S18 6.3.1',
'Yoda 条件(常量应在右): %s' % ' '.join(m.group(0).split())))
# S19 (4.4.2) 函数定义左大括号必须与右括号同行
for name, ln, _body, _pre, bl, pl in defs:
if bl != pl:
v.append((ln, 'S19 4.4.2', '函数定义左大括号须与右括号同行: %s' % name))
return v
def main():
failed = False
for path in sys.argv[1:]:
try:
v = check_file(path)
except OSError as e:
print('cannot read %s: %s' % (path, e))
failed = True
continue
if v:
failed = True
print('%s: %d 处规范违规' % (path, len(v)))
for ln, rule, msg in v:
print(' L%-4s [%s] %s' % (ln, rule, msg))
if failed:
sys.exit(1)
if __name__ == '__main__':
main()