静态代码分析程序的流程设计

432 阅读11分钟

大家好,我是王有志。一个分享硬核 Java 技术的金融摸鱼侠。

上一篇文章中,我向大家汇报了 CA 的诞生,定位,能力与使用,今天向各位汇报下 CA 的设计与实现。

Tips:在设计 CA 的整体流程时,我们预设了一个前提,待分析的项目是通过 Maven 构建的 Java 项目,并使用 Git 进行版本控制。

CA 的流程设计

我们先来回顾下上一篇文章中提到的 CA 的两个核心功能:

  • 功能 1: 根据分支间的差异,解析到本次修改的方法,并分析出方法的调用链路;

  • 功能 2: 通过指定类型和方法,分析该类型中的指定方法,在程序中的调用链路。

在功能 1 中,CA 需要做 3 件事情:

  1. 对比分支间差异,解析到发生变更的方法;

  2. 通过发生变更的方法,生成方法调用链路;

  3. 使用方法调用链路,输出不同形式的报告。

在功能 2 中,CA 只需要做两件事:

  1. 通过用户指定的方法,生成方法调用链路;

  2. 使用方法调用链路,输出不同形式的报告。

可以看到,功能 1 和功能 2 都要分析方法的调用链路,输出不同形式的报告,只不过对于功能 1 来说,这个方法是分支对比后的结果,对于功能 2 来说,这个方法是用户指定的,那么功能 2 就相当于功能 1 的子集,我们只需要完成功能 1 的设计,并将公共的部分提取出来,复用到功能 2 中就可以了。

对于这种情况,我们首先想到的是使用责任链模式来设计 CA 的主流程,责任链中每个节点负责独立的逻辑处理,CA 只需要根据功能来组装责任链的节点就可以实现不同的需求了,那么我们的初版的设计如下图所示:

CA流程的初版设计.png

当然了,实际的情况肯定会复杂一些,因为我们还有一个宏大的设想“CA 应该要能够支持多种版本控制软件,多种编程语言,多种项目构建方式”。

CA 的节点设计

确定完主流程后,我们开始拆分这个流程中应该具备哪些节点。首先是差异对比分析这部分功能,输入是两个不同的分支名称(针对于功能 1),输出的是发生变更的方法。那么在这个过程中需要经历哪些操作呢?

项目下载节点

最容易想到是对比分支间的差异,这部分功能该如何实现呢?

我们可以利用版本控制软件自身的差异对比功能(如 Git Diff)来实现,我们可以通过远程调用代码托管平台(如 GitLab)的接口来实现,也可以将项目下载到本地,使用本地命令来完成,这两种方式该如何选择?

我们的选择是将项目下载到本地,使用本地命令来完成差异对比的功能,为此引入了项目下载节点。为什么要这么做?

这是因为,不同的代码托管平台提供的接口请求报文和响应报文会有较大的差异,兼容起来成本会非常大;其次,在后续确定具体变更方法和分析方法调用链路的过程中,我们也需要使用到项目的代码;最后,不仅仅是进行差异对比分析的功能 1 中需要下载项目到本地,功能 2 中虽然不进行差异对比分析,但仍需要将项目下载到本地,以便于后续分析方法调用链路时使用

因此,我们必须要将项目下载的功能独立拆分成单独的处理节点。在引入了项目下载节点后, CA 的流程变成了如下图所示:

引入项目下载节点.png

你可能会有疑问,如果 CA 已经下载过项目到本地了还需要这个节点吗?

答案是依旧需要,在实际的实现中,CA 会检测本地是否存在该项目,如果本地不存在改项目,会下载项目到本地;如果本地存在该项目,会对项目的所有分支执行一次更新操作,毕竟在两次分析之间,项目可能已经发生了多次变更。

差异对比节点

接下来我们要做的是进行差异对比,我们需要思考下,差异对比仅仅只需要调用版本控制的命令吗?

显然是不行的,因为版本控制软件的差异对比本质是文本的差异对比,以 GIt 为例,本地调用git diff命令后,输出的结果如下:

image.png

其次,不同的版本控制软件的差异对比结果的展示形式不同,如果不做处理就将结果提供给分析使用,后续的处理节点还需要解析不同版本控制软件的差异对比结果,这无疑是将复杂度扩散到不属于它的地方了,CA 需要将不同版本控制软件的差异对比结果转换为 CA 内部的差异对比结果,后续处理时使用 CA 内部的差异对比结果,将兼容不同版本控制软件差异对比结果的复杂度收敛到差异对比节点中

我们再来思考一件事,我们在修改时会涉及到 3 种基本操作,新增,修改和删除,那么对于每种类型的操作都可以在同一个代码分支上进行分析吗?

