首页 / 资讯中心 / 文章详情

开源代码审查工具open-code-review:规则引擎与CI落地实践

开源代码审查工具open-code-review:规则引擎与CI落地实践 ★ FEATURED ARTICLE
1. 为什么我要自己做一套开源代码审查工具1.1 代码审查是刚需但痛点比想象中多先说个背景。我所在的团队前后端加起来不到二十人代码量随着业务增长涨得飞快。代码审查这件事我们一直靠人工交叉Review加几个Lint工具硬扛。刚开始还好到后面问题越来越多Reviewer没时间细看大部分时候只扫一眼格式和命名有经验的老手被拉去审代码经常跟前端后端十几个需求并行真正能静下心来逐行看的时间少得可怜新人提交的代码风格跟团队约定完全不一致靠人工一个个指出来又慢又容易漏。我印象最深的一次事故一个同事改了接口的返回结构调用方那边没有同步调整代码在Review的时候根本没人注意到这个调用链上线之后线上报错一片排查了大半天才发现是类型不匹配。事后复盘的时候大家都很无奈——PR描述写得不清楚代码变更太大Reviewer根本看不出改动影响到了哪里。这种场景做开发的基本都经历过。代码审查的本质是提前发现问题、统一规范、传递知识但人工审查的效率和质量随着代码规模增长衰减得非常厉害。大家不是不想做好Review是真的没有一套工具能把该看什么、重点看哪里、哪些地方容易出问题这件事系统性地支撑起来。我决定自己动手做一套开源的代码审查工具项目名就叫 open-code-review。目标很直接把代码审查从靠人眼硬扫变成人机协同的标准化流程——机器先跑一轮把重复性、规则性的问题全部挡下来人工只需要专注看逻辑、设计、调用链这些机器判断不了的东西。1.2 市面方案为什么不够用在动手之前我把市面上的方案摸了一遍。ESLint、Pylint、RuboCop这类Lint工具做得确实不错语法检查、风格检查、安全扫描全覆盖但也有限制——它们只管单文件的语法和风格跨文件的调用关系、变更影响范围、提交信息规范、分支状态这类代码审查场景真正在意的信息基本管不到。CodeClimate、SonarQube这类平台级工具我也试过。质量模型做得很全面能出各种维度的评分和报告但有两个问题让我最终放弃了它们。一是太重架设、配置、维护的成本都不低团队规模不大时性价比很差二是太封闭规则引擎虽然支持自定义但学习成本高改起来也不顺手我想加一个检查本次提交是否修改了接口定义却没有同步更新文档这种贴近业务场景的规则折腾了半天也没找到好办法。另一方面GitHub、GitLab自带的Review功能我也认真研究过。它们的思路是提供一套流程框架把Review的过程管理起来比如强制至少一个人批准、按文件维度评论、多轮对话等。但它默认你有人看至于这个Reviewer能不能看出问题完全取决于个人水平。每次都要靠人肉检查Diff里核心文件有没有被动过这种体力活完全可以交给工具。折腾完一圈之后我的结论是不是现有工具有多大缺陷而是它们的定位都在某个环节缺少一个可以让我按自己的审查逻辑自由定义的、轻量的、能嵌入到现有工作流里的工具。我要的其实很简单——告诉工具我们团队的审查规则是什么它就能在每次提交代码的时候自动跑一遍告诉我哪些文件需要重点看、哪个改动有风险、哪条规范被触犯了。这就是 open-code-review 最初的原型。2. 整体设计open-code-review 的架构思路2.1 核心设计目标动手写代码之前我把这个工具的核心目标梳理成四条后面所有的设计和取舍都围绕这四条展开。第一规则驱动而不是逻辑写死。代码审查的标准每个团队都不一样A团队要求函数不能超过50行B团队可能觉得80行都行C团队追求严格类型D团队还在用JavaScript。所以工具的核心不能是内置一堆规则然后让你用而是提供一个规则引擎让团队自己定义和调整规则。第二轻量可集成不绑架现有流程。我不希望引入一个工具就要把所有代码托管平台换掉或者强制团队成员到一个新的Web界面里看报告。它应该是一个命令行工具可以进Git Hook、可以进CI流程、也可以在本地手动跑输出结果能对接现有的IM通知、代码托管平台评论等。第三静态分析与变更分析并重。代码审查不像全量代码扫描它关注的是这次变更引入了什么问题。所以工具要能理解Git的提交信息知道这次改了哪些文件、增删了哪些行然后基于这个变更集合做精准分析而不是每次都把整个仓库扫一遍。第四渐进式落地不搞一刀切。新工具如果第一天上线就给团队甩出一堆错误大概率会被抵触。所以规则的严重级别要可以配可以先以warning模式跑一段时间让大家感受一下它的判断思路再逐步把规则收紧。这四个目标确定之后整个架构的轮廓就很清晰了最底层是Git变更数据采集中间是静态分析引擎上层是规则引擎最外面是多种形式的输出适配器。模块之间通过标准数据结构衔接互不依赖这样任何一层都可以单独替换或扩展。2.2 技术选型与模块划分技术选型这块我没选那些性能极致但开发成本高的底层语言最终定了Python。原因有几个团队里大部分人都会Python后续维护没有门槛Python的分析类库生态非常成熟不管是Git操作还是各类语言解析器都有现成的库可以借力开发速度足够快迭代一个原型可能只需要几天时间。性能方面对代码审查来说不是瓶颈后面专门做了优化跑一次全量变更的分析也就几秒钟。模块划分上我分了五个核心组件各干各的活GitAdapter负责从Git仓库提取变更信息包括变更文件列表、每个文件的Diff内容、提交信息、分支对比等。底层封装了Git命令和GitPython库统一对外提供结构化的变更数据。Scanner根据文件类型分发到不同的语言分析器。目前内置了Python、JavaScript、Java和通用的文本分析器每种分析器负责解析代码结构、提取函数和类定义、识别关键模式等。RuleEngine规则管理和匹配的核心。每条规则是一个独立的Python类或者JSON描述定义触发条件和处理逻辑。RuleEngine负责把Scanner提取到的代码结构信息喂给每条规则去判断。Reporter把命中结果按配置输出成不同格式支持Markdown、JSON、纯文本也可以对接GitHub评论和飞书/钉钉Webhook。CLI入口统一暴露命令行接口通过子命令区分不同的操作模式比如审查单个文件、审查某次提交、审查两个分支之间的差异、全量扫描等。这里有一个关键的设计决策规则的匹配数据从哪来。我没有让扫描器直接暴露原始的代码文本给规则而是做了一层语义化提取。比如一个Python文件扫描之后得到的不是第32行到第40行的文本而是一个名为UserService的类有2个方法方法handle_login共42行循环嵌套深度为2。规则引擎拿到的是一份结构化的代码体检表这样规则写起来非常自然不用自己去解析代码文本。2.3 为什么不用正则硬匹配这可能是踩坑最深的一块了。最开始原型阶段我图省事试图用正则表达式直接匹配代码文本做审查比如查有没有TODO注释函数名是不是驼峰。做了一版之后很快发现一个致命问题——注释和字符串里的内容会被误判。比如有人在注释里写# TODO: 这里不能用TODO正则一匹配就报了一条代码里存在TODO待处理项实际上这就是一条关于TODO的讨论而已。又比如字符串正好包含某个需要匹配的片段也会产生误报。正则方案在简单场景下确实快但规则的表达能力和误判率都不受控。所以后面我推倒重来Scanner层统一基于AST做解析。AST抽象语法树会把代码解析成结构化的树形数据注释、字符串、函数定义、变量声明在树里都是不同类型的节点规则引擎可以精确地判断这个TODO到底是在注释里还是实际代码里这个函数实际有多少行、有没有超过长度限制。AST方案前期成本高一些每种语言都需要找对应的解析器但换来的是规则的稳定性和精度。现在open-code-review里的规则误报率是可以压到很低的这在代码审查工具里非常关键——一旦误报太多团队成员就不再看它的输出整个工具就废了。3. 核心功能实现拆解3.1 规则引擎如何自定义审查规则规则引擎是open-code-review的核心我花在上面设计的时间最多。先说清楚它的工作方式然后给一个实际例子。每条规则从语义上分成三个部分元信息名称、级别、描述、触发条件在什么场景下生效、审查逻辑发现什么问题算命中。在实现上我设计了一种声明式加命令式的混合模型元信息和触发条件用JSON声明审查逻辑用Python函数实现两者通过规则名称绑定。举个例子一条检测未使用的导入规则大致长这样{ name: unused-import, severity: warning, description: 检测Python文件中未被使用的导入语句, languages: [python], trigger: file, enabled: true }对应的Python逻辑from open_code_review.rules import BaseRule class UnusedImportRule(BaseRule): name unused-import def check_file(self, file_info, results): # file_info 是 Scanner 解析后的结构化文件信息 imports file_info.get_imported_names() used set() for name in file_info.get_all_name_references(): used.add(name) for imp in imports: if imp.name not in used and imp.alias not in used: results.add_warning( lineimp.line, messagef导入 {imp.name} 未被使用 )这样写有一个好处规则的编写者完全不需要关心代码文本长什么样只需要跟Scanner约定好的数据结构打交道。文件有多少行、函数嵌套几层、定义了哪些变量Scanner已经帮你全部挖好了。更有意思的是跨文件规则。比如新增了接口定义但没有同步修改接口文档这条规则它会接收整个变更集合的数据找到所有被修改的接口定义文件然后去检查文档目录里是否有对应的变更提交。这类规则价值很高因为人工Review跨文件的一致性检查往往是最容易被忽略的。3.2 变更分析不是扫描全仓库而是精准看Diff代码审查跟一般的静态扫描最大的区别在于它关注的是变更。open-code-review在分析时做了一个比较关键的操作先通过Git拿到变更范围然后只对变更涉及的文件和函数做深度检查。具体来说GitAdapter会解析出这次提交修改了哪个文件的哪几行然后Scanner针对这些变更区域做精确解析。同时还需要做上下文关联——比如一个函数被修改了即使修改的只有几行整个函数的结构、被调用位置、相关依赖都应该纳入检查范围。这就是前面提到的事故案例的逆过程提前发现调用方没同步变更这类问题靠的就是从变更函数反查所有引用点。这个功能的实现细节值得说一下。Git的Diff输出默认是统一格式每一行前面带或者-号但只依赖这个行号信息是不完整的。我在GitAdapter里做了一层Grep逻辑会把变更区域涉及的符号名提取出来比如新增的函数名、修改的类名、删除的变量名然后用这些符号名在整个项目里做引用搜索找出所有受影响的位置。这些位置会作为关联变更点附加到分析结果里。这个设计的效果非常明显。一次真实的Review过程中扫描结果会标注出这样的信息函数getUserInfo被修改影响以下调用方src/api/user.js 第32行、src/views/Profile.vue 第57行。Reviewer拿到报告后优先检查这些关联点是否都处理了比从头到尾扫一遍Diff高效得多。3.3 多语言支持的实现路径多语言支持是open-code-review比较受关注的一块。很多Lint工具都专注于单一语言而我做这个工具的时候目标就是团队内的全栈项目所以Python、JavaScript、Java都得支持。实现上每种语言对应一个独立的Scanner模块但对外暴露的接口是一致的接收文件路径和变更范围输出FileInfo结构。FileInfo里面包含了语言无关的通用信息比如类、函数、变量、导入、注释、复杂度指标等同时每种语言的Scanner可以额外输出语言特有的信息比如Python的装饰器列表、JavaScript的异步函数标记这些可以作为高级规则的匹配数据。这里有一个平衡问题语言相关的规则必然存在比如Python的except Exception大范围异常捕获、JavaScript的宽松相等这类规则是语言特有的没法通用。我的做法是Scanner输出的FileInfo统一结构但每个字段同时带一个language标签规则触发时可以通过tag指定只对某一种语言生效。这样通用规则和专属规则可以在同一个引擎里和谐共存。新加一种语言的成本也被有效降低了。只要照着已有的Scanner实现一个解析器把语言特有信息和通用信息都提取出来那么内置的所有通用规则自动兼容新语言。如果社区后续有人贡献了Go或者TypeScript的Scanner整个平台可以用非常低的成本扩展过去。4. 实操过程从零搭建一套可用的代码审查流程4.1 安装与初始化先说安装。open-code-review目前发布在PyPI上直接用pip安装就行pip install open-code-review装完之后在项目根目录执行初始化命令会生成一个配置文件open-code-review init生成的配置文件是.ocr_config.json结构大致如下{ language: python, severity: { error: 1, warning: 30, info: 0 }, rules: { enable: [unused-import, function-too-long, svg-dangerous-tags], disable: [line-too-long], function-too-long: { max_lines: 60 } }, output: markdown, webhook: { enabled: false, url: } }这里有个细节值得展开说一下severity部分的三个数字我设计成每条规则的积分权重。open-code-review跑完之后会把命中的规则按级别加权汇总给出一个当前变更的代码风险分。这个分数的意义不是用来排名或者惩罚谁而是给团队一个一目了然的信号这个PR的风险级别是低、中还是高。如果分数超过一定阈值就可以在CI里面直接拦截合入。4.2 写出第一条自定义规则安装配置完成之后大多数人第一件想做的事情肯定是写一条我自己团队的规则。我给你完整演示一遍。假设我们团队约定新增的Python函数除非在指定的白名单文件里否则不建议超过60行同时每个函数最多接受5个参数超过就建议拆分了。这个约定可以用下面的步骤实现。首先在.ocr_config.json里注册规则文件路径{ rules: { custom_rule_dir: ./.ocr_rules } }然后在.ocr_rules目录下新建一个Python文件from open_code_review.rules import BaseRule from open_code_review.rules.data import FunctionInfo class FunctionTooLongRule(BaseRule): name function-too-long def check_function(self, func: FunctionInfo, results): if func.language python and func.end_line - func.start_line 60: results.add_warning( filefunc.file_path, linefunc.start_line, messagef函数 {func.name} 长度 {func.end_line - func.start_line} 行超过团队约定60行 ) if len(func.parameters) 5: results.add_warning( filefunc.file_path, linefunc.start_line, messagef函数 {func.name} 参数数量 {len(func.parameters)} 个超过团队约定5个 )保存之后在命令行跑open-code-review run --target-dir src/你会看到输出报告里违反这个约定的函数全被标出来了精确到行号和函数名。Reviewer拿到这个报告只需要确认这些函数是不是真的该拆省去了自己一个个数行数、数参数的时间。4.3 与GitHub Actions集成实现CI拦截命令行能跑还不够真正的价值在CI里。我以GitHub Actions为例给出一个可用的workflow配置name: code-review on: [pull_request] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependency run: | pip install open-code-review - name: Run open-code-review run: | open-code-review run --target-dir . \ --base-branch origin/main \ --severity-threshold 30 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}这个配置文件里有个关键参数是--base-branch origin/main它的作用是指定跟哪个分支做对比工具只分析从main分支分出来之后新增的变更而不是全量扫描整个仓库。--severity-threshold 30的意思是如果本次变更的代码风险分超过30CI就会以非零状态退出PR就无法自动合入。你可能想问分数是怎么累积的。我举个例子一条error级别的规则命中权重是10分一条warning级别的规则命中根据具体规则权重一般是1到3分不等。一个PR里面如果有2个error级别的严重问题风险分就会到20再加上几个warning基本就超过30的阈值了。阈值你可以根据自己对代码质量的要求调整有的团队定20也有的定50。4.4 推送结果到团队IM工具跑完之后光在CI日志里看结果还不够高效。open-code-review自带的Reporter支持把结果推送到飞书、钉钉、企业微信的Webhook也支持直接输出成GitHub PR评论。以飞书为例在配置文件里打开webhook开关填入Webhook地址{ output: markdown, webhook: { enabled: true, type: feishu, url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxx } }每次CI跑完之后审核结果会推到一个独立的代码审查通知群里里面包含变更摘要、命中规则列表、风险分、以及最关键的关联变更文件提示。同学在群里收到一条这次PR有3个warning其中有一个函数影响了8处调用方请重点确认的消息比在邮件里翻半天日志高效得多。4.5 Java/JavaScript项目的适配要点如果你团队项目不是纯Python适配也不复杂。只要在配置里面增加语言节点把对应的解析器打开就行{ language: [python, javascript, java], parsers: { python: {enabled: true}, javascript: {enabled: true}, java: {enabled: true} } }这里有一个我从Java项目里总结出来的适配经验Java的类型体系比Python复杂很多Scanner虽然能提取到类和方法的定义但是泛型、重载、注解这些信息在规则里往往拿不到精确的类型信息。所以Java相关的规则目前偏向于结构审查比如一个类的方法数量是否过多Controller里是否直接操作了数据库实体。如果你做的是强类型语言项目后续可以考虑接入更专业的语义分析引擎来增强类型推断能力。5. 常见问题与排查技巧实录5.1 误报率太高怎么办误报是代码审查工具最影响信任度的问题。我在真实使用中遇到过的误报来源大概有四类给你列成一个排查清单误报类型典型场景解决方案注释与字符串误判注释里出现关键词被当代码命中升级到AST解析或加白名单规则规则阈值不合理60行函数约定对某个目录不适用为不同目录配置不同阈值跨文件引用误报方法被框架反射调用工具认为未使用定义全局白名单符号或关闭该条规则变更范围过大大范围重构导致大量关联点被标记按目录拆分变更或设置规则应用范围这里最有效的办法是给团队一条Silence通道。open-code-review支持在代码里用注释方式忽略特定规则# ocr-ignore: function-too-long 该函数是自动生成的无法拆分 def generated_function(): ...这个机制非常实用。规则引擎在检查到该行上方存在指定的ignore注释时会跳过这条规则的命中。这样既保留了规则的全局约束力又给特殊情况留了口子。合理使用Ignore机制之后误报率可以压到5%以下团队成员对工具输出的信任度会大幅提升。5.2 扫描速度慢怎么优化代码审查工具跑太慢会拖累CI的体验。我们之前跑一个中等规模项目全量扫描一次耗时40多秒后来做了三个优化把耗时压到了8秒以内。第一个优化是只扫描变更相关文件。如果只改了一个Python文件就不需要把整个项目的所有文件都扫一遍。在GitAdapter层面比对分支之后拿到变更文件列表然后只对这个列表做Scanner分析。这个优化效果最明显常规情况下覆盖率大概能减少80%左右。第二个优化是增量缓存。open-code-review会缓存每个文件上一次的分析结果并记录文件的Hash值。如果文件在两次提交之间没有变化直接复用缓存结果。只有真正被修改的文件才重新跑Scanner。这个优化对高频迭代的项目效果非常显著。第三个优化是并发分析。Scanner层面对文件的分析是相互独立的我用concurrent.futures做了多线程分析默认线程数是CPU核心数。Python的多线程在GIL限制下对CPU密集型任务提升有限但AST解析的时候很多时间花在文件IO和对象分配上多线程还是能带来不错的收益。5.3 团队接受度低怎么推进工具做出来了规则也配置好了最难的一关其实是团队落地。这个我不能给你一个通用的标准答案但我可以分享我自己试过觉得有效的方法。第一个方法是先报告、后拦截。新规则上线第一周所有规则都设置为warning级别不拦截任何CI只推送到群里让大家看结果。这周的作用是让团队成员对工具有感知知道它的判断依据是什么。一周之后收集大家的反馈把明显不合理或者误报率高的规则调整掉再逐步把重要规则提升到error级别。第二个方法是让团队自己维护规则。与其我一人定了规则让大家遵守不如在每周约一个规则评审环节大家一起讨论哪些规则要加、哪些阈值要调。比如后端同学觉得60行的限制太长大家商量后改成80行这类调整有参与感同学的接受度会高很多。第三个方法是把工具当成脚手架而不是警察。在团队里我反复强调一个立场open-code-review的目的是把最耗体力的检查自动化让Reviewer把时间花在真正值得的地方——架构合理性、业务逻辑边界、后续扩展性。引入工具不是要让代码提交变麻烦而是减少Review的琐碎负担。5.4 一个真实的重构案例复盘最后分享一个真实的使用案例这个案例让我对这工具的价值有了更深的认同。当时我们要对一套老系统做一次接口层的重构涉及20多个文件、10多个接口。负责重构的同学非常谨慎一次提交把整个重构的Diff交上来。人工Review的话面对这样频繁的接口改动很难看出每个旧调用方有没有遗漏。open-code-review跑出来的报告里明确标注了以下接口定义被修改影响的调用方清单接口 GET /api/order/detail 签名变更影响5处调用方 - src/views/order/detail.vue 第84行 - src/api/order.js 第27行 - src/pages/admin/order-list.vue 第156行 - src/pages/account/order-history.vue 第92行 - src/utils/order-export.js 第41行Reviewer拿到这份清单之后逐一检查这5处调用方是否适配了新的接口返回结构。最后我们在这个环节发现了两处漏改的调用点直接避免了一次线上故障。放在以前这种问题靠人工一层层找真的是非常容易漏。我自己的体会是代码审查工具做得好的标志就是能让你在正确的地方花时间。open-code-review能给团队带来的价值不在于替代Reviewer而是把Reviewer从繁琐中解放出来让精力真正集中在代码的逻辑、设计和长期维护性上。如果你团队也在为代码Review效率发愁我建议你可以从小范围试点开始。装上工具写两条覆盖团队最关键问题的规则推送一周报告看看效果再逐步调整。工具永远是辅助真正的关键在于你们如何定义和迭代自己的代码审查标准。
阅读完成 · 觉得有帮助?
咨询建站