一、概述
随着业务迭代速度的加快,团队也变得越来越大,人工codeReview的成本也越来越高,而且完全依赖人工也无法确保能review到每一处改动,这个时候,运用静态代码检测就能极大的提高检测效率,节省开发人员的时间。
目前iOS端主流的静态代码分析工具大概是这三个:Clang静态分析,infer,OCLint,三者各有优缺点。
Clang: 苹果自带的词法分析器,如果需要自定义检测规则,需要重新下载编译llvm,还需要修改Xcode的编译器,配置各种路径。比较繁琐,不推荐。
infer: facebook推出的的静态代码检测工具,检测效率高,支持增量分析,支持OC,java等多种语言。但是定制性不强,可自定义规则程度不高,而且暂无法和sonarQube平台配合分析。
OCLint: 可用于持续集成,常和Jenkins、SonarQube平台配合适用。可定制化程度高,更多的检查规则和定制,能够发现很多潜在的问题。
由于考虑到需要和Jenkins、SonarQube配合使用,而且需要根据团队的代码规范来高度自定义检测规则,这里暂选用OCLint做静态代码分析。
二、OCLint集成
1.通过Homebrew安装
- 安装homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 安装OCLint
brew install OCLint
- 安装xcpretty
sudo gem install xcpretty
2.源码安装(⚠️如果想自定义规则,源码安装是必不可少的️)
-
github上下载OCLint源码
-
安装CMake和Ninja
安装CMake,前往cmake官网,下载Mac操作系统对应的安装包。安装完成之后,运行cmake图形界面程序,在左上角的选项栏中选择Tools,点击How to install for Command Line Use。 之后弹出来一个消息框:
One may add CMake to the PATH:
PATH="/Applications/CMake.app/Contents/bin":"$PATH"
Or, to install symlinks to '/usr/local/bin', run:
sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install
Or, to install symlinks to another directory, run:
sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install=/path/to/bin
安装Ninja
brew install Ninja
- cd进入源码目录的
oclint-scripts
文件夹中,执行./make
,执行后,网速快的话,大概要等半个小时,慢的话,估计个把小时。这个过程会下载llvm,clang源码,然后编译llvm,clang,oclint源码,安装完毕后,见下图:

