代码评审是软件质量保证一种活动,由一个或者多个人对一个程序的部分或者全部源代码进阅读理解。一般来说分为作者和评审者两种角色,作者方提供代码逻辑的介绍和代码,评审者则对提供的代码基于设计,功能性和非功能性等方面认知进行阅读并提出问题。常见的评审组织形式是有同行评审(Peer Review)和小组检查 (Team Inspection)两种方式。
在代码评审中,评审的目的在通过代码的评审发现潜在的问题,同时分享和表达是代码评审的重要收获,我们知道人相同在不同的文化下生产力是不同的,代码评审是一个工具,工具受文化的影响的同时也影响着文化,最终朝着我们希望的责任共担、持续改进的方向发展。
一、代码评审演进
随着互联网的发展,开发人员也越来越重视代码评审带来的代码的代码质量提高以及代码评审间接带来的分享及人员备份效果,已经不满足于只是简单的发现当前问题解决问题记录问题,需要满足从评审基本跟进、评论管理、评审报告以及评审方式多样化、评审与研发流程相结合等需求。
① 代码评审检查表:手工定义要检查项,检查完进行打卡标记结果。
② 插件快速评审导入导出:快速在插件上进行评论,并将评论结果导出给被评审人,被评审人导入评审结果查看,评审表不可复用,一旦代码变更则无法准确定位、也无法再次跟踪评审修改结果。
③ 在线代码评审:在线插件或网页评审,提供提交前提交后评审,可多人评审策略管控、代码评审与需求/缺陷关联管理。
④ 自动化代码评审:结合现有的Sonar扫描、安全扫描进行对提交的代码进行自动化检查,使代码在人工评审之前已经经历一轮自动评审,代码评审通过之后可自动触发构建、部署等。
⑤ 智能化代码评审:根据AI大模型,可对提交代码进行综合评价(编码标准、可用性、可读性、可维护性、安全性、高性能、异常控制、设计原则、可扩展性、代码复杂度等等)并给出相关测试建议等,未来大模型对代码评审还有更大的空间。
二、代码评审解决的需求和痛点是什么?
vivo当前已经有EasyCR评审工具,那为什么我们还需要继续开发调研代码评审工具呢?
我们先看看下面通过内部调研获取的信息,看看用户希望的代码评审工具需求和痛点是什么?
针对当前vivo代码评审工具我们继续升级补充场景:
- 增加评审方式:对原自由评审方式(主要是提交后进行代码评审)增加评审控制方式(提交代码至仓库前进行代码评审、合并时提交代码评审)。
- 支持网页/插件:增加网页端评审功能,满足不同角色进行评审及用户体验上的优化,增强插件版评审功能。
- 支持研发流程控制:上线过程中可作为人工卡点一项检查项(可通过代码是否评审、代码评分、代码问题解决情况等进行判断),通过线上管理,提高上线质量。
- 支持自动化检查:代码提交前,提交后可进行代码自动化检查,对代码进行自动评审。
- 增加用户定制化需求:如评审权限、评审通知方式、评审策略多人评审管理、评审报告订阅等。
当前市场上有很多优秀的代码评审工具,但是很少有评审工具能满足所有的场景,角色不同,需要的能力不同,同一个角色不同团队使用的方式不同,我们需要一款解决用户痛痒爽的代码评审工具。
三、vivo代码评审系统架构
四、vivo代码评审工具使用流程
在代码评审中,CR可以是一次Commit,也可以是一次MergeCommit,那么针对一次CR我们可以随时对已经提交的commit进行评审,也可以在CRpush至代码库之前拦截,同时也可以在一次合并之前进行代码评审。
代码评审模式:
1. 提交前评审(Pre-push Code Review)
2. 提交后评审(Post-push Code Review)
① 合并评审
② 自由评审
提交前评审:VCR基于VCR在提交push至Gitlab代码仓库之前,对代码进行拦截,并进行评审,支持一次评审请求作为一次评审,可对一次一次评审请求查看所有变更记录并进行评审追踪。利用开源工具Gerrit,将评审请求推送至Gerrit中,评审通过后,将代码从Gerrit同步至Gitlab仓库
提交后评审:
①合并评审:VCR基于Gitlab 在一次MR的基础上进行代码评审。
②自由评审:针对用户当前代码库当前分支信息或历史commit进行评审。
五、vivo代码评审工具实施
5.1 确认技术架构
提交仓库前进行代码评审,我们使用当前成熟的代码评审Gerrit,实施过程中最大的问题是用户如何低成本切换及简单评审的问题,对于当前Gerrit评审工具遇到的问题如何解决呢?
1) 我们知道Gerrit评审工具需要提供给用户Gerrit代码库地址,并进行下载使用,当前用户使用的代码库习惯不能更改,也是不愿意修改的,那么我们如何解决呢?
给插件加持,提供用户黑盒切换至评审代码库,或执行一键下载代码库功能,底层使用Gerrit与代码托管库同步机制解决代码一致性问题,用户在使用代码库时同原使用方式一致。
2) Git代码提交,CR为最小单位,CR可作为一次评审,但还有很多用户使用的习惯是一次push作为一次评审,如何解决用户一次push为一次评审呢?
a)需要对代码关系链需要进行整理,识别出一次push作为一次评审记录,用户多次追加提交记录至评审请求,需要重新识别出关系链作为原push请求的评审记录,Git原生对代码变更的情况比较多,我们对一些场景进行分析再特殊处理,不穷举。
b)可对最小粒度CR的评审,也同时提供一次push请求内容进行评审,更方便快捷。
用户不管是提交前评审、合并时评审,都可能会产生一次push,多次commit,用户需要对最小粒度CR评审,也需要对最新变更所有内容进行评审。
5.2 插件改造实施
根据我们对用户的调研过程中,用户对代码评审插件网页同时兼容的要求比较高,针对idea插件我们如何改造代码评审,这里我们着重对Gerrit插件改造展开说明。
步骤1:了解插件框架、配置、打包、运行
1)插件框架整体介绍
(图片来源于网络)
- 开发方式:在官网的描述中,创建IDEA插件工程的方式有两种分别是使用DevKit(IntelliJ Platform Plugin 模版创建)和Gradle构建方式,这两种方式在构建项目和打包发布上有所区别,同时官方提供了将Devkit迁移至Gradle的方式。
参考:https://plugins.jetbrains.com/docs/intellij/developing-plugins.html - 框架入口:一个 IDEA 插件开发完,要考虑把它嵌入到哪,比如是从 IDEA 窗体的 Edit、Tools 等进入配置还是把窗体嵌入到左、右工具条还是IDEA窗体下的对话框。
- UI:思考的是窗体需要用到什么语言开发,没错,用的就是 Swing、Awt 的技术能力。
- API:在 IDEA 插件开发中,一般都是围绕工程进行的,那么基本要从通过 IDEA 插件 JDK 开发能力中获取到工程信息、类信息、文件信息等。
- 外部功能:这一个是用于把插件能力与外部系统结合,比如你是需要把拿到的接口上传到服务器,还是从远程下载文件等等。
2)Gradle创建
新版通过 New-> Project->IDE Plugin进行创建,旧版通过New Project->Gradle->IntelliJ Platform Plugin进行创建。
项目结构如下:
3)配置介绍
plugin.xml
<!DOCTYPE idea-plugin PUBLIC "Plugin/DTD" "http://xxxx">
<idea-plugin>
<!-- 插件唯一id,不能和其他插件项目重复,所以推荐使用包名+插件名com.xxx.xxx的格式
插件不同版本之间不能更改,若没有指定,则与name相同 -->
<id> com.your.company.unique.plugin.id </id>
<!-- 插件名称,别人在官方插件库搜索你的插件时使用的名称 -->
<name> Plugin display name here </name>
<!-- 插件版本,格式:BRANCH.BUILD.FIX (MAJOR.MINOR.FIX) -->vs
<version>1.0.0</version>
<!-- 供应商主页和email(不能使用默认值,必须修改成自己的)-->
<vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor>
<!-- 插件的描述 (不能使用默认值,必须修改成自己的。并且需要大于40个字符)-->
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<!-- 插件版本变更信息,使用<![CDATA[ ]]> 来支持HTML格式;
将展示在 settings | Plugins 对话框和插件仓库的Web页面 -->
<change-notes><![CDATA[
<p>
<li>1.0.0</li>
<ul>
<li>
1.新增xxx功能 <br/>
2.优化xxx功能 <br/>
</li>
</ul>
</p>
]]>
</change-notes>
<!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description -->
<!-- 插件兼容构建的IDE版本, until-build可以不写,默认到最新版 -->
<idea-version since-build="203.4818.26" until-build="211"/>
<!-- please see http://confluence.jetbrains.net/display/IDEADEV/Plugin+Compatibility+with+IntelliJ+Platform+Products
on how to target different products -->
<!-- 插件依赖,可以依赖模块或插件 -->
<depends>com.intellij.modules.lang</depends>
<depends>Git4Idea</depends>
<depends optional="true" config-file="plugin-maven.xml">org.jetbrains.idea.maven</depends>
<!—idea第一次打开, 实际上就是订阅了应用程序打开的事件-->
<application-components>
<component>
<implementation-class>xxxxx</implementation-class>
</component>
</application-components>
<!—打开项目 -->
<project-components>
<component>
<implementation-class>
xxxxx
</implementation-class>
</component>
</project-components>
<!-- 插件定义的扩展点,以供其他插件扩展该插件,类似Java的抽象类的功能
如何在https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html -->
<extensionPoints>
</extensionPoints>
<!-- 声明该插件对IDEA core或其他插件的扩展,Ns是NameSpace的缩写 -->
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="代码评审" icon="/icons/xx_13x13.png" anchor="bottom" factoryClass="xxx" />
</extensions>
<!-- 编写插件动作 https://plugins.jetbrains.com/docs/intellij/plugin-actions.html-->
<actions>
<action id="com.xx.xx.AddCommentAction"
class="com.xx.xx.actions.AddCommentAction"
text="添加评论"
description="为选中的代码添加评论意见"
icon="AllIcons.Actions.StartDebugger">
<!—编辑器右键弹出菜单--!>
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
<!--快捷方式--!>
<keyboard-shortcut first-keystroke="alt X" keymap="$default"/> </action>
</action>
</actions>
</idea-plugin>
4)插件运行调试打包安装
Gradle构建方式进行调试打包安装
运行/调试:runIde 可以选择Debug模式或者是Run模式
打包
安装:可以将打的包发布市场(本地idea配置插件仓库),从Marketplace搜索插件或者是直接从Settings->plugins->Install->Install Plugin from Disk安装
步骤2:研究Gerrit插件源码,搞清楚整理开发流程和模块
步骤3:基于Gerrit插件规划VCR插件模块,增加clone、branch、mergeRequest、VCR模块,并对各组件增强
步骤4:定制原有流程模块push,自动化关联工作项
在使用Git依赖插件之前,先了解一下插件的扩展以及扩展点(Extensions、Extension Points)。
Intellij 平台提供了允许一个插件与其他插件或者 IDE 交互的 extensions 以及 extension points 的概念。
- Extension Points:如果你想要你的插件可以被其他插件使用,那么你必须在你的插件内声明一个或多个扩展点(extension points)。每个扩展点定义了允许访问这个点的类或者接口。
- Extensions:如果你想要你的插件扩展其他插件或者 Intellij 平台,你必须声明一个或多个 extensions。
可以在 plugin.xml 中的和块中定义 extensions 以及 extension points。
plugin.xml
<!--依赖插件包--!>
<depends>Git4Idea</depends>
<!—idea第一次打开, 实际上就是订阅了应用程序打开的事件-->
<application-components>
<component>
<implementation-class>com.demo.intellij.plugin.vcr.push.VcrPushExtension$Proxy</implementation-class>
</component>
</application-components>
上述我们看到依赖的Git4Idea 包,如果我们想修改原生的的Git,先看下push依赖包中如何实现的。
Git4Idea(plugin.xml)
<extensions defaultExtensionNs="com.intellij">
<pushSupport implementation="git4idea.push.GitPushSupport"/>
...
</extensions>
intellij-dvcs.jar(plugin.xml)
<extensionPoints>
<extensionPoint name="pushSupport"
interface="com.intellij.dvcs.push.PushSupport"
area="IDEA_PROJECT"
dynamic="true"/>
....
</extensionPoints>
从上述可看到,Git4Idea 的GitPushSupport扩展实现push的功能点,接下来我们主要对GitPushSupport进行javassist字节码修改以达到扩展git push组件能力。
扩展使用GitPushSupport之前,需要将需要的类进行装载至GitPlugin中,然后再对GitPushSupport进行字节码改造,至此对git Push原生插件页进行改造。
步骤5:使用树状列表模式,展示一次push请求VCR提交内容及多个CR情况
主要是实现JTreeTable,对VCR与CR进行管理。
一次评审请求VCR包含所有CR的提交变更记录,可针对该变更记录进行代码评审,单个CR也可以进行评审。
步骤6:展示变更文件视图及定制评论展示模块,精准定位代码
代码评审主要根据编辑器获取代码行及位置,评论可精准定位到代码行。
1)changeBrowser变更视图展示VCR变更文件信息
2)双击文件,diff视图展示inline和side-by-side两种代码差异
声明扩展,针对扩展类进行定制化改造。
plugin.xml
<diff.DiffTool implementatinotallow="com.demo.intellij.plugin.vcr.ui.diff.VcrCommentsDiffTool$Proxy"/>
3)添加代码块评论,定位代码块
AddCommentAction.java
public class AddCommentAction extends AnAction implements DumbAware {
public AddCommentAction(String label,
Icon icon,
CommentsDiffTool commentsDiffTool,
Editor editor,
List<CommentInfo> fileComments
....
) {
super(label, null, icon);
}
private CommentInput createComment() {
//获取用户选择代码位置位置
//行的情况下,默认是开头和行结束 得到光标的位置caretModel.getOffset();
/*取到插字光标模式对象 CaretModel caretModel = editor.getCaretModel();
得到光标的位置int caretOffset = caretModel.getOffset();
//得到一行开始和结束的地方
int lineNum = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
获取一行内容String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
*/
Document document = editor.getDocument();
int lineNum = document.getLineNumber(editor.getCaretModel().getOffset()) ;
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
.....
}
}
所有评论展示列表如何精准定位代码
SafeHtmlHistoryComments.java
public class SafeHtmlHistoryComments extends JPanel {
private Iterable<CommentInfo> fileComments;
private List<CommentInfo> commentInfos = new ArrayList<>();
private CommentInfo currentCommentInfo;
private SelectedComment selectedComment;
private SelectedComment operatorSelectedComment;
private Editor editor;
public SafeHtmlHistoryComments(Editor editor,Iterable<CommentInfo> fileComments, Comment selectedComment) {
super(new BorderLayout());
....
HistoryCommentListPanel historyCommentListPanel = new HistoryCommentListPanel(fileComments);
//双击table某行触发代码定位
historyCommentListPanel.addTableMouseDoubleHit(new Consumer<CommentInfo>() {
@Override
public void consume(CommentInfo commentInfo) {
codeTextHit(editor,commentInfo);
}
});
}
/**
* 定位代码
* @param editor
* @param commentInfo
*/
private static void codeTextHit(Editor editor, CommentInfo commentInfo) {
SelectionModel selectionModel = editor.getSelectionModel();
// 优化:如果文件修改过了,则不进行选中操作,换为提示
if (null != commentInfo.startIndex && null != commentInfo.endIndex && commentInfo.startIndex != 0 && commentInfo.endIndex != 0) {
editor.getCaretModel().moveToOffset(commentInfo.endIndex);
selectionModel.setSelection(commentInfo.startIndex, commentInfo.endIndex);
} else if (null != commentInfo.line && commentInfo.line != 0) {
int lineNum = commentInfo.line - 1;
editor.getCaretModel().moveToOffset(lineNum);
CharSequence charsSequence = editor.getMarkupModel().getDocument().getCharsSequence();
if(null!=commentInfo.range) {
RangeUtils.Offset offset = RangeUtils.rangeToTextOffset(charsSequence, commentInfo.range);
selectionModel.setSelection(offset.start, offset.end);
}else{
Document document = editor.getDocument();
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
selectionModel.setSelection(lineStartOffset, lineEndOffset);
}
}
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
....
}
六、未来展望
6.1 自动化代码评审
- 代码提交评审或代码合并之前,先自动化检查(Sonar/安全扫描)快速发现并纠正潜在问题,检查成功后提交评审。
- 代码评审通过之后,结合流水线,自定义部署构建策略,实现快速迭代。
- 自动汇聚测试报告,根据评审问题类型进行分类,不断改进Sonar检查规则,从而形成良性循环。
6.2智能化代码评审
- 提交代码评审之后,通过AI大模型对代码进行综合评价,并给出建议。
- 通过智能代码评审,产生评审报告,并进行智能化分析。