OClint介绍
是一个静态代码分析工具, 通过分析C,C++, Objective-C代码,找出可能出现问题的代码,冗余代码,并可以自己定义规则去分析检索。
oclint可以自己下载源码编译,也可使用Homebrew安装brew install oclint,截止笔者文章时Homebrew中oclint版本为OCLint version 21.10。
由于 OCLint 是一个基于 Clang tool 的静态代码分析工具,所以不得不提一下 Clang。 Clang 作为 LLVM 的子项目, 是一个用来编译 c,c++,以及 oc 的编译器。 OCLint 本身是基于 Clang tool 的,换句话说相当于做了一层封装。 它的核心能力是对 Clang AST 进行分析,最后输出违反规则的代码信息,并且导出指定格式的报告。
OCLint默认有71条规则,查看OCLint规则索引,可以在xocde的脚本里面写代码过滤不想要的规则。
查看项目基本信息
xcodebuild -list
OCLint安装
1.通过Homebrew
brew tap oclint/formulae
brew install oclint
2.确认安装成功
oclint
3.安装xcpretty
sudo gem install xcpretty
OCLint 的使用
这里介绍一些简单常用的用法,足够应对很多场景,其他详细用法可以到OCLint Manual上查看。
OCLint 有三个指令:oclint、oclint-json-compilation-database、oclint-xcodebuild。
- 1、oclint:常规核心指令。
- 2、oclint-json-compilation-database:从编译好的compile_commands.json 文件中读取配置信息并执行 oclint。
- 3、oclint-xcodebuild:主要用于生成compile_commands.json文件,现在已经不进行维护了,我们可以用xcpretty替代,来生成json文件。
oclint指令
使用语法
oclint [options] <source> -- [compiler flags]
[options]为一些参数选项,可以是规则加载选项、报告形式选项等:
1、 -R <路径> : 检测所用的规则的路径,默认路径$(/path/to/bin/oclint)/../lib/oclint/rules 2、-disable-rule <规则名>: 让相对应的规则失效(OCLint 规则列表)。 3、-rc <参数>=<值> :修改阈值 4、-report-type <报告类型>,有"text"、“html”、“json”、“pmd”、“xcode”几个类型 5、-o <路径> 报告生成路径。
eg:
oclint -R /path/to/rules -disable-rule ObjCAssignIvarOutsideAccessors -report-type xcode
oclint-json-compilation-database 指令
这是我们主要用到的指令,它是oclint 指令的升级版,使用起来相对 oclint 指令简单方便。 可以通过 -- 的方式在指令的最后加上oclint 选项。
oclint-json-compilation-database 指令有过滤文件选项 -i :包含进某些文件 -e :过滤掉某些文件
eg:
# 排除Pods 里的文件
oclint-json-compilation-database -e Pods
# 包含进Pods里的文件
oclint-json-compilation-database -i Pods
oclint-json-compilation-database支持oclint的所有参数
OCLint选项:
-R = <directory> - 将目录添加到规则加载路径
-allow-duplicated-violations - 在OCLint报告中允许重复的违例
-disable-rule = <rule name> - 禁用规则
-enable-clang-static-analyzer - 启用Clang静态分析器,并将结果集成到OCLint报告中
-enable-global-analysis - 编译每个源,并跨全局上下文进行分析(取决于源文件的数量,可能导致高内存负载)
-extra-arg = <string> - 附加到编译器命令行的附加参数
-extra-arg-before = <string> - 添加到编译器命令行的附加参数
-list-enabled-rules - 列出已启用的规则
-max-priority-1 = <threshold> - 允许的最大优先级1违例数
-max-priority-2 = <threshold> - 允许的最大优先级2违例数
-max-priority-3 = <threshold> - 允许的最大优先级3违例数
-no-analytics - 禁用匿名分析
-o = <path> - 将输出写入<path>
-p = <string> - 构建路径
-rc = <parameter> = <value> - 覆盖规则的默认行为
-report-type = <name> - 更改输出报告类型html xcode pmd json text xml
-rule = <rule name> - 显式选择规则
-p <build-path>用于读取编译命令数据库。
-max-priority-1, -max-priority-2, -max-priority-3如若不设置,检索到有不匹配的规则会直接报错
oclint-xcodebuild 指令
因为oclint-xcodebuild 指令已经不再维护,实际应用中用xcpretty代替,所以这里就不进行介绍。
其他命令工具:xcodebuild 和 xcpretty
xcodebuild
xcodebuild是苹果发布自动构建的工具,可以通过命令行脚本的方式编译、打包Xcode 工程。 可以在终端输入man xcodebuild来查询相关用法
xcpretty
用xcpretty生成OClint 解析用的json 文件 这里用xcpretty是因为oclint-xcodebuild不再维护了,在XCode 8之后采用xcpretty来生成。
用XCode检测代码
- 首先在targets点添加按钮:
- 在other选项中,选“Aggregate”,然后命名为"OCLint":
- 接着选中OCLint Target,进入Build Phases选项,点左上角的加号,选择“New Run Script Phase”
- 然后输入脚本即可:
示例脚本:
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
cd ~/Desktop/xmopenPlatform/
xcodebuild -scheme sdkTest -workspace XMOpenPlatform.xcworkspace clean
xcodebuild -scheme sdkTest -workspace XMOpenPlatform.xcworkspace -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json
oclint-json-compilation-database -e Pods -- -report-type xcode
oclint-json-compilation-database -e Pods -- -report-type html xcode/html xcode集成/输出html报告
接着选择OCLint Scheme,Command+B就可以执行了 在执行前最好删除drivedata里面的数据缓存,以保证不会解析到旧的编译内容。 编译成功后Xcode上会在不符合规则的代码上显示Warning:
因为OC代码的方法命可能会有很长,我们可以补充覆盖默认规则
-rc LONG_LINE=200
| 名称 | 描述 | 默认阈值 |
|---|---|---|
| CYCLOMATIC_COMPLEXITY | 方法的循环复杂性(圈负责度) | 10 |
| LONG_CLASS | C类或Objective-C接口,类别,协议和实现的行数 | 1000 |
| LONG_LINE | 一行代码的字符数 | 100 |
| LONG_METHOD | 方法或函数的行数 | 50 |
| LONG_VARIABLE_NAME | 变量名称的字符数 | 20 |
| MAXIMUM_IF_LENGTH | if希块的行数 | 15 |
| MINIMUM_CASES_IN_SWITCH | switch语句中的case数 | 3 |
| NPATH_COMPLEXITY | 方法的NPath复杂性 | 200 |
| NCSS_METHOD | 一个没有注释的方法语句数 | 30 |
| NESTED_BLOCK_DEPTH | 块或复合语句的深度(嵌套深度) | 5 |
| SHORT_VARIABLE_NAME | 变量名称的字符数 | 3 |
| TOO_MANY_FIELDS | 类的字段数 | 20 |
| TOO_MANY_METHODS | 类的方法数 | 30 |
| TOO_MANY_PARAMETERS | 方法的参数数 | 10 |
还有很多默认规则可以去覆盖详情可以查看此文章
自定义规则
安装ninja
git clone https://github.com/ninja-build/ninja.git && cd ninja
python3 configure.py --bootstrap
cp ninja /usr/local/bin/
在日常的开发中,oclint提供我们的规则可能不够用,这时候就需要我们自定义规则去分析,这时候我们就需要去编译一套oclint源码
下载源码时需要注意与homebrew下载的oclint版本需要统一,不然编写的规则跑不起来...
cd oclint源码路径/oclint-scripts
./make
如果没有报错的话,大约30分钟后编译完成,大概过程是下载LLVM、clang的源代码,编译LLVM、clang与OCLint的默认规则。 等待一段时间看build文件中是否有编译好的文件,然后执行脚本创建规则,脚本位于oclint-scripts/scaffoldRule
oclint-scripts/scaffoldRule 名称 -t 类型 -c custom
类型分为
- Generic
暂未使用过
- ASTVisitor(AbstractASTVisitorRule)
AST 访问者规则,该抽象类提供了一系列节点被访问的接口,只需要重载某些方法,即可处理相应节点内的校验逻辑
- ASTMatcher(AbstractASTMatcherRule)
基于匹配模式,你需要构造一些匹配器并加载。只要找到匹配项,callback 就以该 AST节点作为参数调用method,你就可以在callback中收集违例信息。
- SourceCodeReader(AbstractSourceCodeReaderRule)
提供了一种eachLine方法。我们可以获取每行的文本和当前行号。然后,我们可以处理文本。例如,我们可以计算文本的长度,可以理解它是否为注释,可以确定是否存在空格和制表符的混合使用,等等。
在oclint根项目下创建文件夹随意命名,cd进文件夹,
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
创建规则——scaffoldRule 脚本
这是由 oclint 提供的一个脚手架。相关介绍如下使用脚手架创建规则 可以使用该脚本可以方便的创建自定义规则。
编写规则
通过阅读 oclint 的官方文档,以及阅读 Clang AST 的介绍。现在我们已经知道了,oclint 的大致工作方式。首先通过调用 Clang 的 api 把源文件一个个的生成对应的 AST;其次遍历 AST 中的每个节点,并根据相应的规则将违例情况写入违例结果集;最后根据配置的报告类型,将违例结果输出成指定的报告格式。
按照上文,我们现在已经得到了一个 xcodeproj 工程。现在可以打开我们创建的规则的 cpp 源文件。 首先我们可以看到,使用脚手架生成的规则,模板代码有近 2000 行,是不是有点慌? 不用担心。这些模板里,大多都是 Visit 开头的方法,这是 oclint 提供给我们的回调方法, 也就是说在访问到 AST 上相应的节点时就会触发的方法。
./scaffoldRule AllSwitchStatements -c controversial -t ASTMatcher
The scaffold script will create AllSwitchStatementsRule.cpp file, and store it in controversial folder under oclint-rules/rules. The rule will be generated like the following:
会生成AllSwitchStatementsRule.cpp CMakeLists.txt两个文件
编译自定义的规则成动态库
编译方法1:重新执行oclint-scripts/make
cd oclint源码路径/oclint-scripts
./make
这个太慢了,不推荐,很不方便。
编译方法2:将规则相关的内容整合成一个Xcode工程
将规则相关的内容整合成一个Xcode工程,并且我们的每个规则都是一个scheme,编译时可以只选择编译那个选择的规则生成对应的dylib。 OCLint工程使用CMakeLists的方式维护各个文件的依赖关系,可以使用CMake自带的功能将这些CMakeLists生成一个xcodeproj工程文件。
在OCLint源码目录下建立一个文件夹oclint-xcodeproject
mkdir clint-xcodeproject
#! /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
执行命令
bash xcode-debug.sh
编译出来的动态库放入到安装oclint的规则库路径里面
which oclint
/usr/local/bin/oclint
cd /usr/local/lib/oclint/rules
将生成的dylib文件放入/usr/local/lib/oclint/rules即可
后来发现oclint目录下还有好几个文件夹,oclint-core,oclint-driver,oclint-metrics,oclint-reporters,countly等,是都可以生成xcode工程的。然后,按照步骤,一个个生成了xcode工程,然后进行编译。发现oclint-driver这个工程是可以直接跑起oclint的,建议使用oclint-driver进行调试,使用oclint-rules进行调试的话每次改动都需要重新编译然后查看结果,特别不方便。
$: mkdir clint-xcodedriver
$: cd clint-xcodedriver
$: 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-driver
编写规则
写一个例子
#import <Foundation/Foundation.h>
@interface DDDebugObject : NSObject
@property (nonatomic, copy) NSString *resultStr; //这是需要检测的变量名
@end
@implementation DDDebugObject
@end
使用clang dump查看例子代码的AST
clang -Xclang -ast-dump -fsyntax-only test.m
...
|
|-ObjCInterfaceDecl DDDebugObject //interface声明
| |-super ObjCInterface 'NSObject' //继承NSObject
| |-ObjCImplementation 'DDDebugObject'
| |
| |-ObjCPropertyDecl titleStr 'NSString *' readwrite copy nonatomic //property声明
| |
| |-ObjCMethodDecl implicit - titleStr 'NSString *' //titleStr的getter方法
| `-ObjCMethodDecl implicit - setTitleStr: 'void' //titleStr的setter方法
| `-ParmVarDecl titleStr 'NSString *' //setter方法入参
|
|
`-ObjCImplementationDecl DDDebugObject //implement声明
|-ObjCInterface 'DDDebugObject'
|
|-ObjCIvarDecl implicit _titleStr 'NSString *' synthesize private //自动合成ivar
|
|-ObjCPropertyImplDecl <invalid sloc> titleStr synthesize //property实现
| |-ObjCProperty 'resultStr'
| `-ObjCIvar '_resultStr' 'NSString *'
|
|-ObjCMethodDecl implicit - titleStr 'NSString *' //getter方法实现
`-ObjCMethodDecl implicit - setTitleStr: 'void' //setter方法实现
`-ParmVarDecl titleStr 'NSString *' //setter方法入参
找到需要校验的节点 能取到property名字的节点都好几个,这里随便选一个:
|-ObjCPropertyImplDecl <invalid sloc> titleStr synthesize //titleStr对应的ivar
| |-ObjCProperty 'titleStr'
| `-ObjCIvar '_titleStr' 'NSString *'
需要关注的节点是:
ObjCPropertyImplDecl
只要取出这个节点里面的属性名,就可以用来检测了
写代码
回到我们创建好的AllSwitchStatementsRule.cpp文件,可以发现有一千多行的回调方法。
认真观察下,可以发现有很多Objc相关的方法。
1. 使用关键字在规则模版中找到对应的调用方法 这里我们直接在文件搜索关键字:ObjCPropertyImplDecl, 找到了这个回调:
/* Visit ObjCPropertyImplDecl
bool VisitObjCPropertyImplDecl(ObjCPropertyImplDecl *node)
{
return true;
}
*/
打开这个回调的注释,我们在这里写检测代码。
2. 获取节点名等相关信息
查询oclint和clang文档之后,可以找到获取属性名的方法:
//获取节点名等相关信息
ObjCPropertyDecl *decl = node->getPropertyDecl();
string name = decl->getNameAsString();
3. 根据规则检查
为了实现检测规则,简单定义一下缩写字数组, 然后判断是否包含这些关键字
vector<string> abbreviates {
"btn",
"str",
"lb",
"txt",
"img",
"vc"
};
4. 如果违规就加入violationSet
完整代码如下:
/* Visit ObjCPropertyImplDecl
*/
bool VisitObjCPropertyImplDecl(ObjCPropertyImplDecl *node)
{
//获取节点名等相关信息
ObjCPropertyDecl *decl = node->getPropertyDecl();
//titleStr
string name = decl->getNameAsString();
//根据规则检查是否合规
vector<string> abbreviates {
"btn",
"str",
"lb",
"txt",
"img",
"vc"
};
transform(name.begin(),name.end(),name.begin(),::tolower);
for (auto i = abbreviates.begin(); i != abbreviates.end(); i++) {
string eachAbbreviate = *i;
//如果违规就加入violationSet
if (name.find(eachAbbreviate) != string::npos) {
string rawName = decl->getNameAsString();
addViolation(decl, this, "Property "+rawName+" using illegal abbreviate:" +
eachAbbreviate);
}
}
return true;
}
调试规则
如何使用oclint-rules进行调试
Xcode 工程创建好之后,我们需要对指定的 Scheme 添加启动参数。并且在 Scheme 的 Info 一栏选择 Executable ,选择上文中编译完成的 oclint 可执行文件。
将生成的oclint增加可执行权限
cd /Users/xmly/Desktop/study/OClint/oclint/build/oclint-release/bin
cp oclint-0.15 oclintexe
chmoe -R 777 oclintexe
如图:
因为咱们须要调试自定义规则,就须要在刚能跑起来的oclint中调起规则,oclint有个参数-R,表示rule所在文件夹路径。经过Arguments Passed On Lanunch的方式,能够添加参数。 启动参数如下: (第一个参数是规则加载路径,第二个是测试规则用文件)
-R=/Users/xmly/Desktop/study/OClint/oclint/clint-xcodeproject/rules.dl/Debug /Users/xmly/Desktop/study/OClint/oclint/clint-xcodeproject/test.m -- -x objective-c -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
准备完成后即可运行规则,在控制台中可以输出你的规则运行的结果以及调试信息。
如果使用oclint-driver进行调试
- 新建workspace
- 将oclint-driver生成的OCLINT_DRIVER.xcodeproj文件拖进workspace
- 将oclint-rules生成的OCLINT_RULES.xcodeproj文件拖进OCLINT_DRIVER.xcodeproj工程作为OCLINT_DRIVER的子项目, 然后在OCLINT_DRIVER的依赖中添加OCLINT_RULES中的AllSwitchStatementsRule这样修改自定义插件后,只需要编译OCLINT_DRIVER工程就可以进行断点调试。
经过Arguments Passed On Lanunch的方式,能够添加参数。 启动参数如下: (第一个参数是规则加载路径,第二个是测试规则用文件)
-R=/Users/xmly/Desktop/study/OClint/oclint/clint-xcodeproject/rules.dl/Debug /Users/xmly/Desktop/study/OClint/oclint/clint-xcodeproject/test.m -- -x objective-c -isystem /Users/xmly/Desktop/study/OClint/oclint/build/oclint-release/lib/clang/11.0.0/include -iframework /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks -isystem /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
设置运行后,终端打印错误
oclint: error: cannot find dynamic library for report type: text
Program ended with exit code: 2
搜索错误 cannot find dynamic library 断点查看 reportDirPath 值为
/Users/xmly/Desktop/study/OClint/oclint/clint-xcodedriver/bin/Debug/../lib/oclint/reporters
因为我们改了bin的路径,它根据 bin路径/../lib/oclint/reporters 来查找 reporters ,这里直接将 oclint-release下的 lib 目录拷贝到bin路径
按照上述步骤进行,在OCLINT_RULES.xcodeproj工程中的AllSwitchStatementsRule.cpp添加断点既可以进行调试。
使用规则
使用 Xcode 编写的规则完成编译后,可以在 Xcode 的 Products group 中找到相应的 dylib 文件。
cp libAllSwitchStatementsRule.dylib /usr/local/lib/oclint/rules/lib
通过将新规则拖放到规则加载路径中,可以立即使用它们。 因此,只需要将我们自定义规则生成的 dylib 放入默认的规则加载目录即可。当然这里的规则目录也是可以配置的。一个项目可以使用多个规则搜索路径,可以为不同的项目指定不同的规则加载路径。
编译生成dylib动态库规则文件
调试完规则之后,使用当前的xcode工程可以直接生成规则动态库,但文件会很大,需要做如下修改:
-
scheme选中规则名IllegalPropertyAbbreviations
-
点击scheme名,选择Edit scheme
-
修改Build Configuration为MiniSizeRel,点击Close
-
编译一次
-
在Xcode打开Product文件夹,找到 libIllegalPropertyAbbreviationsRule.dylib
(目录:oclint-ruleproject/rules.dl/MinSizeRel)
- 把它放到生产版本的OCLint目录/usr/local/lib/oclint/rules/lib下即可生效