build文件夹就是release环境下的安装好的oclint,自带的规则已编译好,以dylib动态库的形式放在build/oclint-release/lib/oclint/rules文件夹下。
3.检测项目
- 附一段适用于小项目,项目文件不多的检测脚本。 xxx修改为自己的项目名即可
#!/bin/bash -il
source ~/.bashrc
myworkspace=xxx.xcworkspace
myscheme=xxx
# clean cache
rm -rf ~/Library/Developer/Xcode/DerivedData/;
rm compile_commands.json;
rm oclint_result.xml;
# clean -- build -- OCLint analyse
echo 'start analyse';
xcodebuild -workspace $myworkspace -scheme $myscheme clean&&
xcodebuild -workspace $myworkspace -scheme $myscheme \
-configuration Debug GCC_PRECOMPILE_PREFIX_HEADER=YES CLANG_ENABLE_MODULE_DEBUGGING=NO COMPILER_INDEX_STORE_ENABLE=NO \
-destination 'platform=iOS Simulator,name=iPhone X' \
| xcpretty -r json-compilation-database -o compile_commands.json&&
oclint-json-compilation-database -e Pods -e node_modules -- \
-report-type pmd \
-rc LONG_LINE=300 \
-rc LONG_METHOD=200 \
-rc LONG_VARIABLE_NAME=40 \
-rc LONG_CLASS=3000 \
-max-priority-1=1000 \
-max-priority-2=1000 \
-max-priority-3=2000 \
-disable-rule=UnusedMethodParameter \
-disable-rule=AvoidPrivateStaticMembers \
-disable-rule=ShortVariableName \
-allow-duplicated-violations=false >> oclint_result.xml; \
echo 'end analyse';
# echo result
if [ -f ./oclint_result.xml ];
then echo 'done';
else echo 'failed';
fi
如果分析中出现以下错误:
Traceback (most recent call last):
File "/usr/local/bin/oclint-json-compilation-database", line 87, in <module> exit_code = subprocess.call(oclint_invocation, shell=True)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 522, in call return Popen(*popenargs, **kwargs).wait()
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 710, in __init__ errread, errwrite)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1335, in _execute_child raise child_exception OSError: [Errno 7] Argument list too long
那么是xcpretty -r json-compilation-database -o compile_commands.json
这一步,生成的compile_commands.json文件过大,这个问题具体可以查看oclint的issue233,里面有相应的解决方案。
4.集成到Xcode中
由于Xcode目前已经禁止集成插件,如果想把oclint的自定义的规则提示,集成到Xcode中,变成即时提示,需要HackXcode,还需要重新下载编译llvm,修改Xcode自带的llvm编译器,比较麻烦。这里采用OCLint官网对Xcode的配置方法,该方法虽不能即时提醒,但是可以通过编译Target,把项目所有不符合oclint规则的提示都显示出来。
三、自定义规则前期准备
前面OCLint集成的第二步源码安装,有说到,要想自定义规则,必须通过源码安装,因为我们需要把OCLint自带的规则和我们自定义的规则集成到Xcode中,这样方便我们写自定义规则的代码和编译。
1.生成自定义规则模版
- cd到源码根目录,执行下面的命令。
oclint-scripts/scaffoldRule RuleName -t ASTVisitor
-t <Type> -n <Name> -c <Category> -p <Priority>
-t: 表示自定义的规则继承自那个模版,OCLint的规则模版有三个:
RuleBase
|
|-AbstractASTRuleBase
| |_ AbstractASTVisitorRule
| |_AbstractASTMatcherRule
|
|-AbstractSourceCodeReaderRule
AbstractSourceCodeReaderRule:eachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类。
AbstractASTVisitorRule:可以访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的。在 apply 方法内可以看到代码实现,开发者只需要重载 bool visit* 方法来访问特定类型的节点。其值表明是否继续递归检查。
AbstractASTMatcherRule️ :实现setUpMatcher 方法,在方法中添加 matcher,当检查发现匹配结果时会调用 callback 方法。然后通过 callback 方法来继续对匹配到的结果进行处理。
-n: 即规则检查完成后的规则名。 -c: 规则的分类。 -p: 规则的优先级。
2.将自定义规则生成Xcode工程,方便编写代码,和编译。
- 在OCLint源码目录下建立一个文件夹oclint-xcodeproject,cd到该目录,执行命令:
touch create_xcodeproject.sh
vim create_xcodeproject.sh
chmod 777 create_xcodeproject.sh
- 将下面脚本复制进去
#! /bin/sh -e
cmake -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
- 结束编辑输入wq保存退出后,执行命令
./create_xcodeproject.sh
执行该脚本后,会在oclint-xcodeproject下生成Xcode工程,见下图:

四、Clang与AST简介
在上述Xcode中,找到自己自定义的规则文件,可以看到里面有很多bool visitXXX回调函数被注释了,该回调函数就是我们后面写自定义规则代码的主要区域。
要想编写自定义的规则,需要有一点点C++的基础,还需要懂一点Clang,不懂也没关系,写的时候看源码就可以了,Clang的API都很清楚明了。
1.AST语法树
先看一张图:

从图中可以看出Clang是LLVM的编译器前端,用来分析语言的语法,它能够将语言转换成抽象语法树(abstract syntax code,AST)。
比如现在有一段OC代码:
self.testBlock = ^{
[self testDemoBlock];
};
[self setObjcBlock:^{
BViewController *BVC = [[BViewController alloc] init];
[self.navigationController pushViewController:BVC animated:YES];
}];
BOOL res = arc4random()%2;
if (res == NO) NSLog(@"%@",@(res));
BOOL conditionA = arc4random()%3;
BOOL conditionB = arc4random()%3;
if (conditionA == YES && conditionB) {
NSLog(@"%@", @(conditionA));
}else {
NSLog(@"%@", @(conditionA));
}
我现在想查看这个文件的AST语法结构,可以执行下面这个命令
clang -Xclang -ast-dump -fsyntax-only test.m
执行后,结构如下:
-ImplicitParamDecl 0x7f9409825750 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
| | |-BlockDecl 0x7f9409831948 <line:34:22, line:36:5> line:34:22
| | | |-capture ImplicitParam 0x7f94098256f0 'self' 'ViewController *'
| | | `-CompoundStmt 0x7f9409831a88 <col:23, line:36:5>
| | | `-ObjCMessageExpr 0x7f9409831a58 <line:35:9, col:28> 'void' selector=testDemoBlock
| | | `-ImplicitCastExpr 0x7f9409831a40 <col:10> 'ViewController *' <LValueToRValue>
| | | `-DeclRefExpr 0x7f9409831a18 <col:10> 'ViewController *const' lvalue ImplicitParam 0x7f94098256f0 'self' 'ViewController *'
| | |-BlockDecl 0x7f9409831b88 <line:39:24, line:42:5> line:39:24
| | | |-capture ImplicitParam 0x7f94098256f0 'self' 'ViewController *'
| | | |-CompoundStmt 0x7f9409831db0 <col:25, line:42:5>
| | | | `-DeclStmt 0x7f9409831d58 <line:40:9, col:62>
| | | | `-VarDecl 0x7f9409831c70 <col:9, col:61> col:26 BVC 'BViewController *' cinit
| | | | `-ImplicitCastExpr 0x7f9409831d40 <col:32, col:61> 'BViewController *' <BitCast>
| | | | `-ObjCMessageExpr 0x7f9409831d10 <col:32, col:61> 'id':'id' selector=init
| | | | `-ObjCMessageExpr 0x7f9409831ce0 <col:33, col:55> 'id':'id' selector=alloc class='BViewController'
| | | `-VarDecl 0x7f9409831c70 <col:9, col:61> col:26 BVC 'BViewController *' cinit
| | | `-ImplicitCastExpr 0x7f9409831d40 <col:32, col:61> 'BViewController *' <BitCast>
| | | `-ObjCMessageExpr 0x7f9409831d10 <col:32, col:61> 'id':'id' selector=init
| | | `-ObjCMessageExpr 0x7f9409831ce0 <col:33, col:55> 'id':'id' selector=alloc class='BViewController'
| | `-CompoundStmt 0x7f9409832040 <line:28:21, line:57:1>
| | |-ExprWithCleanups 0x7f9409831e40 <line:39:5, line:42:6> 'void'
| | | |-cleanup Block 0x7f9409831b88
| | | `-ObjCMessageExpr 0x7f9409831e08 <line:39:5, line:42:6> 'void' selector=setObjcBlock:
| | | |-ImplicitCastExpr 0x7f9409831df0 <line:39:6> 'ViewController *' <LValueToRValue>
| | | | `-DeclRefExpr 0x7f9409831b60 <col:6> 'ViewController *' lvalue ImplicitParam 0x7f94098256f0 'self' 'ViewController *'
| | | `-BlockExpr 0x7f9409831dd8 <col:24, line:42:5> 'void (^)(void)'
| | | `-BlockDecl 0x7f9409831b88 <line:39:24, line:42:5> line:39:24
| | | |-capture ImplicitParam 0x7f94098256f0 'self' 'ViewController *'
| | | |-CompoundStmt 0x7f9409831db0 <col:25, line:42:5>
| | | | `-DeclStmt 0x7f9409831d58 <line:40:9, col:62>
| | | | `-VarDecl 0x7f9409831c70 <col:9, col:61> col:26 BVC 'BViewController *' cinit
| | | | `-ImplicitCastExpr 0x7f9409831d40 <col:32, col:61> 'BViewController *' <BitCast>
| | | | `-ObjCMessageExpr 0x7f9409831d10 <col:32, col:61> 'id':'id' selector=init
| | | | `-ObjCMessageExpr 0x7f9409831ce0 <col:33, col:55> 'id':'id' selector=alloc class='BViewController'
| | | `-VarDecl 0x7f9409831c70 <col:9, col:61> col:26 BVC 'BViewController *' cinit
| | | `-ImplicitCastExpr 0x7f9409831d40 <col:32, col:61> 'BViewController *' <BitCast>
| | | `-ObjCMessageExpr 0x7f9409831d10 <col:32, col:61> 'id':'id' selector=init
| | | `-ObjCMessageExpr 0x7f9409831ce0 <col:33, col:55> 'id':'id' selector=alloc class='BViewController'
| | `-IfStmt 0x7f9409832008 <line:50:5, line:55:5>
| | |-<<<NULL>>>
| | |-<<<NULL>>>
| | |-OpaqueValueExpr 0x7f9409831fe8 <<invalid sloc>> '_Bool'
| | |-CompoundStmt 0x7f9409831f50 <line:50:49, line:52:5>
| | `-CompoundStmt 0x7f9409831fd8 <line:53:10, line:55:5>
从上面结构图中,我们大致可以看出: block被分析成了BlockDecl节点,if语句分析成了IfStmt,中间的花括号{}就是CompoundStmt,定义的变量就是VarDecl节点,等等,上图一层一层的结构,就形成了树状结构。
-
Clang中视所有代码单元为语句(statement),Clang中使用类Stmt来代表statement。Clang构造出来的语法树,其节点类型就是Stmt。针对不同类型的语句,Clang有对应的Stmt子类,例如GotoStmt、IfStmt。Clang中的表达式也被视为语句,Clang使用Expr类来表示表达式,而Expr本身就派生于Stmt。
-
一种是Stmt(Statement),另外一种是Decl(Declaration)。Stmt很明显,就是表达式的节点,具有操作性,比如CompoundStmt对应的{},ifstmt对应if,以上所有的Operator、Expr、Literal结尾的都是stmt的子类。Decl就对应的var,method,function, BlockDecl。 我们可以通过某个节点往下遍历所有节点。比如FucntionDecl往下遍历可以得到ParamVarDecl和CompoundStmt,然后通过CompoundStmt可以得到IfStmt和ReturnStmt。
-
每个语法树节点都会有一个子节点列表,在Clang中一般可以使用如下语句遍历一个节点的子节点
for (Stmt::child_iterator it = stmt->child_begin(); it != stmt->child_end(); ++it) {
Stmt *child = *it;
}
当用Clang进行进行语法分析的时候,它每遍历到一个Stmt或者Decl,都会执行对应的回调函数,在回调函数里,我们就能根据Clang的API分析代码的规范性或者结构,甚至能直接拿到分析的源码,比如:
//遍历OC属性节点
bool VisitObjCPropertyDecl(ObjCPropertyDecl *decl)
{
return true;
}
//遍历block节点
bool VisitBlockDecl(BlockDecl *node)
{
return true;
}
//遍历到了if语句
bool VisitIfStmt(IfStmt *node)
{
return true;
}
五、编写自定义规则及常用访问Stmt,Decl
1.bool VisitVarDecl(VarDecl *node)
VarDecl:即变量和常量的声明。
如何区分静态变量、本地变量、文件变量.
//静态变量
node->isStaticLocal()
//本地变量
node->isLocalVarDecl()
//文件变量
node->isFileVarDecl()
检测是不是常量:
private:
ASTContext *Context;
public:
void setASTContext (ASTContext &context)
{
this->Context = &context;
}
node->getType().isConstant(*this->Context)
2.bool VisitObjCPropertyDecl(ObjCPropertyDecl *decl)
即访问属性节点。
- 获取属性修饰词
//拿到属性的修饰词
ObjCPropertyDecl::PropertyAttributeKind attribute = decl->getPropertyAttributes();
//注意拿到的不是一个数组,而是一个十六进制的枚举,如果想判断该属性是否包含某个修饰词,要用与运算,如下:检测是否存在copy修饰词。
attribute & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_copy
- 获取属性声明类型
//转为了字符串类型
StringRef type = StringRef(decl->getType().getAsString());
- 获取属性名
string name = decl->getPropertyDecl()->getNameAsString();
3.bool VisitIfStmt(IfStmt *node)
即访问if语句,这里的if语句,是包含else语句的。
- 拿到if花括号里的语句
Stmt *thenStmt = node->getThen();
//判断if 语句有没有{}
if (!thenStmt || !(isa<CompoundStmt>(thenStmt))) {
addViolation(node, this, "if 语句没有{}");
}
- 获取if中的条件
//该方法只是获取if中的条件表达式
Expr *condition = node->getCond();
- 获取if条件中的源码
Expr *condition = node->getCond();
SourceManager *sourceManager = &_carrier->getSourceManager();
SourceLocation startLocation = sourceManager->getFileLoc(condition->getBeginLoc());
SourceLocation endLoc = sourceManager->getFileLoc(condition->getEndLoc());
通过condition->getBeginLoc()和condition->getEndLoc()拿到的源码位置是有问题的,它默认碰到二元运算符,就会返回,如下图:


#include "clang/Lex/Lexer.h"
,接着上面内容
///此方法会定位到 ')',而上面那个方法则定位到最后一个条件的 ‘==’
SourceLocation endLocation = Lexer::findLocationAfterToken(endLoc, tok::r_paren, _carrier->getSourceManager(), LangOptions(), false);
int length = sourceManager->getFileOffset(endLocation)
sourceManager->getFileOffset(startLocation) - 1;
StringRef stmtStr = StringRef(sourceManager->getCharacterData(startLocation), length);
Lexer::findLocationAfterToken
这个方法中第二个参数tok::r_paren
有四个选项,表示碰到哪个字符,就返回相应的位置信息,拿到源码后,我们就能进行相应的操作了。

4.bool VisitObjCMethodDecl(ObjCMethodDecl *node)
即访问OC方法。
- 获取返回值类型
std::string returnType = node->getReturnType().getAsString();
- 获取方法名
StringRef selectorString = StringRef(node->getSelector().getAsString());
- 获取方法参数列表
ArrayRef<ParmVarDecl*> parameters = node->parameters();
ParmVarDecl继承自VarDecl,通过它可以再拿到参数类型,和参数名。
5.bool VisitObjCMessageExpr(ObjCMessageExpr *node)
即方法调用。
- 判断是类方法还是实例方法
//获取方法调用类型,有四个取值Class,Instance,SuperClass,SuperInstance
ObjCMessageExpr::ReceiverKind kind = node->getReceiverKind();
//类方法
node->isClassMessage()
//实例方法
node->isInstanceMessage()
- 获取传参
Expr **exprArray = node->getArgs();
- 获取方法调用类型
string type = node->getReceiverType().getAsString();
6.bool VisitBlockDecl(BlockDecl *node)
即访问Block结点,使用这个结点,我们可以对block里面使用了self做警告处理,下面是检测block中可能造成循环引用的大概思路。
这里说一下可能造成循环引用的情况,self持有对象的方法或者属性中含有block,如果block代码块中含有self,基本都会造成循环引用,还有一种情况就是,一个局部对象或者类持有了一个block代码块,代码块里面含有self,这种情况可能会造成,也可能不会,比如UIView的动画块或者一些线程函数dispatch代码中,使用self,不会造成循环引用。
既然是为了统一代码规范,那不管self里面有没有持有block,如果block里面没有书写strong或者weak,我们都给它添加warning(由于reactive-obj有weakify和strongify,block中可以直接写self,所以不能简单得通过block代码块中含有self,就给予警告)。
六、检测成果与分析
OCLint可输出很多格式的分析报告,比如:
html报告: 将结果可视化,并定位到具体那一行代码,给出相应的提示。


检测成果:
1.Block循环引用
项目中可能存在的Block循环引用予以告警并告知负责人。该类问题可能导致内存泄漏,所以解决优先级较高。
2.属性命名规范告警
- 不合规范的属性修饰词
- 属性名以大写或者下划线开头
- 以UI或者new开头的属性名
3.构造方法返回类型告警
对于构造方法的返回类型为id的给予告警,给出更改提示为instancetype。
4.变量命名规范告警
局部变量以大写字母开头,或者常量不以项目前缀为开头的给予告警,此类告警优先级较低。
5.非字面量创建对象告警
项目中,NSArray、NSNumber、NSDictionary类型的对象,如果初始化时,带有默认值,提倡使用字面量的方式创建,即使用@[],@{}等。
6.Case语句告警
Switch, Case语句中,如果一个case条件没有以break结尾,可能会导致逻辑错误或其他问题,给予告警,优先级较高。
7.条件语句告警
如果if后的语句没有{},给予告警;if条件中如果含有对布尔值的判断,给予提示,可以通过用取反或省略布尔值代替。
8.代码重复率检测
通过jenkins将项目代码上传到sonarQube平台,该平台会分析项目代码的重复率,通过整理,可以提高项目代码的复用性和封装性。
七、总结
实现OCLint静态代码检测,其实有很多坑在里面,比如脚本编写,官方的脚本不一定可用,需要做一定的修改,项目大了,也不一定适用,再就是源码安装OCLint,可能安装过程中,会有很多奇奇怪怪的问题,后面自定义规则上,需要分析代码的AST树结构,再查看Clang的源码和API,思考实现检测的逻辑。总之,实现过程中会有很多坑坑洼洼,但是实现完成后,可以节省大量的人力,对提高代码质量、开发效率以及App的使用体验都有很大的帮助。