一、Code Review 简介
1.1 Code Review 的作用及目前存在的问题
在开发流程中,其中有一个环节是 Code Review。通过 Code Review,不但可以提高代码质量,提前发现 bug,还可以统一团队的代码规范。此外还有益于形成团队的技术氛围,加强团队成员的沟通,以老带新,互助成长等等。
虽然 Code Review 很重要,但是在互联网公司追求快速迭代,业务需求频繁变更以及倒排期的情况下,很多公司的技术团队都是在加班加点的情况下才能跟得上发版的节奏,根本没有时间进行 Code Review。或者即使有,也是流于形式。这就导致有些本可以在 Code Review 环节发现的问题,在提测后被测试出来,有些更是在上线以后才被发现。而不进行 Code Review 或者流于形式的原因,无非是以下几点:
- 工期压得太紧,Code Review 占用开发时间;
- 需求频繁变更,代码的生命周期太短,Code Review 没有必要;
- 团队成员没有 Code Review 的意识和经验。
其实,我们可以将 Code Review 的内容分为固定规则和代码逻辑两部分。对于固定规则部分,借助于工具,完全可以实施自动化的 Code Review。这样团队成员在 Code Review 时只需要关注代码逻辑就可以了,从而既节约了时间,又提升了代码质量,一举两得。
1.2 Phabricator 工具介绍
Phabricator 是一套网络应用程序,目的在于使人们更容易构建软件,特别是在与团队一起工作时。原是 Facebook 内部开发的可视化代码评审工具,现在独立出去,由 Phacility 维护。其主要包含以下几个功能:
- Differential:代码审查工具
- Diffusion:代码仓库浏览器
- Maniphest:bug 追踪工具
- Phriction:wiki 文档管理
Phabricator 以相对较小的设置成本,为软件开发人员提供一个可靠的工具集。 这些工具集成在一起相互协作,并且都是免费和开源的。 其易于学习,理解和使用,运行也非常快速,同时被证明在大数据量( Facebook 有超过 500,000 个提交代码库)和大型组织下具有良好的扩展性。
Arcanist 是 Phabricator 的命令行工具,是开发人员和 Phabricator 沟通的桥梁。它提供对许多 Phabricator 工具的命令行访问,与静态分析(“lint”)和单元测试集成,并管理常见工作流,如将更改提交到 Differential 以供审核。
小结
这里的自动化 Code Review,利用的就是 Arcanist 与静态分析集成的特性,通过自定义 linter,在人工代码审核前先对代码做静态分析。风格类的问题,可以自动进行修复;其它的问题,需要团队成员手动修复。所有问题都修复后,才进入人工审核环节。
1.3 实践本教程的前置要求
若要实践本篇教程,需要满足如下要求:
- 1、已安装 Phabricator
- 2、已安装 Arcanist
可以参考 Phabricator 用户文档 进行环境的安装和配置。
二、Objective-C 代码的自动化 Code Review
针对 Objective-C 代码,团队目前在实践中已经实现了两个方面的自动化 Code Review,一个是静态代码分析,一个是代码格式。
- 在静态代码分析方面,使用的是 OCLint;
- 在代码格式方面,使用的是 clang-format。
2.1 使用 OCLint 进行静态代码分析
2.1.1 OCLint 简介
OCLint 是一个可对 C、C++、Objective-C 代码进行静态分析的工具,可以有效的提升代码质量,能够找出编译器不能发现的潜在问题:
- 可能的 bug:空的 if/else/try/catch/finally 语句。
- 未使用的代码:未使用的局部变量和函数参数。
- 复杂的代码:高 cyclomatic 复杂度,高 NPath 复杂度,以及过长的方法。
- 冗余代码:没有必要的 if 语句以及无用的括号。
- 糟糕的代码:方法行数过多,参数过多。
- 坏的实践:反向的逻辑,对参数重新赋值。
- 其它
目前最新版本为 21.10,包含 70+ 条规则,也支持自定义规则。
OCLint 的工作原理,可以参见下图:
由图可见,OCLint 工作时,首先需要先对工程进行编译,编译过程中的输出,经 xcpretty 工具处理,生成 compile_commands.json 文件。这个文件里列出了工程中每个源文件编译时所用的命令,oclint-json-compilation-database 工具正是基于此文件,对每个源文件执行的静态代码分析。
Arcanist 并没有内置使用 OCLint 的 linter,因此,若想在 arc diff 时使用 OCLint 对代码进行静态分析,需要自定义 linter。
由于 OCLint 的安装和使用,根据开发者身份的不同(制定规则的人和使用规则的人),步骤也会不同。在介绍详细的步骤之前,先看一下整体的思维导图:
2.1.2 Arcanist 集成 OCLint
下面介绍详细的配置步骤:
- 第 1 步, 安装 OCLint
- 规则制定
这里以安装在 ~/Documents 目录为例,从源码构建安装
// 安装依赖
brew install cmake ninja
git clone https://github.com/oclint/oclint.git
cd oclint/oclint-scripts
./make
cd ..
编译完成以后,会有以下路径:
~/Documents/oclint/build/oclint-release
这个就是编译好的 OCLint,我们还需要把这个路径添加到系统的 PATH 中,
在 ~/.bash_profile 或 ~/.zshrc中添加:
OCLINT_HOME=~/Documents/oclint/build/oclint-release export
PATH=$OCLINT_HOME/bin:$PATH
然后执行 source .bash_profile 或 source .zshrc,
使得路径配置立即生效。
- 使用
直接从 Homebrew 安装即可
brew tap oclint/formulae
brew install oclint
安装完成以后,可以在命令行运行 oclint --version 验证安装是否成功。
- 第 2 步,安装 xcpretty
xcpretty 是一个灵活的和快速的 xcodebuild 日志格式化工具,可以将 xcodebuild 的 log 输出格式化为 json 格式。sudo gem install xcpretty
如果遇到提示权限错误,可以选择将其安装在/usr/local/bin。用下面的命令:sudo gem install -n /usr/local/bin xcpretty
- 第 3 步,自定义 Arcanist 使用 OCLint 的 linter
如前所述,OCLint 本身并不能直接和 Arcanist 结合起来使用。我们需要做的是,根据 Arcanist 的关于自定义 linter 文档,自定义一个 linter,这个 linter 在执行的时候使用 OCLint 来对代码进行静态代码分析。
这里有一个定义好的 linter,可以将其添加到项目中,作为一个 git submodule 即可。在项目根目录运行:git submodule add https://gitlab.ihuayue.cn/staryiOS/OCLintLinter.git
然后修改 .arcconfig 文件,目的是为了告诉 Arcanist 去加载自定义的 linter:"load": [ "./OCLintLinter/" ]
可以通过运行 arc linters 命令,验证自定义的 linter 是否可以被 Arcanist成功加载。
如果可以在输出的 linter 列表中看到 CONFIGURED oclint (oclint),说明 Arcanist 现在已经可以加载我们自定义的 OC linter 了。 - 第 4 步,配置项目构建参数
由于 OCLint 工作时,首先要对工程进行编译,所以我们需要配置 xcodebuild 命令所用的参数。这些参数可以在 .arcconfig 文件里进行配置,目前 OC linter 支持如下可配置的参数:
- xcodebuild.workspace:要编译的 workspace;
- xcodebuild.scheme:要编译的 scheme;
- xcodebuild.configuration:指定编译 Debug 版本还是 Release 版本,默认为 Debug;
- xcodebuild.sdk:编译时使用的 SDK。默认为 iphonesimulator;
- xcodebuild.other-flags:传递给 xcodebuild 命令的其它选项。
这里值得一提的是 xcodebuild.other-flags,将其设定为"-dry-run",xcodebuild 命令在执行时,并不会 clean 之前的构建,也不去做真正的链接等操作,可以极大的减少 OCLint 运行所需时间。
完成配置后的 .arcconfig 应该看起来象下面这样:
{
"phabricator.uri" : "http://172.31.0.117:8800/",
"editor" : "vim",
"load": [ "./OCLintLinter/" ],
"xcodebuild.workspace": "ReaderLight.xcworkspace",
"xcodebuild.scheme": "Dreame",
"xcodebuild.configuration": "Debug",
"xcodebuild.other-flags": "-dry-run",
"xcodebuild.sdk": "iphonesimulator"
}
- 第 5 步,配置使用的规则
前面讲了 OCLint 有 71 条规则,在实际的使用中,各个团队可能会希望禁用某些规则,或是调整某些规则的门限值(如一行代码最长可以有多少字符)。OCLint 对这两种方式都支持,并且可以在命令行选项中指定或是通过配置文件指定。
根据 OCLint 的官方文档:Preserve Configurations,合理的做法是在项目根目录创建一个 .oclint 进行规则的配置。但是,在实际使用过程中,会发现配置一开始是生效的,但是在团队运行了一段时间以后,又突然失效了。无奈之下,只能在自定义 linter 的代码中,通过命令行的方式进行指定。
具体做法是在 OCLintLinter.php 中,修改如下方法:
protected function getDefaultFlags() {
$config = $this->getEngine()->getConfigurationManager();
$flags = $config->getConfigFromAnySource('lint.oclint.options', array('-enable-clang-static-analyzer','-disable-rule=BitwiseOperatorInConditional','-disable-rule=ObjCAssignIvarOutsideAccessors','-disable-rule=AssignIvarOutsideAccessors','-rc=LONG_LINE=200','-rc=LONG_METHOD=150','-rc=LONG_VARIABLE_NAME=50','-rc=CYCLOMATIC_COMPLEXITY=20','-rc=NCSS_METHOD=60','-rc=MINIMUM_CASES_IN_SWITCH=2'));
//print_r($flags);
return $flags;
}
禁用某条规则的格式为-disable-rule=RuleName,具体的规则的名字可以在 rules 中查找。
设置某条规则的门限值的格式为-rc=RuleName=Value,例如-rc=LONG_LINE=200。
在实际的开发中,有时会出现一些特殊的情况,导致代码违背了配置的某条规则。例如:
针对系统的库函数或宏等产生的警告;
修改的代价很高,且不易修改。
此时,我们可以选择告诉 OCLint 不要针对相关代码产生警告,而不是去修改我们的代码。
- 第 6 步,告知 Arcanist 对 Objective-C 文件使用自定义的 linter
整个配置过程的最后一步,是需要告知 Arcanist 工具,针对 Objective-C 源文件,调用 OC linter。在 .arclint 文件进行如下配置:"linters": { "Objective-C": { "type": "oclint", "exclude": "(^Pods/)" } }
至此,所有准备工作已经完成,以后在使用 arc diff 命令时,会自动对修改过的源文件,使用 OCLint 进行静态代码分析。提交者在提交代码供团队成员 review 之前,需要修改所有分析出来的问题,或是告知 OCLint 不修改是有意的行为。
关于 .arclint 文件的详细配置,详见 Arcanist User Guide: Lint
2.2 使用 clang-format 来统一代码格式
一个团队中,所有成员的代码格式最好是统一的。传统的做法是,团队负责人或是架构师输出一份编码规范文档,里面有详细的格式要求和说明,然后要求所有成员记住这些格式要求,并在实际编码的过程中去遵守。这种方案在实际的落地时,效果会大打折扣。因为这样会要求每个团队成员都有很好的执行力,主观能动性,以及需要熟记所有的规范。
2.2.1 clang-format 简介
所幸的是,业界已经有可以自动格式化代码的工具。这里介绍下 clang-format,它是基于 clang 的一个命令行工具,能够格式化 C、C++、Objective-C 代码,并且内置支持多种代码风格:Google, Chromium, LLVM, Webkit,Microsoft, Mozilla,也支持自定义风格。
Arcanist 自身并没有内置 clang-format 的 linter,因此需要自定义一个 linter,并进行相应的配置。
2.2.2 Arcanist 集成 clang-format
- 第 1 步,安装 clang-format
可以通过以下命令进行安装:brew install clang-format
- 第 2 步,自定义 Arcanist 使用 clang-format 的 clang-format-linter
- 针对某个工程项目进行安装
在项目的根目录,运行以下命令,然后将变更/添加的文件提交到 git 仓库中。
git submodule add https://gitlab.ihuayue.cn/staryiOS/clang-format-linter.git
然后在 .arcconfig 文件中添加如下内容:
{ "load": [ "./clang-format-linter/" ] }
- 全局安装
Arcanist 可以从绝对路径加载模块,但是有一个 trick,它会搜索它的上一级目录。这意味着我们可以将 clang-format-linter 克隆到 arcanist 和 libphutil 的安装目录,最终目录应该看起来是下面的样子:> ls arcanist clang-format-linter libphutil
这种情况下 .arcconfig 文件应该看起来像下面的样子:{ "load": [ "clang-format-linter" ] }
以上步骤完成后,我们来验证 clang-format-linter 是否可以被 arcanist 加载成功。
运行 arc linters,如果能看到列表中有CONFIGURED clang-format (clang-format),说明自定义的 linter 加载成功。
- 第 3 步,定义我们团队的代码格式
在使用 clang-format 来格式化代码之前,我们还需要定义自己的代码格式。
推荐的作法是基于内置的代码格式配置文件进行修改,这里以基于 LLVM 代码格式为例:
运行 clang-format -style=LLVM -dump-config > .clang-format 先得到一个文件,然后根据文档 Clang-Format Style Options 进行配置,文档里面有详细的各个格式的说明。
上述命令产生的文件里,编程语言设定为 Cpp,若在 Objective-C 项目中直接使用,会提示如下错误:Configuration
file(s) do(es) not support Objective-C。要解决此错误,只需要打开 .clang-format 文件,将
Language 修改为 ObjC 即可解决此问题。
- 第 4 步,告知 Arcanist 对 Objective-C 文件使用 clang-format-linter
整个配置过程的最后一步,是需要告知 Arcanist 工具,针对 Objective-C 源文件,使用 clang-format-linter 来进行 lint。在 .arclint 文件中进行如下配置:{ "linters": { "clang-format": { "type": "clang-format", "include": "(^Classes/)" } } }
至此,所有准备工作已经完成,以后使用 arc diff 时,所有代码会被自动的格式化为预定义的格式,Code Review 时再也不需要在代码格式问题上浪费时间了。
三、Swift 代码的自动化 Code Review
3.1 SwiftLint 简介
Swift 语言目前已经被不少新项目所采用,在国外尤其如此,Stack Overflow 上 iOS 开发的相关问题,很多回答也是基于 Swift 的。针对这个趋势,realm 推出了SwiftLint,一个用于强制检查 Swift 代码风格和规范的工具。它在工作时,hook 了 Clang 和 SourceKit,使用源代码的 AST 表示,来得到更精确的分析结果。
目前最新版本为 0.46.2,支持 Swift 版本为 4.2 及以上。
3.2 Arcanist 集成 SwiftLint
接下来介绍如何将其配置到项目中。
- 第 1 步,安装 SwiftLint
在终端里执行如下命令:brew install swiftlint
- 第 2 步,自定义 Arcanist 使用 SwiftLint 的 linter
也有开发者为 SwiftLint 编写了自定义的 linter,与 clang-format-linter 类似,也有两种安装方式。
- 针对某个工程项目进行安装
在项目的根目录,运行以下命令,然后将变更/添加的文件提交到 git 仓库中。git submodule add https://gitlab.ihuayue.cn/staryiOS/swift-linter.git
然后在 .arcconfig 文件中添加如下内容:{ "load": [ "./swift-linter/" ] }
- 全局安装
将 swift linter 克隆到 Arcanist 和 libphutil 的安装目录,最终目录应该看起来是下面的样子:> ls arcanist swift-linter libphutil
然后在 .arcconfig 文件中添加如下内容:{ "load": [ "swift-linter" ] }
以上步骤完成后,我们来验证 swift linter 是否可以被 Arcanist 加载成功。运行 arc linters,如果能看到列表中有CONFIGURED swift-lint (swift),说明自定义的 linter 加载成功。
- 第 3 步,配置规则
最新版本中包含了 75 条以上的规则,并且 Swift 社区仍在继续贡献新的规则。可以在 Rules 这篇文档里,了解最新的完整规则列表及每条规则的详细信息,并有代码示例。
并不是所有的规则,都是默认开启的,只有其中一部分是默认开启的。配置时,可以使用配置文件进行全局配置;对于代码中一些特殊的地方,可以在代码中禁用一些规则。
- (1) 使用配置文件进行配置
在项目根目录创建一个 .swiftlint.yml 文件,可以配置以下参数:disabled_rules:
从默认开启的规则集合中,禁止掉某些规则;opt_in_rules:
开启某些不在默认开启的规则集合中的规则;whitelist_rules:
只有在这个列表中的规则,才会被开启。这个选项不能和前面两个并存,因为是互斥的。 - (2) 配置文件示例:
disabled_rules: # rule identifiers to exclude from running - colon - comma - control_statementopt_in_rules: # some rules are only opt-in - empty_count # Find all the available rules by running: # swiftlint rulesincluded: # paths to include during linting. `--path` is ignored if present. - Sourceexcluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods - Source/ExcludedFolder - Source/ExcludedFile.swift - Source/*/ExcludedFile.swift # Exclude files with a wildcardanalyzer_rules: # Rules run by `swiftlint analyze` (experimental) - explicit_self# configurable rules can be customized from this configuration file# binary rules can set their severity levelforce_cast: warning # implicitlyforce_try: severity: warning # explicitly# rules that have both warning and error levels, can set just the warning level# implicitlyline_length: 110# they can set both implicitly with an arraytype_body_length: - 300 # warning - 400 # error# or they can set both explicitlyfile_length: warning: 500 error: 1200# naming rules can set warnings/errors for min_length and max_length# additionally they can set excluded namestype_name: min_length: 4 # only warning max_length: # warning and error warning: 40 error: 50 excluded: iPhone # excluded via stringidentifier_name: min_length: # only min_length error: 4 # only error excluded: # excluded via string array - id - URL - GlobalAPIKeyreporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
除了根据团队需要针对已有的规则进行配置,SwiftLint 还支持自定义规则
- (3) 在源代码中禁用一些规则
在一些特殊情况下,我们会违背一些规则,但是在其它情况下,这些规则还是要被遵守。遇到这种情况时,我们可以在代码中,使用注释来禁用规则。格式如下:// swiftlint:disable <rule1> [<rule2> <rule3>...]
列出的规则将会被临时禁用,直到源文件的结束,或是遇到了一个匹配的开启注释:// swiftlint:enable <rule1> [<rule2> <rule3>...]
看几个例子:
// swiftlint:disable colon
let noWarning:String = "" // No warning about colons immediately after variable names!
// swiftlint:enable colon
let hasWarning:String = "" // Warning generated about colons immediately after variable names
使用关键字 all 来指定所有的规则:
// swiftlint:disable all
let noWarning:String = "" // No warning about colons immediately after variable names!
let i = "" // Also no warning about short identifier names
// swiftlint:enable all
let hasWarning:String = "" // Warning generated about colons immediately after variable names
let y = "" // Warning generated about short identifier names
还可以在 disable 或 enable 后面加上 :previous,:this 或是 :next,这时禁用规则只对前一行,当前行,或是下一行生效。
// swiftlint:disable:next force_cast
let noWarning = NSNumber() as! Int
let hasWarning = NSNumber() as! Int
let noWarning2 = NSNumber() as! Int // swiftlint:disable:this force_cast
let noWarning3 = NSNumber() as! Int // swiftlint:disable:previous force_cast
- 第 4 步,告知 Arcanist 对 Swift 源文件使用 swift linter
整个配置过程的最后一步,是需要告知 Arcanist 工具,针对 Swift 源文件,使用 swift linter 来进行 lint。在 .arclint 文件中进行如下配置:
{ "linters": { "swift": { "type": "swift-lint", "include": "(^PathToSource/.*\\.swift$)" } } }
至此,配置完毕,以后使用 arc diff 命令时,会对 Swift 源文件使用 SwiftLint 来进行静态分析。
四、实战演示
前面我们花了很多时间进行环境的配置,规则的配置,想必大家都已经迫不及待的想看一下,自动化的 Code Review 在实战中的表现了。接下来,我们会演示如下内容:
- 对 Objective-C 代码应用 OCLint;
- 在源文件中告知 OCLint 不使用某些规则;
- 自定义一个 OCLint 规则;
- 对 Objective-C 代码应用 clang-format;
- 对 Swift 代码应用 SwiftLint;
- 自定义一个 SwiftLint 规则。
4.1 对 Objective-C 代码应用 OCLint
在示例工程的 ViewController.m 中,添加以下代码,并将代码 commit,但是不要 push,下同。
void foo() { }
void bar() { }
//下面的代码会违背规则:ConstantIfExpression
void example_ConstantIfExpression(){
if (true) // always true {
foo();
}
if (1 == 0) // always false {
bar();
}
}
//下面的代码会违背规则:JumbledIncrementer
void example_JumbledIncrementer(int a) {
for (int i = 0; i < a; i++) {
for (int j = 0; j < a; i++) {
// references both 'i' and 'j'
}
}
}
//下面的代码会违背规则:UseContainerLiteral
void example_UseContainerLiteral(){
NSArray *a = [NSArray arrayWithObjects:@1, @2, @3, nil];
NSDictionary *d = [NSDictionary dictionaryWithObjects:@[@2,@4] forKeys:@[@1,@3]];
NSLog(@"a =%@, d = %@", a, d);
}
//下面的代码会违背规则:ShortVariableName
void example_ShortVariableName(int i) // i is short{
int ii = 0; // ii is short
printf("ii = %d", ii);
}
//下面的代码会违背规则:UnusedLocalVariable
int example_UnusedLocalVariable(int a){
int i = 0; // variable i is declared, but not used
return 0;
}
//下面的代码会违背规则:ParameterReassignment
void example_ParameterReassignment(int a){
if (a < 0) {
a = 0; // reassign parameter a to 0
}
}
执行 arc diff --trace 并填写相关信息后,终端上显示 Linting…,说明正在执行 linter。稍后,打印出如下警告信息:
这说明我们配置的 OC linter,检测出了将要提交的代码中存在的问题,并且指明了行号和错误的位置。开发人员在提交 review 之前,需要做针对性的修改,以消除这些警告。
4.2 在源文件中告知 OCLint 忽略规则
在源文件中,我们有两种方式,可以和 OCLint 通信,告知其忽略一些规则或是全部规则。
使用 Annotations
可以像下面这样禁用一条规则:
__attribute__((annotate("oclint:suppress[unused method parameter]")))
也可以同时禁用多条规则:
__attribute__((annotate("oclint:suppress[high cyclomatic complexity]"), annotate("oclint:suppress[high npath complexity]"), annotate("oclint:suppress[high ncss method]")))
或者禁用所有规则:
__attribute__((annotate("oclint:suppress")))
Annotation 可以被声明在不同的地方,不同的地方,会导致不同的作用域,例如:
bool __attribute__((annotate("oclint:suppress"))) aMethod(int aParameter) {
// warnings within this method are suppressed at all
// like unused aParameter variable and empty if statement
if (1) {}
return true;
}
// suppress sender variable
- (IBAction)turnoverValueChanged: (id) __attribute__((annotate("oclint:suppress[unused method parameter]"))) sender {
int i;
// won't suppress this one
[self calculateTurnover];
}
// again, suppress the warnings for entire method
- (void)dismissAllViews:(id)currentView parentView:(id)parentView __attribute__((annotate("oclint:suppress"))) {
[self dismissTurnoverView];
// plus 30+ lines of code of dismissing other views
}
使用 !OCLint comment
使用该语法,可以对某一行代码禁用所有规则。例如:
void a() { int unusedLocalVariable; //!OCLINT}
如果不加 //!OCLINT,那么该行代码会违背规则 UnusedLocalVariable;反之则不会产生任何警告。
4.3 自定义一个 OCLint 规则
OCLint 虽然定义了 70+ 条规则,但并不一定能满足所有人的需要,考虑到这一点儿,OCLint 提供了对自定义规则的支持。
规定的制定主要基于三个类:
- AbstractASTVisitorRule
- AbstractASTMatcherRule
- AbstractSourceCodeReaderRule,
它们的关系如下:
RuleBase | |-AbstractASTRuleBase | |-AbstractASTVisitorRule | |-AbstractASTMatcherRule | |-AbstractSourceCodeReaderRule
AbstractSourceCodeReaderRule
提供 eachLine 方法,逐行的读取源码。如果想要基于每行的内容编写规则,可以继承该类。
AbstractASTVisitorRule (一般继承这个类)
继承自该类的 rule,可以实现访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的,在 AbstractASTVisitorRule 的 apply 方法中可以看到代码实现,开发者只需要通过重写 bool Visit* 方法来访问特定类型的节点,在该函数中实现检查操作,其返回值往往是返回 true,返回值表示是否继续递归检查。
AbstractASTMatcherRule
继承自 AbstractASTMatcherRule 的 rule,实现 setUpMatcher 方法,在 setUpMatcher() 方法中实现添加 matcher,当检查发现匹配的结果时,会调用 callback() 方法,故重定义 callback 方法来对匹配的结果进行处理操作。
举一个现实中的例子
在 MVVM 设计模式下,我想让 ViewModel 的属性都是只读的。因为我只想通过与 Model 的数据绑定来更新 ViewModel 的值,或是在其内部更新状态。现在我需要实现一个规则来找出那些非只读属性。
- 第 1 步,创建自定义规则的源文件
这里以 OCLint 安装在 ~/DevEnvironment/oclint 为例。
cd ~/DevEnvironment/oclint
oclint-scripts/scaffoldRule ViewModel -t ASTVisitor
其中 ViewModel 是自定义规则的名字,ASTVisitor 是我们将要继承的 Rule。命令运行完成以后,会在 oclint-rules/rules/custom 目录生成对应的 CPP 文件和 MakeFile,如图所示:
- 第 2 步,生成 Xcode 工程目录,便于开发
mkdir oclint-xcoderules
cd oclint-xcoderules
touch create-xcode-rules.sh
chmod 777 create-xcode-rules.sh`
打开 create-xcode-rules.sh 并添加以下内容:
#! /bin/sh -ecmake -G Xcode \ -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++ \ -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \ -D OCLINT_BUILD_DIR=../build/oclint-core \ -D OCLINT_SOURCE_DIR=../oclint-core \ -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics \ -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \ -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
然后执行
./create-xcode-rules.sh
如果看到下面的输出,说明 xcode 工程创建成功。
并且在 oclint-xcoderules 目录下生成了以下文件:
- 第 3 步,打开生成的工程,找到自定义规则源文件,开发
打开工程后,在 Project Navigator 中拉到最下面,找到 ViewModelRule.cpp,并将 VisitObjCImplementationDecl 方法取消注释,然后修改实现为:
bool VisitObjCImplementationDecl(ObjCImplementationDecl *node) {
ObjCInterfaceDecl *interface = node->getClassInterface();
bool isViewModel = interface->getName().endswith("ViewModel");
if (!isViewModel) {
return false;
}
for (auto property = interface->instprop_begin(), propertyEnd = interface->instprop_end(); property != propertyEnd; property++) {
clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;
if (propertyDecl->getName().startswith("UI")) {
addViolation(propertyDecl, this);
}
auto attrs = propertyDecl->getPropertyAttributes();
bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) > 0;
if (isReadwrite && isViewModel) {
addViolation(propertyDecl, this);
}
}
return true;
}
- 第 4 步,编译工程,生成自定义规则的动态链接器
保存文件后,选择 ViewModelRule 的 scheme,然后按下 CMD + B 来构建。编译完成后,结果会生成在如图所示的位置: - 第 5 步,将生成的动态链接库,拷贝到 rules 目录
为了能像内置的规则一样使用,需要将其拷贝到如下位置: - 最后,验证自定义的规则是否生效
在 ViewController.m 中加入如下代码:
@interface TestViewModel : NSObject
@property (nonatomic, strong) NSNumber *uid;
@end
@implementation TestViewModel
@end
将代码 commit 后,执行 arc diff,并填写相应信息后,过会儿会看到如图所示警告:
说明我们自定义的规则生效了!
4.4 对 Objective-C 代码应用 clang-format
假设我们对代码有以下要求:
- 连续多行的等号,对齐在同一列;
- 不允许if (a) { return; } 这类代码,必须要换行;
- switch 语句,即使 case 对应的代码极少,也不允许都写在一行;
- 仅允许空方法可以写在一行;
- switch 语句中,case 必须缩进;
- 函数体的开头,不允许有多余的空行。
- 要想实现上述代码风格,我们需要在配置文件中做如下设置:
AlignConsecutiveAssignments: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
IndentCaseLabels: true
KeepEmptyLinesAtTheStartOfBlocks: false
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//break rule: AlignConsecutiveAssignments
int a = 0; int addition = 1; int loadingSetup = 2;
printf("a = %d, additon = %d, loadingSetup = %d", a, addition, loadingSetup);
//break rule:AllowShortBlocksOnASingleLine
if (a) {return;}
//break rule: AllowShortCaseLabelsOnASingleLine
switch (a) {
case 0: NSLog(@"hello"); break;
case 1: NSLog(@"1"); break;
default: break;
}
//break rule: IndentCaseLabels
switch (addition) { case 0: break; case 1: break; default: break; }
}
//break rule: KeepEmptyLinesAtTheStartOfBlocks
- (void)additionalEmptyLinesNotAllowed {
NSLog(@"this is the method body");
}
void emptyIsAllowed() {}
//break rule: AllowShortFunctionsOnASingleLinevoid
nonEmptyIsNotAllowed() {NSLog(@"I'm not empty");}
将修改 commit 后,运行 arc diff 时,源文件会按照配置进行格式化处理,以我们 dreame 为例,这里看一下修改前和修改后的对比:
代码被按照规范进行了格式化!
4.5 对 Swift 代码应用 SwiftLint
创建一个 Swift 源文件,基类为 UIViewController,然后在类声明的上方,添加以下代码:
//下面的代码会违背规则:Duplicate Imports
//重复添加了某个框架
import AVFoundation
import Dispatch
import AVFoundation
//下面的代码会违背规则:Dynamic Inline
//@inline(__always)和dynamic应该避免一起使用
class C {
@objc @inline(__always) public dynamic func f() {}
}
在实现里面添加如下代码:
//下面的代码会违背规则:Colon
//定义变量的时候,冒号应该紧跟着变量名;用于字典的时候,应该紧跟着key
func test_colon() {
let abc :String="def"
print(abc)
let def: ([String], String, Int) = (["hello"], "world", 2)
let gh :Int=0
let dic : [String: String] = ["hello" :"world", "hi" :"swift"]
print(def)
print(gh)
print(dic)
}
//下面的代码会违背规则:Comma Spacing
//逗号前面不应该有空格,后面应该有一个空格
func test_comma(a: String ,b: String) {
enum MyEnum {
case EnumA ,EnumB
}
}
//下面的代码会违背规则:Cyclomatic Complexity
//复杂度过高
func test_cyclomatic_complexity() {
if true {
if true {
if false {}
}
}
if false {}
let i = 0
switch i {
case 1: break
case 2: break
case 3: break
case 4: break
default: break
}
for _ in 1...5 {
guard true else {
return
}
}
}
//下面的代码会违背规则:Discarded Notification Center Observer
//注册NotificationCenter的观察者的时候,如果使用了block版本,那么应该存储返回的observer,这样
//才能去移除观察者
func test_discarded_notification_center_observer() {
NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { _ in }
}
//下面的代码会违背规则:Discouraged Direct Initialization
//有些类的对象,不建议直接调用初始化方法进行创建
func test_discouraged_direct_init() {
let device = UIDevice()
let bundle = Bundle()
print("device=\(device), bundle = \(bundle)")
}
//下面的代码会违背规则:Identifier Name
//变量命名不规范
func test_identifier_name() {
let MyLet = 0
print(MyLet)
let _myLet = 0
print(_myLet)
let myLet_ = 0
print(myLet_)
let myExtremelyVeryVeryVeryVeryVeryVeryLongLet = 0
print(myExtremelyVeryVeryVeryVeryVeryVeryLongLet)
let i = 0
let id = 0
print("i=\(i), id=\(id)")
}
将代码 commit,然后运行 arc diff,SwiftLint 会给出如下警告:
…
不规范的代码都被明确的标记出来了,团队成员在提交代码 review 之前,必须消除这些警告!
4.6 自定义一个 SwiftLint 规则
SwiftLint 支持在配置文件 .swiftlint.yml 里,基于正则表达式添加自定义的规则。
举个例子,我们需要在产品文档以及代码中统一命名,希望能够检测出代码中出现’Ninja’的地方,警告开发者将其统一为’Pirate’,那么我们可以这么定义:
custom_rules:
pirates_beat_ninjas: # 规则标识符
name: "Pirates Beat Ninjas" # 规则名称,可选
regex: "([nN]inja)" # 匹配的模式
match_kinds: # 需要匹配的语法类型,可选
- comment
- identifier
message: "Pirates are better than ninjas." # 提示信息,可选
severity: error # 提示的级别,可选
no_hiding_in_strings:
regex: "([nN]inja)"
match_kinds: string
开发者写了如下代码时:
enum CharacterTypes {
case ninja
case king
}
func test_customRule() {
//Everyone knows ninjas are the best
let character = CharacterTypes.ninja
let hideOut = "My ninja can hide in this string?"
print("character = \(character), hideOut = \(hideOut)")
}
就违背了自定义的规则,会被 SwiftLint 检测出来,给出警告。
小结
在实战演示环节,虽然只展示了少量的规则以及添加了非常简单的新规则,但是我们可以看到,这些工具已经能够有效的节约 Code Review 的时间,并且提升代码的质量和规范性。在实践中,可以先用工具自带的那些规则,如果感觉规则不够用,那么或者自定义规则,或者寻找其它的代码静态分析工具,然后通过自定义 linter,将其集成到 Arcanist 中。比如针对 Objective-C 语言,还有以下工具:
- Infer:Facebook 开源的用来执行增量分析的一款静态分析工具,由 OCaml 语言编写。Infer 可以分析 Objective-C,Java 或者 C 代码,报告潜在的问题。可以访问项目地址或中文网站了解更多信息。
- Coverity:检测和解决 C、C++、Java 和 C# 源代码中最严重的缺陷的领先的自动化方法。
- Faux Pas:一个 Xcode 辅助工具,用以检查 Xcode 项目,找出常见的错误、隐藏的 bug、不良实践以及可维护性问题和风格问题。 拥有可视化界面和命令行两种操作方式。可以查看 Faux Pas 官网 和 Fauxpas 命令行使用介绍与规则全解析 了解其支持的规则。
参考