diff --git a/App/time_sync.c b/App/time_sync.c index 21ebe21d..5ea15c22 100644 --- a/App/time_sync.c +++ b/App/time_sync.c @@ -104,7 +104,7 @@ static void time_sync_handle_conn(int conn_sock) { * 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。 * 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。 */ -void timeSyncTask(void *argument) { +void time_sync_task(void *argument) { (void)argument; /* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息 */ diff --git a/App/time_sync.h b/App/time_sync.h index e1b5c7ef..b025f225 100644 --- a/App/time_sync.h +++ b/App/time_sync.h @@ -31,7 +31,7 @@ extern "C" { * 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。 * 该任务与 FTP 等其它网络任务并发安全。 */ -void timeSyncTask(void *argument); +void time_sync_task(void *argument); #ifdef __cplusplus } diff --git a/Src/freertos.c b/Src/freertos.c index d3ecac37..24959041 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -176,7 +176,7 @@ void MX_FREERTOS_Init(void) { #endif /* 本地时间同步任务(PC 推送 TCP):依赖 netTask 处理消息队列 */ - timeSyncTaskHandle = osThreadNew(timeSyncTask, NULL, &timeSyncTask_attributes); + timeSyncTaskHandle = osThreadNew(time_sync_task, NULL, &timeSyncTask_attributes); /* USER CODE END RTOS_THREADS */ /* Create the event(s) */ diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 25454b98..5d5668f4 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -131,12 +131,21 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | S3 | 3.4.1 | `#define` 宏名必须全大写+下划线+数字 | | S4 | 3.5.1 | typedef 类型名必须以 `_t` 结尾 | | S5 | 6.3.2 | switch 必须包含 default 分支 | -| S6 | 7.5 | 禁用 strcpy/strcat/sprintf/gets(改用 strncpy/strncat/snprintf) | +| 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 | 函数定义左大括号必须与右括号同行 | -**不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、递归、注释掉的代码等,仍需人工评审把关。 +**不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。 **排除文件**(与 cppcheck 钩子范围一致,另加):`Src/`、`Inc/`(CubeMX 生成,重新生成即覆盖)、`Drivers/BSP/GD5F2GQ5UE/nand_ftl.*`(dhara/FatFS 接口适配层,函数名由外部 API 固定)。 @@ -267,7 +276,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) | | `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 | | `scripts/check_coding_standard.py` | scripts/ | 代码规范检查脚本(规范 V1.0 可机械校验项) | -| `docs/代码规范/嵌入式C语言代码规范(V1.0).md` | docs/代码规范/ | 代码规范原文(S1~S9 规则来源) | +| `docs/代码规范/嵌入式C语言代码规范(V1.0).md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) | | `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) | | `docs/代码规范/pre-commit使用说明.md` | 本文档 | 部署与使用说明 | diff --git a/scripts/check_coding_standard.py b/scripts/check_coding_standard.py index 1990a803..23f7acbe 100644 --- a/scripts/check_coding_standard.py +++ b/scripts/check_coding_standard.py @@ -10,13 +10,23 @@ C 语言代码规范检查(《嵌入式C语言代码规范(V1.0)》可机 S3 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字 S4 (3.5.1) typedef 类型名必须以 _t 结尾 S5 (6.3.2) switch 必须包含 default 分支 - S6 (7.5) 禁用不安全字符串函数 strcpy/strcat/sprintf/gets + 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 @@ -26,6 +36,77 @@ FUNC_DEF_RE = re.compile( ) 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: @@ -80,10 +161,10 @@ def check_file(path): 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)\s*\(', text): + # 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 (改用 strncpy/strncat/snprintf)' + v.append((ln, 'S6 7.5', '不安全字符串/输入函数: %s (改用安全版本)' % m.group(1))) # S7 (6.3.3) 禁止 goto @@ -107,6 +188,93 @@ def check_file(path): 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(基于原始文本,字符串掩码会吃掉引号) + for m in re.finditer(r'^\s*#\s*include\s*[<"]([^">]+)[">]', text, re.M): + inc = m.group(1) + if '/' in inc or '\\' in inc or '..' in inc: + 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