Files
STM32F4-Base/scripts/check_coding_standard.py
T
wandering1 301c3e5183 feat(门禁): 编号门禁 id-check 落地(36→37 钩子)+ 批 1 编号单词化改名
新增门禁本体
- scripts/check_ids.py:实现 docs/编号规范.md §8 八项校验;默认 warn-only,
  唯一硬拦项是「可判定比例跌破基线」(防回退的本体)
- scripts/id_registry.json:66 个前缀族,逐项来自 §4 总表 + 全库实测游标,
  偏差一律写进 cursor_note,不凭记忆编造
- scripts/id_aliases.json:旧号 → 新号 / 作废标记(照搬 section_aliases.json 范式)
- 顺带收编 check_test_traceability.py:正则补齐大写后缀与无模块码形态,
  比对范围扩到 test/host 与 7 篇「单元测试说明.md」,另加 --board-only 回退旧口径

批 1 改名(用户授权解禁「不改写存量号」)
- 代码规范条款 S1~S19 → CODE-RULE-nn:78 处 / 2 文件
- UI 像素回归状态 S0~S7 → MENU-STATE-nn:61 处 / 3 文件(冻结在 07)
- 两族属主登记处 = 脚本自身的规则表 / STATES 表,文档只作引用方
- 新增 scripts/rename_ids.py(§7.3 规划脚本):映射表驱动、显式 scope、默认干跑、
  自动写 scoped 别名,跳过 [示例]/[计划] 行与变更记录表,按原字节读写保 BOM 行尾
- 已应用映射表存档 scripts/rename_maps/batch1.json 作审计轨迹

有牙实测(证明门禁不是空转)
- 注入 Z-99 / 裸形 / 悬空 / 越游标 / 未紧邻 / 跨文档重复定义 六例,
  分别被校验 1/2/3/5/6/4 点名,--strict 退出码 1;临时档删除后工作树干净

验证与同步
- 可判定比例基线 82.3% → 82.7%(已 --update-baseline 入档);错误级 1312 → 1291
- ci_local.py 全绿(L0 + L0.5 共 1104 断言);ui-golden Passed;
  check_doc_links.py --all 无法判定 0
- 未改任何 C/H 源文件,故本次不需 @build
- 文档:AGENTS.md 与 pre-commit使用说明.md 计数 36→37 并新增 §2.25 详解;
  编号规范 §4.1 补两行、§8.1 落地记录、§8.2 规范与仓库偏差、§8.3 迁移批次记录
2026-10-21 09:33:54 +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。
检查项(括号内为规范条款号):
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) 函数定义左大括号必须与右括号同行
语义类规则(函数长度、单一职责、错误处理等)无法机械可靠判定,不在此脚本范围。
"""
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 = [] # (行号或'-', 规则, 说明)
# CODE-RULE-01 (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(('-', 'CODE-RULE-01 2.3.1', '缺少 XXX_H 格式的头文件保护宏(禁止 __ 前缀)'))
# CODE-RULE-02 (5.2) 文件头注释
head = '\n'.join(lines[:60])
if '模块名称' not in head and '作者' not in head:
v.append(('-', 'CODE-RULE-02 5.2', '缺少文件头注释(模块名称 / 作者)'))
# CODE-RULE-03 (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, 'CODE-RULE-03 3.4.1', '宏名须全大写+下划线: %s' % m.group(1)))
# CODE-RULE-04 (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, 'CODE-RULE-04 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, 'CODE-RULE-04 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
# CODE-RULE-05 (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, 'CODE-RULE-05 6.3.2', 'switch 缺少 default 分支'))
# CODE-RULE-06 (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, 'CODE-RULE-06 7.5', '不安全字符串/输入函数: %s (改用安全版本)'
% m.group(1)))
# CODE-RULE-07 (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, 'CODE-RULE-07 6.3.3', '禁止使用 goto'))
# CODE-RULE-08 (4.1.1) 禁止 Tab
if '\t' in text:
for idx, line in enumerate(lines, 1):
if '\t' in line:
v.append((idx, 'CODE-RULE-08 4.1.1', '禁止 Tab 缩进(使用 4 个空格)'))
break
# CODE-RULE-09 (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, 'CODE-RULE-09 5.3', '函数定义缺少函数注释(函数功能): %s'
% line.strip()[:50]))
masked = _mask(text)
mlines = masked.splitlines()
defs = _fn_defs(masked)
# CODE-RULE-10 (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, 'CODE-RULE-10 3.3.1', '函数名须全小写: %s' % name))
# CODE-RULE-11 (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()), '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) 函数定义左大括号必须与右括号同行
for name, ln, _body, _pre, bl, pl in defs:
if bl != pl:
v.append((ln, 'CODE-RULE-19 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()