#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ C 语言代码规范检查(《嵌入式C语言代码规范_V1.0》可机械化校验的规则) 由 pre-commit 调用:python scripts/check_coding_standard.py ,发现违规退出码 1。 检查项(括号内为规范条款号): CODE-RULE-01 (2.3.1) .h 文件必须有 XXX_H 格式的头文件保护宏(禁止 __ 双下划线前缀) CODE-RULE-02 (5.2) 文件开头必须有文件头注释(含 模块名称 / 作者) CODE-RULE-03 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字 CODE-RULE-04 (3.5.1) typedef 类型名必须以 _t 结尾 CODE-RULE-05 (6.3.2) switch 必须包含 default 分支 CODE-RULE-06 (7.5) 禁用不安全字符串/输入函数 strcpy/strcat/sprintf/gets/scanf/strtok CODE-RULE-07 (6.3.3) 禁止使用 goto CODE-RULE-08 (4.1.1) 禁止 Tab 缩进(使用 4 个空格) CODE-RULE-09 (5.3) 函数定义必须有函数注释(含「函数功能」) CODE-RULE-10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免) CODE-RULE-11 (2.3.5) 禁止相对/绝对路径 include CODE-RULE-12 (7.6) 禁止可变参数函数(...) CODE-RULE-13 (7.7) 禁止递归函数 CODE-RULE-15 (6.1.4) 文件作用域指针变量必须初始化为 NULL CODE-RULE-16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀 CODE-RULE-17 (2.2) 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过) CODE-RULE-18 (6.3.1) 禁止 Yoda 条件(字面量/常量必须在运算符右侧) CODE-RULE-19 (4.4.2) 函数定义左大括号必须与右括号同行 CODE-RULE-20 (平台约束) 禁止 C 库堆分配 malloc/calloc/realloc/strdup/free 语义类规则(函数长度、单一职责、错误处理等)无法机械可靠判定,不在此脚本范围。 """ 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*\(') # CODE-RULE-20 的例外:平台分配后端的**宿主侧实现**(AGENTS.md「只用 pvPortMalloc/vPortFree, # 或把分配收进平台后端」说的就是这种收口点)。只点名到文件 + 只放行这两个函数名, # 不做目录级豁免——新增一个想 malloc 的文件必须显式加进这里,评审时看得见。 HEAP_ALLOC_ALLOWLIST = { 'simulator/sim/cfg_file_store_sim.c': frozenset({'malloc', 'free'}), } 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 = [] # (行号或'-', 规则, 说明) # CODE-RULE-01 (2.3.1) .h 头文件保护宏 if path.endswith('.h') and \ not re.search(r'#\s*ifndef\s+(?`、`#include <..\..\x.h>`。 # ⚠ 2026-10-11 细化:原实现把**任何含 `/` 的 include** 都判违规,于是 # `` 这类**尖括号系统头**也被误报(模拟器侧两处即中招)。 # 现按形态区分:尖括号 = 系统/依赖头,允许带子目录(由 -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()), 'CODE-RULE-11 2.3.5', '禁止相对/绝对路径 include: %s' % inc)) # CODE-RULE-12 (7.6) 禁止可变参数函数 for m in re.finditer(r'\(\s*[^();{}]*\.\.\.[^();{}]*\)\s*[;{]', masked): v.append((_line(masked, m.start()), 'CODE-RULE-12 7.6', '禁止可变参数函数(...)')) # CODE-RULE-13 (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, 'CODE-RULE-13 7.7', '递归函数: %s' % name)) # CODE-RULE-15 (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, 'CODE-RULE-15 6.1.4', '文件作用域指针变量须初始化为 NULL: %s' % line.strip()[:50])) # CODE-RULE-16 (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, 'CODE-RULE-16 3.2.2', '非 static 全局变量须 g_ 前缀: %s' % line.strip()[:50])) # CODE-RULE-17 (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, 'CODE-RULE-17 2.2', '非 static 函数须在配对头文件声明: %s' % name)) # CODE-RULE-18 (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()), 'CODE-RULE-18 6.3.1', 'Yoda 条件(常量应在右): %s' % ' '.join(m.group(0).split()))) # CODE-RULE-19 (4.4.2) 函数定义左大括号必须与右括号同行 # CODE-RULE-20 (平台约束,2026-10-23) 禁止 C 库堆分配 # 出处**不在规范 V1.0**(其 §6.1 示例本身就写着 malloc/free,属通用 C 模板遗留,已同日在示例 # 下方加"本仓禁止"批注),而在 AGENTS.md「平台约束」+ docs/内存管理/RAM使用分析与优化方案.md: # MicroLIB 的 C 堆由 startup_stm32f407xx.s 的 Heap_Size EQU 0x200 限定为 **512 B**,KB 级分配 # 必然返回 NULL,而**模拟器(宿主 CRT 堆)完全正常、照不出来** —— 典型"只有真机才炸",必须机器拦。 # 判据细节(三条都是踩过坑之后定的): # - 大小写敏感:pvPortMalloc / vPortFree(唯一允许的配对)不会中招; # - 词边界 \b:dhara_nand_is_free(、buf_alloc( 这类"同形后缀"的自有函数名不会误报; # - 跑在 masked 上:注释与字符串字面量里提到 malloc 不报(规范示例就是这种命中)。 # 覆盖名比 AGENTS 列的五枚多带上 strndup / aligned_alloc —— 同族同类,漏一个就等于留个后门。 norm = path.replace('\\', '/') allow = frozenset() for suffix, names in HEAP_ALLOC_ALLOWLIST.items(): if norm == suffix or norm.endswith('/' + suffix): allow = names break for m in re.finditer(r'\b(malloc|calloc|realloc|strdup|strndup|free|aligned_alloc)\s*\(', masked): name = m.group(1) if name in allow: continue v.append((_line(masked, m.start()), 'CODE-RULE-20 平台约束', '禁止 C 库堆分配 %s()(MicroLIB C 堆仅 512 B;用 pvPortMalloc/vPortFree,' '或把分配收进平台后端)' % 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()