Files
STM32F4-Base/scripts/check_coding_standard.py
T
wandering1 7f0087d430 新增S10~S19代码规范检查并统一time_sync_task命名
check_coding_standard.py 新增 9 条机械检查(S14 并入 S6):
- S10 (3.3.1) 函数名全小写(HAL_ 厂商回调豁免)
- 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 函数须在配对头文件声明
- S18 (6.3.1) 禁止 Yoda 条件(常量在右)
- S19 (4.4.2) 函数定义左大括号与右括号同行
S6 词表扩充 scanf/strtok
timeSyncTask 重命名为 time_sync_task(3 处代码引用)
更新 pre-commit 使用说明规则表
2026-09-01 17:38:43 +08:00

301 lines
12 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(基于原始文本,字符串掩码会吃掉引号)
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
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()