基于 AST 的自定义代码规则检查器:从语法树到团队规范
一、正则表达式检查的力不从心
团队想禁某类写法,第一反应是用 grep。
比如"不允许直接 print 调试""必须走日志"。
正则能匹配字面,却看不懂结构。
print可能出现在字符串里、注释里、被调用的方法名里。
正则一刀切,误伤一片。
真正要拦的"函数调用",它反而认不准。
AST(抽象语法树)才是正解。
它理解代码的语法结构,能精准定位"调用表达式"。
本文探讨如何基于 AST 写自定义规则检查器。
二、AST 检查的运行机制
源码经解析器变成树,每个节点代表一个语法结构。
函数调用是Call节点,属性访问是Attribute节点。
遍历树,匹配特定节点模式,即触发规则。
与正则不同,AST 知道上下文。
它能区分"字符串里的 print"和"真的调用 print"。
这是结构化检查的准确性来源。
检查器遍历的关键在"访问者模式"。
对每个节点类型写对应处理逻辑。
命中即报,未命中则递归子节点。
三、生产级检查器实现
下面用ast写一个禁用print调用的检查器。
import ast from pathlib import Path from dataclasses import dataclass @dataclass class Violation: file: str line: int message: str class NoPrintChecker(ast.NodeVisitor): """访问者:只拦截真正的 print 调用,忽略字符串与注释""" def __init__(self, filename: str): self.filename = filename self.violations: list[Violation] = [] def visit_Call(self, node: ast.Call) -> None: func = node.func # 精确识别 print(...) 调用,而非名为 print 的变量 if isinstance(func, ast.Name) and func.id == "print": self.violations.append(Violation( file=self.filename, line=node.lineno, message="禁止直接使用 print,请走日志模块", )) self.generic_visit(node) def check_file(path: Path) -> list[Violation]: tree = ast.parse(path.read_text(encoding="utf-8")) checker = NoPrintChecker(str(path)) checker.visit(tree) return checker.violations if __name__ == "__main__": bad = check_file(Path("sample.py")) for v in bad: print(f"{v.file}:{v.line} {v.message}")真实检查器会支持配置规则列表。
每条规则是一个NodeVisitor子类,框架统一调度。
新增规则只需加类,不改主流程。
四、边界分析与架构权衡
AST 检查准确,但有适用范围。
语言绑定。每门语言要各自的解析器。
Python 用ast,TS 用typescript库。
多语言项目需多套实现,成本翻倍。
宏与语法糖。某些写法 AST 形态复杂。
比如装饰器、上下文管理器,规则要专门处理。
忽略这些,检查就会漏。
性能开销。大文件全量遍历有成本。
可只对变更文件检查,或增量解析。
全仓扫描放在预提交或 CI 低频跑。
与现成工具的关系。ESLint、Ruff 已覆盖大量规则。
自写 AST 检查应只补"它们没有的团队特例"。
不要重复造轮子,只补差异化。
AST 检查的"错误定位友好性"决定采纳率。规则报错只给行号不够,应附上"为什么违规"与"怎么改"的简短说明,让工程师一眼懂、立刻修。另一个被忽视的点是"规则的可配置性":不同团队对同一条规则严松不同,规则应支持按项目开关与调参,而非硬编码全局。最后,检查器自身要被测试,用已知违规与合规的样本做回归,防止检查器升级后误伤正常代码,否则团队会对所有告警失去信任。
五、总结
基于 AST 的自定义检查,本质是"用语法结构换准确性"。
机制上靠访问者遍历节点,精准匹配调用等结构。
工程上只补通用 linter 缺的团队特例。
落地路线:先识别需结构化判断的规则;写NodeVisitor子类;框架统一调度多规则;接预提交与 CI。正则看字面,AST 看结构,团队规范才有了可靠的守门员。