OClint学习笔记

1,253 阅读8分钟

oclint

是一个静态代码分析工具, 通过分析C,C++, Objective-C代码,找出可能出现问题的代码,冗余代码,并可以自己定义规则去分析检索

简单应用

oclint可以自己下载源码编译,也可使用Homebrew安装brew install oclint,截止笔者文章时Homebrew中oclint版本为20.11

Xcode项目选择新建target -> Other -> Aggregate

image.png

选中新建的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

其中xcodebuildclean 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下,就会看到警告信息

image.png

因为OC代码的方法命可能会有很长,我们可以补充覆盖默认规则

 -rc LONG_LINE=200
名称描述默认阈值
CYCLOMATIC_COMPLEXITY方法的循环复杂性(圈负责度)10
LONG_CLASSC类或Objective-C接口,类别,协议和实现的行数1000
LONG_LINE一行代码的字符数100
LONG_METHOD方法或函数的行数50
LONG_VARIABLE_NAME变量名称的字符数20
MAXIMUM_IF_LENGTHif希块的行数15
MINIMUM_CASES_IN_SWITCHswitch语句中的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