答案是不行的,假设我们有 master 分支和 dev 分支,并且在某次修改中删除了 dev 分支中的某个方法,那么它就在 dev 分支中“消失”了,而这个方法又确确实实发生了变更,我们需要对其在程序中影响的链路进行分析,那么我们只能在 master 分支中分析删除这个方法影响了哪些链路了,同理在新增的场景中,master 分支无法满足我们的诉求,只能在 dev 分支中进行处理。所以,CA 还需要根据发生变更的操作类型,标记这些变更需要在哪个分支中进行方法调用链路的分析

最后,CA 在差异对比节点中还做了一件事,将差异对比结果按照文件进行合并,这是因为 Git 的差异对比结果并不是以文件为维度的,而是以变更为维度的,按照文件进行合并后,在确定变更对应的我们只需要读取一次文件即可

我们来总结下,CA 的差异对比节点中,总共做了 3 件事:

  1. 调用版本控制软件的差异对比命令,进行差异对比

  2. 将版本控制软件的差异对比结果转换为 CA 内部的差异结果,并按照变更到操作类型,将变更分组到不同的分支中

  3. 将差异对比结果按照文件进行合并

在引入了差异对比节点后,CA 的流程变成了如下图所示:

引入差异对比节点.png

CA 内部的差异结果本质上还是文本的差异,标记了发生变更的文件,文件的绝对路径和发生变更的行号等。

Tips:额外补充一点,Git Diff 默认使用 Myers 差分算法,它是一种简单高效的文本差分算法,关键思想是,两个文本之间一定可以通过不断地执行插入和删除操作使其相等,这点你可以仔细的体会一下~~

变更检测节点

前面我们已经完成了项目下载和差异对比,将差异对比结果转换为了 CA 内部的差异对比结果,并且按照变更的操作类型将差异对比结果分组到不同的分支中,这两步处理我们将其定义为预处理阶段。

接下来 CA 需要在不同的项目分支中处理这些差异,为此我们需要循环遍历所有分支,并在循环中进行分支切换,解析差异结果,生成调用链路和输出报告等

但是可能会存在一种场景,如果这次没有变更类型为删除的差异结果,那么 CA 只需要在项目的 dev 分支上执行分析就可以了,也就不需要切换到 master 分支了,并且后续的流程都不需要再执行了。

针对于这种情况,我们引入了变更检测节点,检测当前分支中是否有需要分析的变更,如果没有则中断流程,进行下一个分支的处理。在引入了变更检测节点后,CA 的流程变成了如下图所示:

引入变更检测节点.png

分支切换节点

如果完成了变更检测后,该分支下有需要分析的差异结果,CA 就需要进行分支的切换,为此我们引入了分支切换节点。在引入了分支切换节点后,CA 的流程变成了如下图所示:

引入分支切换节点.png

为什么功能 2 中也需要切换分支?

如果项目执行过功能 1,那么它处于哪个分支上对我们来说是不可知的,而对于指定方法进行分析的功能 2 来说,我们需要将项目切换到指定的分支上进行分析。

Tips:实际上变更检测和分支切换可以合并到一个节点中,这点我们也正在考虑~~

差异分析节点

前面我们在差异对比节点中拿到依旧是文本的差异,这里我们需要根据项目使用的编程语言,并根据这些编程语言的规则解析这些文本差异,将其转换为发生变更的方法。

为此我们引入了差异分析节点,该节点中负责将 CA 内部的差异结果转换为编程语言中发生变更的方法。在引入了差异分析节点后,CA 的流程变成了如下图所示:

引入差异分析节点.png

至此,我们在文章最开始提到的差异对比分析的功能就已经全部拆分为责任链中相应的节点了。接下来,我们来看解析方法调用链路的功能和输出报告的功能是否还需要拆分节点。

项目编译节点

前面的流程中,我们已经完成了项目下载,对比差异,解析差异,并拿到了发生变更的方法,下面我们就需要根据这些发生变更的方法分析该方法的上下游调用链路。

但是在分析之前,我们需要先对项目进行编译,因为在分析 Java 项目时,我们使用的是 ASM 进行方法调用的分析,它是依赖字节码文件的(实际上 BCEL 也依赖字节码文件)。因此在正式分析前我们需要先编译项目。

在引入了项目编译节点后,CA 的流程变成了如下图所示:

引入项目编译节点.png

链路分析节点和报告输出节点

至此对于 Java 项目来说,我们已经完成了所有准备工作了,可以开始进行变更方法的调用链路分析了和输出分析报告了。

由于这两个功能相互独立,所以它们可以设置为独立的节点。至此 CA 的完成流程就已经出来了,如下图所示:

完整流程.png

以上就是我们对静态代码分析程序整体流程设计的过程,整体是基于需求出发,逐步分析出的结果,只不过有些节点是我们在实际开发过程中遇到困难时添加的,例如,项目编译节点,在最初的设计中,我们并没有考虑到需要编译项目后才能分析出方法的调用关系。

好了今天的内容就到这里了,下一篇文章中,我会和大家分享 CA 的实现。我是分享硬核 Java 技术的金融摸鱼侠王有志,我们下回见!


尾图(无二维码).png