oclint
是一个静态代码分析工具, 通过分析C,C++, Objective-C代码,找出可能出现问题的代码,冗余代码,并可以自己定义规则去分析检索
简单应用
oclint可以自己下载源码编译,也可使用Homebrew安装brew install oclint,截止笔者文章时Homebrew中oclint版本为20.11
Xcode项目选择新建
target->Other->Aggregate
选中新建的target ->
Build Phases-> 新建Script脚本New Run Script Phases添加脚本
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
cd ${SRCROOT}
xcodebuild -workspace ${PROJECT_NAME}.xcworkspace -scheme ${PROJECT_NAME} -configuration DEBUG -UseModernBuildSystem=NO clean build | xcpretty -r json-compilation-database -o compile_commands.json
oclint-json-compilation-database -e Pods -v -- -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999 -report-type xcode
其中
xcodebuild的clean build得放一行,不然oclint-json-compilation-database的命令会报错(官方demo是没放在一行),类似
xcodebuild clean
xcodebuild build
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如若不设置,检索到有不匹配的规则会直接报错
选中刚创建的target,
build下,就会看到警告信息
因为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 |
还有很多默认规则可以去覆盖详情可以查看此文章
自定义规则
在日常的开发中,oclint提供我们的规则可能不够用,这时候就需要我们自定义规则去分析,这时候我们就需要去编译一套oclint源码
下载源码时需要注意与homebrew下载的
oclint版本需要统一,不然编写的规则跑不起来...
cd oclint源码路径/oclint-scripts
./make
等待一段时间看build文件中是否有编译好的文件,然后执行脚本创建规则,脚本位于oclint-scripts/scaffoldRule
oclint-scripts/scaffoldRule 名称 -t 类型 -c custom
类型分为
- Generic
暂未使用过
- ASTVisitor
AST 访问者规则,该抽象类提供了一系列节点被访问的接口,只需要重载某些方法,即可处理相应节点内的校验逻辑
- ASTMatcher
基于匹配模式,你需要构造一些匹配器并加载。只要找到匹配项,callback 就以该 AST节点作为参数调用method,你就可以在callback中收集违例信息。
- SourceCodeReader
提供了一种
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
可以将脚本放入
shell文件中
AST
他是抽象语法树 (Abstract Syntax Tree)的简称,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
在OC中,可以使用Clang命令将我们的代码抽象成语法树
clang -Xclang -ast-dump -fsyntax-only 源文件地址
举个例子
#import <Foundation/Foundation.h>
@implementation Test
- (void)test{
NSURLString(@"sssddd");
NSString * bshd = @"玩的";
NSURLString(bshd);
}
@end
执行命令后
-ObjCInterfaceDecl 0x7f825c8b0e70 <./test.m:3:1, <invalid sloc>> col:17 implicit Test
| `-ObjCImplementation 0x7f825c8b0f78 'Test'
`-ObjCImplementationDecl 0x7f825c8b0f78 <col:1, line:9:1> line:3:17 Test
|-ObjCInterface 0x7f825c8b0e70 'Test'
`-ObjCMethodDecl 0x7f825c8b1010 <line:4:1, line:8:1> line:4:1 - test 'void'
|-ImplicitParamDecl 0x7f825c8b1198 <<invalid sloc>> <invalid sloc> implicit self 'Test *'
|-ImplicitParamDecl 0x7f825c8b1200 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
|-FunctionDecl 0x7f825c8b1290 parent 0x7f825783c008 <<invalid sloc>> line:5:5 implicit used NSURLString 'int ()'
|-VarDecl 0x7f825c8b13f8 <line:6:5, col:24> col:16 used bshd 'NSString *' cinit
| `-ObjCStringLiteral 0x7f825c8b1480 <col:23, col:24> 'NSString *'
| `-StringLiteral 0x7f825c8b1460 <col:24> 'char [7]' lvalue "\347\216\251\347\232\204"
`-CompoundStmt 0x7f825c8b1550 <line:4:13, line:8:1>
|-CallExpr 0x7f825c8b13b8 <line:5:5, col:26> 'int'
| |-ImplicitCastExpr 0x7f825c8b13a0 <col:5> 'int (*)()' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f825c8b1340 <col:5> 'int ()' Function 0x7f825c8b1290 'NSURLString' 'int ()'
| `-ObjCStringLiteral 0x7f825c8b1380 <col:17, col:18> 'NSString *'
| `-StringLiteral 0x7f825c8b1360 <col:18> 'char [7]' lvalue "sssddd"
|-DeclStmt 0x7f825c8b14a0 <line:6:5, col:32>
| `-VarDecl 0x7f825c8b13f8 <col:5, col:24> col:16 used bshd 'NSString *' cinit
| `-ObjCStringLiteral 0x7f825c8b1480 <col:23, col:24> 'NSString *'
| `-StringLiteral 0x7f825c8b1460 <col:24> 'char [7]' lvalue "\347\216\251\347\232\204"
`-CallExpr 0x7f825c8b1510 <line:7:5, col:21> 'int'
|-ImplicitCastExpr 0x7f825c8b14f8 <col:5> 'int (*)()' <FunctionToPointerDecay>
| `-DeclRefExpr 0x7f825c8b14b8 <col:5> 'int ()' Function 0x7f825c8b1290 'NSURLString' 'int ()'
`-ImplicitCastExpr 0x7f825c8b1538 <col:17> 'NSString *' <LValueToRValue>
`-DeclRefExpr 0x7f825c8b14d8 <col:17> 'NSString *' lvalue Var 0x7f825c8b13f8 'bshd' 'NSString *'
生成如上的结构,前面的ImplicitCastExpr,DeclStmt等等是我们需要关注的节点
AST文档, 参考资料
回头以一个简单的例子演示下生成规则的那四种(三种)区别
判断方法名首字母不能是大写
使用上诉脚本生成对应工程
Generic
暂且略过
ASTVisitor
以上诉代码为素材,我们可以找到ObjCMethodDecl节点,我们在模板工程里面可以找到bool VisitObjCMethodDecl(ObjCMethodDecl *node)函数,由此我们可以编写对应逻辑
bool VisitObjCMethodDecl(ObjCMethodDecl *node){
Selector sel = node -> getSelector();
string selectorName = sel.getAsString();
char c = selectorName[0];
if (isUppercase(c)) {
// 提示
// 获取将要报错的位置
SourceLocation loc = node->getSelectorLoc(i);
string message = "方法名/方法参数: \'" + selectorName + "\' 不能以大写开头";
addViolation(loc, loc, this, message);
}
return true;
}
ASTMatcher
上述描述已经说过,这个是以callback的形式来处理的,直接贴代码
virtual void callback(const MatchFinder::MatchResult &result) override
{
const ObjCMethodDecl * decl = result.Nodes.getNodeAs<ObjCMethodDecl>("objcMethod_id");
char c = decl -> getNameAsString().c_str()[0];
if (isUppercase(c)) {
string message = "ASTMatcher:方法名 \'" + decl -> getNameAsString() + "\' 不能以大写开头";
addViolation(decl, this, message);
}
}
virtual void setUpMatcher() override
{
addMatcher(objcMethodDecl().bind("objcMethod_id"));
}
SourceCodeReader
这个就是遍历每一行,这个可以使用正则去匹配下
virtual void eachLine(int lineNumber, string line) override
{
regex rgx("^(\\+|-)( )*\\(.*\\)[\\s\\S]*");
smatch match;
string::const_iterator iterStart = line.begin();
string::const_iterator iterEnd = line.end();
if (regex_search(line, match, rgx)) {
regex test("\\).+\\{");
regex_search(line, match, test);
char u = match.str().c_str()[1];
if ('A'<= u && u<='Z') {
string message = "\nSourceCode:方法名 \'" + line + "\' 不能以大写开头";
addViolation(lineNumber, 1, lineNumber, 1, this, message);
}
}
}
笔者C++能力等于0,这丑陋的代码就请凑合看下
调试
写好规则后肯定是需要调试的,总不能每次写好都输出一个,然后去项目里测试,所以我们这时候可以配置下
- 选中当前编写规则的
target->Edit Scheme->Info->Executable选择oclint
oclint如果是想使用homebrew下载的请使用
which oclint
# 输出 /usr/local/bin/oclint
ls -al /usr/local/bin/oclint
#输出 lrwxr-xr-x 1 mac admin 33 4 29 19:33 /usr/local/bin/oclint -> ../Cellar/oclint/20.11/bin/oclint
若使用编译源码的则路径在
源码地址/build/oclint-release/bin/下,将其中的oclint-版本号改名为oclintExe(名字随便取主要是让其变为可执行文件)即可
- 选中当前编写规则的
target->Edit Scheme->Arguments添加参数
-R=当前规则项目路径/rules.dl/Debug 分析的文件路径 -- -x objective-c -isystem oclint源码路径/build/oclint-release/lib/clang/12.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
选中你的target直接run就可以了
应用到自己的项目
在最前面的项目中可以将命令加 -R参数
oclint-json-compilation-database -e Pods -v -- -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999 -report-type xcode