1. 从零搭建代码评审工具:为什么我要自己造这个轮子
第一次听到"open-code-review"这个说法,是在一次内部技术分享会上。当时团队里有人抱怨,说每次代码合并请求一多,评审就变成走过场——要么是资深同事忙得没时间细看,要么是新人不敢提意见,最后代码质量全靠运气。这个场景我相信很多人都遇到过:代码评审本该是质量守门员,结果却成了流程里最容易被敷衍的一环。
我后来花了不少时间琢磨这件事,也试过市面上各种现成的方案,但总感觉差那么点意思。要么太重,部署一套下来光配置就够折腾半天;要么太轻,只能做做格式检查,真正的逻辑问题一个都抓不住。于是我就想,能不能自己动手做一个轻量、可扩展、真正能落地的代码评审工具?这就是"open-code-review"这个项目诞生的背景。
说白了,这个项目要解决的核心问题就三个:第一,让代码评审有统一的检查标准,不依赖某个人的经验和心情;第二,把重复性的检查工作自动化,让人把精力放在真正需要判断力的地方;第三,评审结果要能沉淀下来,形成团队自己的知识库。它适合所有正在被代码质量困扰的开发团队,不管你是三五人的小团队还是几十人的中型团队,只要你有代码评审的需求,这套思路都能直接拿去用。
我见过太多团队在代码评审上走两个极端:一种是把评审当成形式,点个通过就完事;另一种是定了一堆规则,结果没人执行,最后规则全躺在文档里吃灰。这个项目的设计初衷,就是在这两个极端之间找到一条能真正跑通的路。接下来我会把整个搭建过程、踩过的坑、以及那些文档里不会写的经验,一点一点拆开讲清楚。
2. 代码评审工具的核心能力拆解与需求优先级排序
2.1 先想清楚:代码评审到底在评什么
很多人一上来就想着用什么工具、接什么钩子,但我觉得第一步应该是把"评什么"这件事想明白。代码评审的检查项其实可以分成几个层次,我习惯把它们分成四类:
- 格式与规范类:缩进、命名、注释、文件结构这些,这类问题最适合自动化,人去看纯属浪费时间。
- 逻辑与正确性类:边界条件、异常处理、并发安全、资源释放,这类需要一定的静态分析能力,但也有很多模式可以固化下来。
- 设计与架构类:模块划分、接口设计、依赖关系,这类最依赖人的判断,工具只能辅助提示。
- 业务与需求类:实现是否符合需求、有没有遗漏场景,这类基本只能靠人,但工具可以帮忙做检查清单。
把这四类想清楚之后,你就会发现一个很关键的事实:不是所有检查都值得自动化,也不是所有自动化都能替代人。我见过有人试图用工具去判断"这段代码设计得好不好",结果就是规则越写越多,误报率越来越高,最后没人看报告了。
所以我在设计这个项目的时候,定了一个原则:工具负责"确定性高、重复性高、人容易漏"的检查,人负责"需要上下文、需要判断、需要权衡"的评审。这个边界划清楚了,后面的技术选型和功能设计才不会跑偏。
2.2 需求优先级:哪些功能先做,哪些可以往后放
需求列了一堆,但资源有限,必须排优先级。我用的方法很简单:按"痛感强度"和"实现成本"两个维度打分,优先做那些痛感强、成本低的。
| 需求 | 痛感强度 | 实现成本 | 优先级 |
|---|---|---|---|
| 代码格式自动检查 | 高 | 低 | P0 |
| 常见缺陷模式扫描 | 高 | 中 | P0 |
| 评审结果统一展示 | 高 | 低 | P0 |
| 与代码托管平台集成 | 中 | 中 | P1 |
| 自定义规则配置 | 中 | 中 | P1 |
| 历史评审数据统计 | 低 | 高 | P2 |
| 智能修复建议 | 中 | 高 | P2 |
这个排序背后的逻辑是:先让工具跑起来,再让它跑得好。P0的功能做完,工具就已经能产生实际价值了;P1是让它更好用;P2是锦上添花,等前面跑稳了再说。
我特别想强调一点:不要一上来就追求"大而全"。我见过一个团队,花了三个月做了一个功能极其丰富的评审系统,结果因为配置太复杂,上线之后没人愿意用。反而是另一个团队,先用一个简单的脚本把格式检查跑起来,大家用顺手了,再慢慢加功能,最后效果反而更好。
2.3 技术选型:为什么我选了这套组合
技术选型这块,我主要考虑三个因素:上手门槛、扩展能力、维护成本。
语言层面我选了 Python,原因很直接:团队里大部分人都会,写规则方便,生态里现成的解析库多。如果你团队是 Java 背景,换成 Java 也完全没问题,思路是一样的。
代码解析这块,我没有自己从头写解析器,而是用了现成的抽象语法树(AST)解析库。这里有个经验:不要试图用正则表达式去做代码分析。我一开始也想过用正则匹配一些简单模式,结果发现稍微复杂一点的情况就各种误报漏报,比如字符串里出现的代码片段、注释里的示例代码,正则根本区分不了。用 AST 之后,这些问题自然就解决了,因为解析器已经帮你把语法结构理清楚了。
规则引擎的设计我参考了"检查器模式":每个检查规则是一个独立的类,实现统一的接口,输入是 AST 节点,输出是问题列表。这样做的好处是,加新规则只需要写一个新类,不用改核心逻辑。规则之间互不干扰,某个规则出问题也不会影响其他规则。
存储这块,我用的是轻量级的嵌入式数据库,评审结果直接存本地文件。为什么不直接上 MySQL 或者 PostgreSQL?因为对于大多数团队来说,评审数据的量级并不大,用嵌入式数据库省去了部署和维护的成本,而且迁移起来也方便。等数据量真的上来了,再换也不迟。
3. 规则引擎的设计思路:让检查规则像插件一样即插即用
3.1 检查器的抽象接口设计
规则引擎是整个工具的核心,它的设计好坏直接决定了后续加规则的难易程度。我设计的检查器接口大概长这样:
class BaseChecker: name = "base_checker" description = "基础检查器" def check(self, ast_node, context): """ 输入:AST节点和上下文信息 输出:问题列表,每个问题包含位置、严重级别、描述 """ raise NotImplementedError这个接口看起来简单,但有几个设计细节值得说一下。
第一,context参数。一开始我没加这个参数,后来发现很多检查需要跨文件的信息,比如"这个函数在别的地方有没有被调用"、"这个变量在别的文件里有没有定义"。加上 context 之后,检查器就能拿到全局信息,能力一下子扩展了很多。
第二,返回的问题列表里,我强制要求包含位置信息和严重级别。位置信息是为了能精确定位到代码行,严重级别是为了后续做过滤和排序。这两个字段看起来不起眼,但实际用起来非常关键——没有位置信息,报告就是一堆废话;没有严重级别,用户就会被一堆低优先级问题淹没。
第三,我特意没有在接口里定义"修复方法"。原因是我觉得自动修复这件事风险太高,尤其是在代码评审场景下,改错了比不改还麻烦。工具应该做的是"指出问题",而不是"替人做决定"。
3.2 内置规则集:从哪些检查开始做起
内置规则我没有贪多,第一批只做了十来条,但都是经过实战验证、误报率极低的。我把它们分成几组:
格式规范组:
- 行长度超限检查
- 命名规范检查(变量、函数、类)
- 未使用的导入检查
- 未使用的变量检查
缺陷模式组:
- 可变默认参数检查(Python 里很经典的坑)
- 异常捕获过于宽泛检查
- 资源未正确释放检查
- 空比较检查(比如
== None应该用is None)
复杂度组:
- 函数过长检查
- 嵌套层级过深检查
- 圈复杂度过高检查
这里我想重点说一下可变默认参数这个检查。这是 Python 里一个非常经典的坑:
def add_item(item, target_list=[]): target_list.append(item) return target_list这个函数看起来没问题,但实际上target_list在函数定义时就被创建了,所有调用共享同一个列表。这种 bug 非常隐蔽,人工评审很容易漏掉,但用 AST 检查就很简单——只要检测到默认参数是可变对象(列表、字典、集合),就报出来。
类似的还有异常捕获过于宽泛:
try: do_something() except: pass这种写法会把所有异常都吞掉,包括键盘中断和系统退出。检查规则就是检测except后面有没有指定异常类型,以及有没有pass这种空处理。
这些规则的特点是:规则本身很简单,但能抓到真实存在的问题。我建议你在做自己的规则集时,也遵循这个原则——宁可少几条,也要保证每条都是真正有用的。
3.3 规则的配置与开关机制
规则不能写死,必须能配置。我设计了一个配置文件,用 YAML 格式,大概长这样:
rules: line_length: enabled: true max_length: 120 severity: warning unused_import: enabled: true severity: error function_length: enabled: true max_lines: 50 severity: warning complexity: enabled: false max_complexity: 10这个配置机制有几个考虑:
第一,每条规则可以单独开关。有些团队可能觉得某条规则太严格,直接关掉就行,不用改代码。
第二,参数可调。比如行长度限制,有的团队用 80,有的用 120,配置里改一下就行。
第三,严重级别可配。同一个问题,在不同团队可能重视程度不一样。比如未使用的导入,有的团队觉得是错误,有的觉得是警告,配置里可以调。
我还加了一个规则继承机制:可以定义一个基础配置,然后不同项目继承这个基础配置,只覆盖需要改的部分。这样多个项目之间可以共享大部分配置,维护起来方便很多。
提示:配置文件建议纳入版本管理,这样规则的变化历史可以追溯,也方便团队协作。
4. 从代码提交到评审报告:完整链路的搭建过程
4.1 触发时机:什么时候跑检查最合适
检查什么时候跑,这个看似简单的问题其实很有讲究。我试过几种方案,各有优劣:
方案一:提交时本地跑。用 Git 钩子在pre-commit阶段触发。优点是反馈最快,问题在提交前就能发现;缺点是本地环境差异大,有的人配了钩子有的人没配,而且本地跑会拖慢提交速度。
方案二:推送时服务端跑。在代码推送到远端后触发。优点是环境统一,不依赖本地配置;缺点是反馈有延迟,而且如果检查不通过,代码已经推上去了。
方案三:合并请求时跑。在创建合并请求时触发。优点是时机刚好,评审和检查可以一起看;缺点是需要和代码托管平台集成。
我最后采用的是组合方案:本地钩子做快速检查(只跑格式和明显的缺陷),服务端做完整检查(跑所有规则)。这样既保证了快速反馈,又保证了检查的完整性。
本地钩子的配置大概是这样:
#!/bin/bash # .git/hooks/pre-commit python -m open_code_review --mode fast --staged-only if [ $? -ne 0 ]; then echo "检查未通过,请修复后再提交" exit 1 fi这里有个细节:--staged-only参数表示只检查暂存区的文件,而不是整个项目。这样速度会快很多,尤其是大项目。
4.2 检查执行流程:从文件到问题的完整路径
一次完整的检查,内部大概经过这几个步骤:
第一步:文件收集。根据触发模式,确定要检查哪些文件。如果是本地钩子,就取暂存区的文件;如果是服务端,就取本次推送涉及的文件。
第二步:语法解析。对每个文件做 AST 解析。这里有个坑:如果文件本身有语法错误,解析会失败。我的处理方式是,解析失败的文件单独标记出来,提示"文件存在语法错误,无法检查",而不是直接报错退出。
第三步:规则执行。遍历所有启用的规则,对每个 AST 节点执行检查。这里我用了一个小优化:把规则按"关注的节点类型"分组,比如有的规则只关心函数定义,有的只关心导入语句。这样遍历 AST 的时候,只需要对每个节点执行关注它的规则,而不是所有规则,效率高很多。
第四步:结果聚合。把所有规则产生的问题收集起来,按文件、按行号排序,去掉重复的问题。
第五步:报告生成。把结果格式化成可读的报告,输出到控制台或者写入文件。
这个流程看起来简单,但每一步都有细节。比如第三步的优化,如果不做的话,规则一多就会明显变慢。我实测下来,一个中等规模的项目(大概几百个文件),不做优化要跑十几秒,做了优化之后两三秒就能跑完。
4.3 报告输出:怎么让结果一目了然
报告的输出格式,我改了好几版。第一版是纯文本,结果就是一大坨,根本看不清重点。第二版加了颜色,好一点,但还是不够。第三版改成了分组展示,按严重级别分组,每个问题显示文件、行号、描述,这才算能用。
现在的报告大概长这样:
检查完成,共发现 12 个问题: [错误] 3 个 src/utils.py:45 未使用的导入: os src/main.py:12 可变默认参数: target_list src/api.py:78 异常捕获过于宽泛 [警告] 7 个 src/utils.py:23 行长度超限 (135 > 120) ... [提示] 2 个 ...除了控制台输出,我还支持生成 JSON 格式的报告,方便和其他系统集成。JSON 报告的结构大概是这样:
{ "summary": { "total": 12, "error": 3, "warning": 7, "info": 2 }, "issues": [ { "file": "src/utils.py", "line": 45, "column": 1, "severity": "error", "rule": "unused_import", "message": "未使用的导入: os" } ] }这个 JSON 结构是刻意设计的:summary方便快速了解整体情况,issues里的每个字段都是结构化的,方便后续做统计和分析。
注意:报告里的文件路径建议用相对路径,不要用绝对路径。绝对路径会暴露本地目录结构,而且在不同机器上不一致,容易造成困惑。
5. 实际使用中踩过的坑与应对经验
5.1 误报问题:规则太严还是代码太烂
工具刚上线的时候,最大的问题就是误报。有一条规则是"函数长度不超过 50 行",结果有个同事写了一个 60 行的函数,被报出来了,他很不服气,说这个函数逻辑很清晰,拆开反而不好读。这件事让我意识到:规则的阈值不能一刀切,得给人留出判断空间。
我的应对方式是:把规则分成"强制"和"建议"两类。强制类的问题必须修复,建议类的问题只是提示,不阻塞流程。比如"未使用的导入"是强制类,因为这是确定的问题;"函数长度"是建议类,因为长度和可读性的关系并不是绝对的。
另外我还加了一个忽略机制:对于某些确实不需要检查的地方,可以在代码里加注释来忽略。比如:
def complex_function(): # noqa: complexity ...这个noqa注释会让检查器跳过这一行。但我也做了限制:忽略注释必须写明原因,不能随便忽略。这样既给了灵活性,又防止了滥用。
5.2 性能问题:大项目怎么跑得快
项目大了之后,检查速度就成了问题。我遇到过一次,一个包含上千个文件的项目,完整检查要跑将近一分钟,本地钩子根本没法用。
优化的思路有几个:
第一,增量检查。只检查本次改动涉及的文件,而不是全量检查。这个在本地钩子和服务端都能用,效果最明显。
第二,缓存解析结果。AST 解析是比较耗时的操作,如果文件没变,解析结果可以缓存起来。我用文件内容的哈希值作为缓存键,文件没变就直接用缓存。
第三,并行执行。多个文件的检查可以并行跑。Python 里用多进程或者多线程都行,我用的是多进程,因为 AST 解析是 CPU 密集型操作,多进程能真正利用多核。
第四,规则分级。把规则分成"快速规则"和"慢速规则",本地钩子只跑快速规则,完整检查才跑所有规则。
这几个优化做完之后,本地钩子的检查时间从十几秒降到了一秒以内,完整检查也从一分钟降到了十秒左右。
5.3 团队推广:怎么让人愿意用
工具做出来了,但没人用,这是最尴尬的。我见过太多内部工具死在这个环节。我的经验是:推广的关键不是功能多强,而是让人感受到"用了确实省事"。
具体做法有几个:
第一,先在小范围试点。找几个愿意尝试的同事,先用起来,收集反馈,把明显的问题修掉。等工具有了一定口碑,再往大范围推。
第二,把工具集成到现有流程里。不要让人额外去学一个新工具,而是把它嵌入到大家已经在用的流程里。比如集成到代码托管平台的合并请求里,评审的时候自动显示检查结果。
第三,让规则可讨论。规则不是一成不变的,团队可以讨论哪些规则要加、哪些要改、哪些要删。这样大家会有参与感,而不是被动接受。
第四,展示价值。定期统计一下工具发现了多少问题、避免了多少潜在 bug,用数据说话。我做过一次统计,工具上线三个月,发现了二十多个真实的 bug,其中有好几个是会导致线上问题的。这个数据一出来,大家的接受度明显提高了。
6. 规则集的持续演进:从能用 to 好用
6.1 根据实际 bug 反推规则
规则集不是拍脑袋想出来的,最好的来源是实际发生过的 bug。我有个习惯:每次线上出问题,复盘的时候都会问一句"这个问题能不能用规则提前发现"。如果能,就加一条规则。
举个例子,有一次线上出了一个空指针的问题,原因是某个函数的返回值没有做空值检查。复盘之后,我加了一条规则:检测函数返回值被直接使用而没有做空值判断的情况。这条规则后来还真抓到了几个类似的问题。
这种"从 bug 反推规则"的方式,好处是规则都是有针对性的,不会出现"为了检查而检查"的情况。而且因为每条规则背后都有一个真实的故事,推广的时候也更有说服力。
6.2 规则的误报率监控
规则加多了之后,误报率就成了一个需要持续关注的问题。我的做法是:给每条规则记录误报次数。当用户标记某个问题是误报时,就记录一次。如果某条规则的误报率超过一定阈值,就把它下线或者调整。
这个机制需要用户的配合,所以我做了一个简单的反馈功能:在报告里,每个问题后面都有一个"标记为误报"的选项。用户点一下就行,不需要填任何东西。
数据积累起来之后,就能看出哪些规则是真正有用的,哪些是添乱的。我统计过,大概有 20% 的规则贡献了 80% 的有效问题,而有些规则几乎全是误报。把那些高误报的规则下线之后,整体的用户体验好了很多。
6.3 自定义规则的扩展方式
内置规则不可能覆盖所有场景,所以必须支持自定义规则。我的设计是:自定义规则就是一个 Python 文件,放在指定的目录下,工具启动时自动加载。
一个自定义规则的例子:
from open_code_review.checker import BaseChecker class NoPrintChecker(BaseChecker): name = "no_print" description = "禁止在生产代码中使用 print" def check(self, ast_node, context): issues = [] if isinstance(ast_node, ast.Call): if isinstance(ast_node.func, ast.Name): if ast_node.func.id == "print": issues.append({ "line": ast_node.lineno, "column": ast_node.col_offset, "severity": "warning", "message": "生产代码中不建议使用 print,请使用日志" }) return issues这个规则检测代码里有没有直接用print。写起来很简单,因为基类已经把大部分工作做好了,自定义规则只需要关注"怎么判断"这一件事。
我还提供了一个规则模板生成器,运行一个命令就能生成一个规则文件的骨架,填上检查逻辑就行。这样即使不熟悉内部实现的人,也能快速写出自己的规则。
7. 一些关于代码评审的思考
做这个项目的过程中,我对代码评审这件事有了更深的理解。工具能解决很多问题,但有些问题工具解决不了。
比如,工具能发现"这个函数太长了",但发现不了"这个函数的职责不清晰"。工具能发现"这个变量没用到",但发现不了"这个变量的命名有歧义"。工具能发现"这里没有做空值检查",但发现不了"这里的业务逻辑理解错了"。
所以我的结论是:工具的价值在于把人从重复劳动中解放出来,让人有更多精力去做真正需要判断力的事情。它不是要替代人,而是要让人做更值得做的事。
另外一点体会是,代码评审的文化比工具更重要。如果团队里大家都不重视代码质量,再好的工具也是摆设。反过来,如果团队有良好的评审文化,即使工具简陋一点,效果也不会差。工具是辅助,文化是根本。
我在实际使用中还发现一个有意思的现象:当工具把那些低级问题都自动检查掉之后,评审时的讨论质量明显提高了。以前评审经常在纠结"这个变量名是不是合适"、"这里要不要加个注释",现在这些都被工具处理了,大家讨论的都是"这个设计是不是合理"、"这个方案有没有更好的选择"。这种变化是潜移默化的,但确实能感觉到。
最后分享一个小技巧:如果你也在做类似的工具,建议从一开始就考虑可观测性。记录每次检查的耗时、发现的问题数、规则的触发次数,这些数据能帮你了解工具的运行状况,也能为后续优化提供依据。我一开始没做这个,后来补上的时候发现很多历史数据都拿不到了,挺可惜的。