基础介绍部分参考:一文说清 OCLint 源码解析及工作流分析
不得不提的 Clang
由于 OCLint 是一个基于 Clang tool 的静态代码分析工具,所以不得不提一下 Clang。 Clang 作为 LLVM 的子项目, 是一个用来编译 c,c++,以及 oc 的编译器。
OCLint 本身是基于 Clang tool 的,换句话说相当于做了一层封装。 它的核心能力是对 Clang AST 进行分析,最后输出违反规则的代码信息,并且导出指定格式的报告。
接下来就让我们看看作为输入信息的 Clang AST 是什么样子的。
Clang AST
Clang AST 是在编译器编译时的一个中间产物,从词法分析,语法分析(生成 AST),到语义分析,生成中间代码。
抽象语法树示例
这里先对抽象语法树有一个初步的印象。
//Example.c
#include <stdio.h>
int global;
void myPrint(int param) {
if (param == 1)
printf("param is 1");
for (int i = 0 ; i < 10 ; i++ ) {
global += i;
}
}
int main(int argc, char *argv[]) {
int param = 1;
myPrint(param);
return 0;
}
复制代码
这里可以清晰的看到,这一段代码的每一个元素与其子节点的关系。其中的节点有两大类型,一个是 Stmt 类,包括 Expr 表达式类也是继承于 Stmt,它是语句,有一定操作;另一大类元素是 Decl 类,即定义。所有的类,方法,函数变量均是一个 Decl 类 (这两个类互不兼容,需要特殊容器节点来转换,比如 DeclStmt 节点) 。另外从数据结构中可以看到,这个树是单向的,只有从某一个顶层元素向下访问。
在终端中可以用如下指令查看语法树:
clang -Xclang -ast-dump -fsyntax-only Example.c
复制代码
访问抽象语法树
无论是 Stmt 还是 Decl 都自带迭代器,可以方便的遍历所有节点元素,再判断其类型进行操作。不过在 Clang 中还有更方便的方法:继承 RecursiveASTVisitor 类。 它是一个 AST 树递归器,可以递归的访问一个 AST 树的所有节点。最常用的方法是 TraverseStmt 和 TraverseDecl。
例如我要访问这么一段代码中所有的函数,即 FunctionDecl,并且输出这些函数的名字,我就要重写 (通过自定义 checker) 这么一个方法:
bool VisitFunctionDecl(FunctionDecl *decl){
string name = decl->getNameAsString();
printf(name);
return true;
}
复制代码
这样,我们就能够访问到这棵 AST 树中所有的 FunctionDecl 节点,并且把其中函数名字给输出出来了。
OCLint 源码解析
首先看一下核心类关系图,有一点初步的印象后,我们开始看代码 👀
- 首先找到入口文件 oclint/driver/main.cpp,及入口函数 main()
该文件的精简后的代码框架如下所示:
int main(int argc, const char **argv)
{
llvm::cl::SetVersionPrinter(oclintVersionPrinter);
// 构造 parser 分析程序
CommonOptionsParser optionsParser(argc, argv, OCLintOptionCategory);
// 配置
oclint::option::process(argv[0]);
...
// 构造 analyzer
oclint::RulesetBasedAnalyzer analyzer(oclint::option::rulesetFilter().filteredRules());
// 构造 driver
oclint::Driver driver;
// 执行分析
driver.run(optionsParser.getCompilations(), optionsParser.getSourcePathList(), analyzer);
std::unique_ptr<oclint::Results> results(std::move(getResults()));
ostream *out = outStream();
// 输出报告
reporter()->report(results.get(), *out);
disposeOutStream(out);
return handleExit(results.get());
}
复制代码
- 接着查看核心的 Driver 类的关键代码片段,有三个比较核心的方法 constructCompilers(),invoke(),run()
// 构建编译器
static void constructCompilers(std::vector<oclint::CompilerInstance *> &compilers,
CompileCommandPairs &compileCommands,
std::string &mainExecutable)
{
for (auto &compileCommand : compileCommands) // 遍历编译命令集
{
std::vector<std::string> adjustedCmdLine =
adjustArguments(compileCommand.second.CommandLine, compileCommand.first);
#ifndef NDEBUG
printCompileCommandDebugInfo(compileCommand, adjustedCmdLine);
#endif
LOG_VERBOSE("Compiling ");
LOG_VERBOSE(compileCommand.first.c_str());
std::string targetDir = stringReplace(compileCommand.second.Directory, "\ ", " ");
if(chdir(targetDir.c_str()))
{
throw oclint::GenericException("Cannot change dictionary into "" +
targetDir + "", "
"please make sure the directory exists and you have permission to access!");
}
clang::CompilerInvocation *compilerInvocation =
newCompilerInvocation(mainExecutable, adjustedCmdLine);// 创建 CompilerInvocation 对象
oclint::CompilerInstance *compiler = newCompilerInstance(compilerInvocation);
// 使用 clang 的 CompilerInvocation 对象 创建 oclint 的 CompilerInstance 对象,oclint 做了封装
compiler->start(); // clang::FrontendAction 核心是获取到 action 并执行
if (!compiler->getDiagnostics().hasErrorOccurred() && compiler->hasASTContext())
{
LOG_VERBOSE(" - Success");
compilers.push_back(compiler); // oclint 封装的 CompilerInstance 对象放入集合中
}
else
{
LOG_VERBOSE(" - Failed");
}
LOG_VERBOSE_LINE("");
}
}
// 实际的进行分析的唤起方法
static void invoke(CompileCommandPairs &compileCommands,
std::string &mainExecutable, oclint::Analyzer &analyzer)
{
std::vector<oclint::CompilerInstance *> compilers; // 编译器容器
constructCompilers(compilers, compileCommands, mainExecutable); // 构建编译器
// collect a collection of AST contexts
std::vector<clang::ASTContext *> localContexts;
for (auto compiler : compilers) // 遍历编译器集合
{
localContexts.push_back(&compiler->getASTContext()); // 将 AST 上下文放入 上下文集合
}
// use the analyzer to do the actual analysis
analyzer.preprocess(localContexts); // 将上下文集合送入分析器 预处理
analyzer.analyze(localContexts); // 分析
analyzer.postprocess(localContexts); // 发送处理
// send out the signals to release or simply leak resources
for (size_t compilerIndex = 0; compilerIndex != compilers.size(); ++compilerIndex)
{
compilers.at(compilerIndex)->end();
delete compilers.at(compilerIndex);
}
}
// main.cpp 调用的核心方法,执行分析
void Driver::run(const clang::tooling::CompilationDatabase &compilationDatabase,
llvm::ArrayRef<std::string> sourcePaths, oclint::Analyzer &analyzer)
{
CompileCommandPairs compileCommands; // 生成编译指令对容器
constructCompileCommands(compileCommands, compilationDatabase, sourcePaths); // 构造编译指令对
static int staticSymbol; // 静态符号
std::string mainExecutable = llvm::sys::fs::getMainExecutable("oclint", &staticSymbol);// 获取 oclint 可执行程序的路径
if (option::enableGlobalAnalysis()) // 启用全局分析的情况
{
invoke(compileCommands, mainExecutable, analyzer);// 调用 invoke 方法,注意 analyzer 也一并入参
}
else
{ // 非全局分析的情况 逐个 compileCommand 进行分析
for (auto &compileCommand : compileCommands)
{
CompileCommandPairs oneCompileCommand { compileCommand };
invoke(oneCompileCommand, mainExecutable, analyzer);
}
}
if (option::enableClangChecker()) // 启用 clang checker
{
invokeClangStaticAnalyzer(compileCommands, mainExecutable); // 调用 clang 的静态分析器
}
}
复制代码
- 最后一个就是 RulesetBasedAnalyzer 类,这个类的代码量非常少,如下所示
void RulesetBasedAnalyzer::analyze(std::vector<clang::ASTContext *> &contexts)
{
for (const auto& context : contexts)
{
LOG_VERBOSE("Analyzing ");
auto violationSet = new ViolationSet();
auto carrier = new RuleCarrier(context, violationSet); // 规则运载者,context 是传递给规则来分析的数据,violationSet 是用于存放处理好的结果集
LOG_VERBOSE(carrier->getMainFilePath().c_str());
for (RuleBase *rule : _filteredRules) // 遍历已经过滤的规则集合
{
rule->takeoff(carrier); // 调用规则的 takeoff
}
ResultCollector *results = ResultCollector::getInstance(); // 取得结果收集器实例
results->add(violationSet); // 将规则处理好的数据加入收集器
LOG_VERBOSE_LINE(" - Done");
}
}
复制代码
从上面的代码可以看出 analyzer 会遍历规则集合,来调用 rule 的 takeoff 方法。rule 的基类是 RuleBase,这个基类含有一个 RuleCarrier 的示例作为成员,RuleCarrier包含了每个文件对应的 ASTContext 和 violationSet,violationSet 用来存放违例的相关信息。 rule 的职责就是,检查其成员变量 ruleCarrier 的 ASTContext,有违例的情况,就将结果写入 ruleCarrier 的 violationSet 中。
环境准备
- OCLint的github上clone下代码 github.com/oclint/ocli…
cd进入oclint-scripts文件夹,执行./make。大约30分钟后编译完成,大概过程是下载LLVM、clang的源代码。- 编译成功后就可以写规则并且编译执行了。为了方便,OCLint提供了一个叫
scaffoldRule的脚本程序,它在oclint-rules目录下。我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/custom/自动帮我们生成一个模板代码,并且加入编译路径中。
# 生成一个名为HdwTestRule类型是ASTVisitor的规则模板
oclint-scripts/scaffoldRule HdwTestRule -t ASTVisitor
- 生成一个包含所有规则的
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
- 于是我们就得到了
xcode工程 - 选择自己的规则,编译,成功后就可以在
Products下看到生成的dylib
如何编写规则
oclint的原理是在编译阶段将语法树以对象的形式返回给检测rule- 在编写规则前,首先查看文件的语法树
clang -Xclang -ast-dump -fsyntax-only ViewController.m
- 语法树会以
stmt和Decl两种对象嵌套生成。stmt类似一个容器,其中的Decl类似语法描述。 - 在检测
rule内监听对应的stmt或Decl的子类回调,在回调中再对规则进行判断即可。
如何将自己的rule添加到检测中
- 查看OCLint的安装路径
$ which oclint
/usr/local/bin/oclint
- 在OCLint安装路径下找到
oclint/rules文件夹,并将规则替换进去 - 在工程根路径下生成报告,测试新规则是否已被添加
xcodebuild -scheme XXX clean && xcodebuild -scheme XXX -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json