OClint学习笔记

2,206 阅读6分钟

OClint介绍

7.webp

是一个静态代码分析工具, 通过分析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 进行分析,最后输出违反规则的代码信息,并且导出指定格式的报告。

5.jpg

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点添加按钮:

1.jpg

  • 在other选项中,选“Aggregate”,然后命名为"OCLint":

2.jpg

  • 接着选中OCLint Target,进入Build Phases选项,点左上角的加号,选择“New Run Script Phase”

3.jpg

  • 然后输入脚本即可:

4.jpg

示例脚本:

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_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

还有很多默认规则可以去覆盖详情可以查看此文章

自定义规则

安装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

如图:

6.png

因为咱们须要调试自定义规则,就须要在刚能跑起来的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下即可生效