c936441c83
本轮把"自研代码落在门禁之外"的口子收掉,同时落地 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)
311 lines
13 KiB
Python
311 lines
13 KiB
Python
#!/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()
|