2.3 使用CheckStyle进行代码审查
和Android Lint相比,CheckStyle同样是一种静态代码检查工具的。内置的检查规则种类超过百种,主要侧重点在代码风格、编程规范等,支持强大、灵活的自定义规则检查。
2.3.1 运行CheckStyle
本小节中,我们介绍运行CheckStyle的两种方式:在Android Studio中使用插件以及作为独立的命令行工具运行。
下面我们逐个进行讲解。
1.在Android Studio中运行检查
在2.1.2小节中,我们已经成功地安装了CheckStyle插件。在Android Studio工作区的左下角可以找到CheckStyle视图,一般在这个视图中运行检查,如图2.23所示。
图2.23 CheckStyle视图
初始环境中,插件包含Google和Sun的代码检查规则,可以通过Rules(规则)下拉菜单选择某个规则,然后单击左侧的开始按钮启动检查。图2.24是检查结果的示例。
图2.24 使用Google代码规则检查结果
通过插件的方式进行检查,方法简单,但需要启动Android Studio才能运行,而且想要执行检查,还要保证工程Build结束才可以。若要抛开这些限制,并使用更丰富的选项执行检查,则不得不使用命令行模式。
下面我们就来介绍如何通过命令行启动检查。
2.在命令行启动检查
(1)下载CheckStyle可执行JAR文件
若要使用命令行模式启动检查,则首先要具备CheckStyle的可执行JAR文件。这个文件可以到CheckStyle的GitHub开源库下载,也可以到Maven仓库中下载。如图2.25所示,我们选择在GitHub下载。
图2.25 下载CheckStyle
截至目前,最新的版本号是8.28。单击checkstyle-8.28-all.jar开始下载。
(2)运行CheckStyle
作为独立运行的CheckStyle提供了很多选项,这些选项的说明可以在官网找到详细解释,这里选择几个常用的选项进行讲解。
先来看一个较为简单的用法,假设我们依旧使用Google的代码规则,并已经取得了相应的XML规则描述文档,其名称为google_checks.xml。该文档和checkstyle-8.28-all.jar位于My Application工程的checkstyle目录中,如图2.26所示。
图2.26 CheckStyle目录结构
接下来,打开macOS的终端(在Windows操作系统中为命令提示符),定位到checkstyle目录下,执行下面的命令:
得到如图2.27所示的输出。
图2.27 CheckStyle命令行模式检查结果
显而易见,总共有6个警告级别的问题,和在Android Studio中的结果一致。
(3)CheckStyle命令详解
下面来拆解前面所执行的命令。
如图2.28所示,整条命令可分解成三部分。第一部分是运行.jar文件的通用方法,相信有Java编程基础的读者并不陌生;第二部分是CheckStyle工具的参数,-c即configurationFile,表示使用哪种代码规范检查(特别注意的是,该参数为唯一的必选参数。当未指定配置文件时运行,将会收到报错提示);最后一部分是要检查的代码路径,“../”表示向上一层,此处的含义是回到工程根目录,在实际使用中,这部分可以是一个目录,也可以是特定的某个文件。
图2.28 CheckStyle命令拆解图示
如果用户有其他的配置文件,那么不妨尝试使用此种方式运行检查。无须启动Android Studio,更无须完成Build,只需要执行上述CheckStyle命令即可。
(4)CheckStyle常用命令
看到这里,你可能会有这些疑问:如何将检查的结果保留下来呢?如果想排除某个目录/文件,该怎么做呢?命令行不如UI界面直观,如果忘记了用法,该怎样获得帮助呢?
带着这3个疑问,我们来了解CheckStyle常用的3个参数。
首先解答第一个问题:如何保留检查结果。
CheckStyle使用-o参数来指定文件的路径,这个文件即结果的输出文件。它的使用方法如下:
执行该命令后,控制台没有任何输出。在任务完成后,我们可以在同级目录下找到result.txt文件。使用任意文本编辑器打开它,其内容如图2.29所示。
图2.29 CheckStyle检查结果
此外,我们还可以使用-f参数来指定结果输出的格式。CheckStyle提供了两种格式:一种是上面默认的纯文本格式;另一种是XML格式,必要时可以使用后者。
接下来回答第二个问题:如何排除某个文件或目录。添加例外的情况通常用于多Module的场景中,观察如图2.30所示的工程结构。
图2.30 多于一个Module的工程结构
图2.30中共有两个Module,即app和submodule,且各自都有Java源码,现在要做的是排除所有无须做检查的源码。这里所说的无须做检查,包含除MainActivity.java、SubMainActivity.java以及XML以外的所有文件。
当然,针对本例,可以指定仅检查这两个文件而非使用排除法,且这样做更加方便。此处使用排除法仅为说明如何正确排除文件。通常在实际开发中,使用排除法更为常见。
要找到所有无关的文件,先要清楚整个工程的文件组织结构。
如图2.31所示,需要排除的文件是除了用方框框起来的两个Java源代码文件以及若干XML源代码文件外的所有文件。这看上去似乎很烦琐,但好消息是:由于我们要使用的Google检查规则只对Java、Properties和XML源代码文件有效,因此只需要排除两个Module中的test相关类即可。
图2.31 多于一个Module的目录结构
CheckStyle使用-e或-x参数添加例外,前者要求给定一个或多个具体清晰的路径,后者要求给定一个或多个描述路径的正则表达式,当有文件或目录匹配到给定的正则表达式时,相应的文件或目录将被跳过。
思路比较简单,但是写起来比较麻烦,就是使用多个-e参数将两个Module中的androidTest和test目录排除在外,即:
如果你对正则表达式比较熟悉,还可以使用-x参数,配合正则表达式让计算机程序帮助找到符合条件的项目,并将其排除在外。
本例中,只要排除名为androidTest和test的目录即可,因此,可以使用下面的命令:
此外,-e和-x参数还可以同时使用。如果只想检查submodule模块的有用代码,可以执行下面的命令:
最后,如果忘记了命令行的使用方法,该怎样快速获得帮助呢?
这里给大家提供两种方法,分别说明如下:
第一种方法是使用--help,即java -jar checkstyle-8.29-all.jar –help,如图2.32所示。
图2.32 CheckStyle本地帮助文档
第二种方法是到CheckStyle官网查询相应的文档获得帮助,如图2.33所示。
图2.33 CheckStyle在线帮助文档
这两种方法相比,第一种更快速,第二种有使用示例,更详细。读者日后可根据实际需要进行选择。
2.3.2 自定义CheckStyle检查规则
接下来,我们来介绍如何自定义CheckStyle配置文件,从而打造属于自己或团队的代码检查规范。
1.Module(模块)
我们都知道,CheckStyle的配置文件是XML格式的文档。通过阅读Sun和Google的代码检查规范可以发现:整个XML文档呈树形结构,其根是一个名为Checker的Module,该Module又包含若干子Module。这些子Module包含4种分类:
(1)FileSetChecks:定义了具体的检查规则,用于检查相应的源代码文件。
(2)Filters:用于过滤检查规则。
(3)File Filters:用于过滤要检查的文件。
(4)AuditListeners:报告已接收的事件。
细心的读者会发现,大部分检查规则都被一个叫作TreeWalker的Module包括。根据上文所述的分类,这个名为TreeWalker的Module属于FileSetCheck Module。它的原理就是将每个待检查的Java源代码文件转换为抽象语法树(与具体语法树/分析树相对,抽象语法树可简称为语法树(Abstract Syntax Tree, AST)。它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。更多详情读者可参阅编译原理中的相关知识点),然后由其中的子Module按照各自定义的规则进行逐项检查,直到全部完成。
2.Properties(属性)
Checker定义了一些通用的属性,这些属性被其他的Module继承和使用。这些属性的定义如下:
· basedir:基本目录名,在有关文件的描述信息中将被去掉,默认值为空。
· cacheFile:缓存所有检查完毕的文件,通常用于避免重复检查,默认值为空。
· localeContry:描述信息所使用的区域编码,其默认值取决于Java虚拟机。
· localeLanguage:描述信息所使用的语言编码,其默认值取决于Java虚拟机。
· charset:字符集名称,默认值取决于名为file.encoding的系统属性值。
· fileExtensions:接受检查的文件扩展名,默认检查所有文件。
· sverity:定义所有违反代码编写规则的严重级别,默认为error。
· haltOnException:在检查过程中,如果发生异常,是否停止检查。默认为true。
· tabWidth:定义了一个制表符包含的空格数量,其默认值为8。
现在,打开Google和Sun的代码规则配置文件,找到相关属性,看看它们是如何定义的。
3.Metadata(元数据)
CheckStyle允许使用Metadata来保存特定的信息,这些信息可以用来保存插件特定信息或其他的有用信息,这些信息在执行代码检查时会被忽略。需要注意的是,为了避免和其他工具或插件发生命名冲突,建议将所有Metadata名称加上特定的前缀。
参考下面的例子:
4.TreeWalker规则定义
如前文所述,TreeWalker及其子Module定义了详细明确的代码检查规则。如果想要对某一个规则项进行检查,那么只需要添加相应的子Module即可。而且子Module的规则是可以被定制的。
举个例子,对于工程My Application,以Google的代码检查规范运行检查,其结果总共有6个warning级别的问题输出。现在,我们把目光聚集在CustomImportOrder规则,由该规则检查出的问题总共有两个,如图2.34所示。
图2.34 违反CustomImportOrder规则的代码位置
打开google_checks.xml,搜索CustomImportOrder,找到如下代码片段:
尝试注释掉CustomImportOrder部分代码,然后再次运行检查,发现问题数量变为4,不再有违反CustomImportOrder规则的报告。
这很好理解,对于某个规则,如果要添加相应的检查,就需要声明它,声明的方式只需添加相应Module即可;未被添加的Module将默认不会被检查。那么,在哪里可以找到所有的检查类别呢?我们在CheckStyle官网中找到了答案。
如图2.35所示,CheckStyle网站上罗列了所有的检查规则,每项规则还有详尽的描述。读者在使用时可根据实际需要,按关键字进行模糊查找,再通过描述来确定是否使用某项规则。
图2.35 CheckStyle规则文档
此外,在CheckStyle的GitHub开源库中,checkstyle_check.xml文件中包含所有的检查规则,我们也可以将其作为参考。
现在,让我们继续针对上文中的CustomImportOrder规则做实验。很明显,在规则配置文档的CustomImportOrderModule中还有若干Property。我们尝试去掉所有的Property,然后再次运行检查,发现之前的6个问题变成了5个,减少了一个违反CustomImportOrder规则的项目。这是为什么呢?
实际上,对于每个Module,当我们添加它时,实际上也同时添加了它的默认属性。
就拿CustomImportOrder来说,它的默认规则如图2.36所示。
图2.36 CustomImportOrder规则属性及默认值
由于这些属性有类似Java中的Override特性,因此通过对比google_checks.xml中的自定义项,我们发现名为sortImportsInGroupAlphabetically属性的值是true,而非默认的false。正是这个原因,让我们从一开始就检查出了6个问题项。
好了,总结一下,对于TreeWalker规则定义:
(1)要增加某种规则检查,需要声明对应的Module,声明的方式即添加子Module项。
(2)利用Property可覆盖默认值的特性,我们可以自定义属于自己或团队的统一编码规则检查配置文件。
(3)所有的检查规则可以在CheckStyle网站或GitHub开源库的checkstyle_check.xml中找到详细描述。
5.Severity(严重性)
我们已经使用Google的代码规则对My Application项目做过一次代码检查了,其结果共有6个warning项。在CheckStyle中,还可以对某类检查结果自定义严重性级别。
打开google_checks.xml文档,在22行左右发现了如下代码:
CheckStyle使用severity属性定义和它相关Module的严重级别,在该Module中的所有子Module若未单独声明该属性,则默认继承父Module的严重级别定义。默认情况下,严重级别为error。
在google_checks.xml中,其对应的Module为Checker,因此所有子Module报告的问题严重级别皆为warning。若去掉严重性定义,则原来的6个warning项将变为error,命令行也会有汇总提示,如图2.37所示。
图2.37 默认严重级别下的结果输出
6.ID(唯一标识符)
打开google_checks.xml,搜索SeparatorWrap。我们惊讶地发现,居然有4个相同的Module,唯一不同的是其中的Property。这是怎么回事呢?
出现同种Module其实在CheckStyle中是允许的。这种情况适用于不同的情境下进行检查。而决定要用哪一个Module,是由其中名为id的Property决定的。因此,Module可以同名,但id必须唯一。这样做,一方面保证了代码检查的灵活性,另一方面解决了多个相同Module之间的冲突。
在google_checks.xml文档中,总共有5个SeparatorWrap Module,其id均不同。在不同的环境中,将启用各自id的Module。