## 前置工作
Oclint(静态代码分析工具)
github.com/oclint/ocli...
安装:
git clone https://github.com/oclint/oclint
Xcpretty(格式化输出工具)
安装:
sudo gem install - n / usr / local / bin xcpretty
Cmake(编译工具, 这里用来构建 LLVM 和 Xcode 工程)
cmake.org/download/
安装: 运行 cmake 图形界面程序,在左上角的选项栏中选择 Tools,点击
How to install for Command Line Use
, 官方给出了三种安装
cmake command line tool
的方法,即终端能够识别 cmake 命令的方法.我选择了官方给出的第二种方法,即复制
sudo "/Applications/CMake.app/Contents/bin/cmake-gui"--install
命令到终端,然后运行
Ninja(比 cmake 更小的编译工具, 这里用来构建 LLVM)
github.com/ninja-build...
安装: 将下好的 Ninja 解压, 拷贝到一个系统目录中 /usr/bin 来完成安装 (此处有坑, 该文件夹权限获取: 重启 Mac,按住 command+R, 进入 recovery 模式.选择打开 Utilities 下的终端,输入:csrutil disable 并回车,然后正常重启 Mac 即可).
## 前言 Clang 是 llvm 的编译器前端, 非常适合进行源码分析. 目前开源的 oclint 就是基于 clang 进行的代码静态检查. 工作中遇到了一些问题需要进行代码分析, 所以学习了一下相关知识. 我们日常用的 clang 有这两种: 1.Xcode 内部自带的 Clang Static Analyzer(简称 CSA) 2.OCLint 这两者都是基于 Clang 的前端编译,CSA 由于被内置,所以使用起来比较方便,但是可用的检查规则比较少,只有 16 条,大部分是核心向的功能例如空指针检测,类型转换检测,空判断检测,内存泄漏检测,无法检测代码风格,可扩展性比较差. OCLint 可用的检查规则有 70 + 条,比 OSA 支持的规则多很多, 还支持自定义规则, 所以选择了 OCLint.
## 准备开发环境
安装 Oclint, 得到的目录结构如下:
.
├── README.md
├── oclint-core
├── oclint-driver
├── oclint-metrics
├── oclint-reporters
├── oclint-rules
└── oclint-scripts
cd 进入 oclint-scripts 文件加,执行./make.大约 30 分钟后编译完成,大概过程是下载 LLVM,clang 的源代码,编译 LLVM,clang 与 OCLint 的默认规则.
编译成功后就可以写规则并且编译执行了.为了方便,OCLint 提供了一个叫 scaffoldRule 的脚本程序,它在 oclint-rules 目录下.我们通过他传入要生成的规则名,级别,类型,脚本就会在目录
oclint - rules / rules / custom /
自动帮我们生成一个模板代码, 并且加入编译路径中.举个例子:
# 生成一个名为BGTestRule类型是ASTVisitor的规则模板
python scaffoldRule BGTestRule -t ASTVisitor
生成两个文件:
# CMakeLists.txt 是对规则BGTestRule的编译描述,由make程序在编译时使用.
├── custom
│ ├── CMakeLists.txt
│ └── BGTestRule.cpp
接着就可以对新添加的内容进行编译了,不过相比于重新执行./make 来说有一个更加优雅的办法,就是将规则相关的内容整合成一个 Xcode 工程,并且我们的每个规则都是一个 scheme,编译时可以只选择编译那个选择的规则生成对应的 dylib.很简单,OCLint 工程使用 CMakeLists 的方式维护各个文件的依赖关系,我们可以使用 CMake 自带的功能将这些 CMakeLists 生成一个 xcodeproj 工程文件.
# 在OCLint源码目录下建立一个文件夹,我这里命名为oclint-xcoderules
mkdir oclint-xcoderules
cd oclint-xcoderules
# 创建一个脚本(代码如下段),并执行它(我写的方便修改参数,其实里面就一句命令,直接执行也行)(PS:刚创建的文件是没有执行权限的,不要忘了chmod)
./create-xcode-rules.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
执行
sh. / create - xcode - rules.sh
于是我们就得到了 Xcode 工程:
├── CMakeCache.txt
├── CMakeFiles
├── CMakeScripts
├── OCLINT_RULES.build
├── OCLINT_RULES.xcodeproj
├── cmake_install.cmake
├── create-xcode-rules.sh
├── lib
├── rules
└── rules.dl
打开
OCLINT_RULES.xcodeproj
:
选择自己的规则,编译,成功后就可以在 Products 下看到生成的 dylib 了.
Jenkins/Xcode 中写入 oclint 的命令:
myworkspace=XXXXXX.xcworkspace # 替换workspace的名字
myscheme=XXXXX # 替换scheme的名字
xcodebuild -workspace $myworkspace -scheme $myscheme clean&&
xcodebuild -workspace $myworkspace -scheme $myscheme \
-configuration Debug \
COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json&&
oclint-json-compilation-database -e Pods -- \
-report-type pmd -o pmd.xml \
-R 这里是你编译生成的dylib目录 (ps :/oclint/oclint-xcoderules/rules.dl/Debug) \
-max-priority-1=9999 \
-max-priority-2=9999 \
-max-priority-3=9999; \
rm compile_commands.json;
if [ -f ./pmd.xml ]; then echo '-----分析完毕-----'
else echo "-----分析失败-----"; fi
## 如何自定义规则 通过查看官方文档, 我们发现 OCLint 的原理是调用 clang 的 API 把一个个源文件生成一个一个 AST 语法树,然后遍历树中的每个节点传入各个规则的整个过程, 在 Xcode8 之前, Xcode 各种代码分析插件也是这样的原理操作的, 详情看参考文档 4,5.
### 分析默认规则 下面的例子是默认规则中较为简单的一个: 判断行长度是否超过了限制长度
class LongLineRule : public AbstractSourceCodeReaderRule
{
public:
//重载方法 输出规则名称
virtual const string name() const override
{
return "long line";
}
//重载方法 输出规则优先级
virtual int priority() const override
{
return 3;
}
//重载方法 输出规则分类
virtual const string category() const override
{
return "size";
}
#ifdef DOCGEN
//重载方法 输出规则使用版本
virtual const std::string since() const override
{
return "0.6";
}
//重载方法 输出规则描述
virtual const std::string description() const override
{
return "当某行代码长度过长, "
"它很大程度上损害了可读性.建议将长行代码分割成多行.";
}
//重载方法 输出规则示例
virtual const std::string example() const override
{
return R"rst(
.. code-block:: cpp
void example()
{
int a012345678901234567890123456789...1234567890123456789012345678901234567890123456789;
}
)rst";
}
//重载方法 输出规则可配置参数
virtual const std::map<std::string, std::string> thresholds() const override
{
std::map<std::string, std::string> thresholdMapping;
thresholdMapping["LONG_LINE"] = "每行代码长度限制, 默认长度 200 .";
return thresholdMapping;
}
#endif
//核心回调 这个回调返回源文件中的每一行 规则方法就写这里
virtual void eachLine(int lineNumber, string line) override
{
int threshold = RuleConfiguration::intForKey("LONG_LINE", 200);
int currentLineSize = line.size();
if (currentLineSize > threshold)
{
string description = "该行长度: " + toString<int>(currentLineSize) +
" 字符,默认规则限制: " + toString<int>(threshold);
//返回闭包 此时产生一个警告
addViolation(lineNumber, 1, lineNumber, currentLineSize, this, description);
}
}
};
### 简单的自定义规则: 到这里我们看新创建的规则模板就会发现, 模板中这些以 Visit 开头的百十个函数都是 OCLint 提供给开发者的回调函数. 只要在这写回调方法中写规则的核心代码就 OK 啦. 示例:
//重载方法 源文件中所有的变量都会到这里 传入的参数是AST树节点的类型
bool VisitVarDecl(VarDecl *declaration)
{
checkVarName(declaration);
return true;
}
//检测变量是否合法
void checkVarName(VarDecl *decl)
{
StringRef className = decl -> getName();
//必须以小写字母开头
char c = className[0];
if (isUppercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
string description = tempName + "首字母应该为小写";
//抛出警告
addViolation(decl, this, description);
}
}
分析 AST 语法树
如无必要, 勿增实体, 分析 AST 的文章好多, 详情看参考文档 2,6.
### 难点 规则代码需用 C++ 来写, 复杂规则需要查文档 ->dump 看 AST 语法树 -> 生成 dylib-> 复制 dylib 到 OCLint 规则目录下 -> 执行检测 -> 查看报告 -> 发现问题 -> 修改代码重新生成 dylib, 暂时还未找到解决方法.
## 总结 如上所述,基于 Clang 的 Oclint 用于风格检查,可以发现和修改的更多是一种格式上的约定和某些明显的不容许或无效逻辑,虽可解决不少问题,但是也有其局限性.实际工作中,一方面可限制使用某些固定的风格,更重要的是保持团队风格的统一和规范,提高其可读性.
## 参考文档
Oclint 官方文档
AST 语法树解释
深入剖析 iOS 编译 Clang / LLVM
手把手教你开发 clang 插件
使用 Xcode 开发 iOS 语法检查的 Clang 插件
OCLint 如何自定义规则
来源: https://juejin.im/post/5a5f13436fb9a01c927eb9b3