指标读者

一线工程师,架构师

预计浏览工夫

15-20min ????

实现浏览的播种

  1. 理解动态代码审核技术的原理
  2. 理解动态代码审核技术工作流

不得不提的 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 到底是如何工作的!

OCLint 源码解析

首先看一下外围类关系图,有一点初步的印象后,咱们开始看代码 ????

1 首先找到入口文件 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());}

2 接着查看外围的 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 的动态分析器    }}

3 最初一个就是 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 的根本用法,以及工作流程。

接下来更灵便也是有更高的应用难度的局部--自定义规定

规定必须实现 RuleBase 类或其派生的抽象类。不同的规定专一于不同的形象级别,例如,某些规定可能必须十分深刻地钻研代码的控制流,相同,某些规定仅通过读取源代码的字符串来检测缺点。

oclint 提供了三个抽象类,以便咱们来编写自定义规定。
AbstractSourceCodeReaderRule(源代码读取器规定),AbstractASTVisitorRule(AST 访问者规定),以及 AbstractASTMatcherRule(AST 匹配器规定)。

依照官网文档的说法,因为 AST 匹配器规定 具备良好的可读性,除非性能是个大问题,咱们可能大多数时候都会抉择编写AST匹配器规定。

AST 访问者规定是基于访问者模式,你只须要重载某些办法(该抽象类提供了一系列节点被拜访的接口),即可解决相应节点内的校验逻辑。(因为 OCLint 应用的是 Clang 生成的形象语法树,因而理解 Clang AST 的 API 在编写规定时十分有帮忙相干链接)。

AST 匹配器规定是基于匹配模式,你须要结构一些匹配器并加载。只有找到匹配项,callback 就以该 AST 节点作为参数调用 method,你就能够在 callback 中收集违例信息。(对于匹配器的更多信息看这里)

这里简略就说这么多,咱们只须要晓得 oclint 提供了抽象类,用于实现自定义规定。对于如何编写一个规定的局部会在下一节开展。

创立规定——scaffoldRule 脚本

这是由 oclint 提供的一个脚手架。相干介绍如下应用脚手架创立规定
能够应用该脚本能够不便的创立自定义规定。

编写规定

通过浏览 oclint 的官网文档,以及浏览 Clang AST 的介绍。当初咱们曾经晓得了,oclint 的大抵工作形式。首先通过调用 Clang 的 api 把源文件一个个的生成对应的 AST;其次遍历 AST 中的每个节点,并依据相应的规定将违例状况写入违例后果集;最初依据配置的报告类型,将违例后果输入成指定的报告格局。

先上一个 oclint 规定编写思路的脑图,有个初步的印象即可。

依照上文,咱们当初曾经失去了一个 xcodeproj 工程。当初能够关上咱们创立的规定的 cpp 源文件。

首先咱们能够看到,应用脚手架生成的规定,模板代码有近 2000 行,是不是有点慌? 不必放心。这些模板里,大多都是 Visit 结尾的办法,这是 oclint 提供给咱们的回调办法, 也就是说在拜访到 AST 上相应的节点时就会触发的办法。


上面咱们来看一个理论的案例,曾经用在 iOS 组的代码查看中的一个规定。
这个规定所做的工作大抵如下,依照 cocoa 的标准要求来查看 if else 条件分支的格局。
具体的格局要求是这样的,if else 和前面跟着的括号以及花括号要宰割开,能够应用空格和换行符。
示例代码如下:

void example(){    int a = 1;    if(a > 0) { // (左侧无空格或换行不合规        a = 10;    }        if (a > 0){ // )右侧无空格或换行不合规        a = 10;    }        if (a > 0)    {        a = 10;    }else { // }右侧无空格或换行不合规        a = -1;    }        if (a > 0)    {        a = 10;    } else{ // {左侧无空格或换行不合规        a = -1;    }}

1 首先在终端中应用 dump 查看 AST(上文曾经介绍了如何查看 AST,如果没看过倡议先看看)。

屏幕上一连串花花绿绿的字符闪过,最初停在了这里!
没错,这正是咱们须要找的。

能够很分明的看到,最上方的变量申明 VarDecl,以及下方的条件语句 IfStmt。

2 须要测验的节点名称曾经确定,就是 IfStmt。
3 接下来,在曾经生成的规定模板中找对应的回调办法。
我揣测,应该叫做 VisitXXIfStmt 之类的。
果然不出所料,咱们找到了!VisitIfStmt 这个办法,看起来正是咱们所须要的。
4 紧接着,咱们须要获取节点名称和节点形容。(具体的代码能够参看下方提供的残缺规定文件)
5 最初是判断这里的办法名是否合乎规定。(能够应用 llvm,Clang,以及 std 提供的各种函数,如果有你须要的)
6 如果检测进去的办法名是不符合规范的,将节点及形容信息退出 violationSet。

到这里,整体的编写流程曾经实现了。置信你看完下方的实例代码,以及再多读几个官网提供的规定代码之后,很快就能够触类旁通的写出本人的规定了。

这里间接给出上文规定的残缺实现:

#include "oclint/AbstractASTVisitorRule.h"#include "oclint/RuleSet.h"using namespace std;using namespace clang;using namespace oclint;class KirinzerTestRule : public AbstractASTVisitorRule<KirinzerTestRule>{public:    virtual const string name() const override    {        return "if else format";    }    virtual int priority() const override    {        return 2;    }    virtual const string category() const override    {        return "controversial";    }#ifdef DOCGEN    virtual const std::string since() const override    {        return "20.11";    }    virtual const std::string description() const override    {        return "用于查看 if else 条件分支中的括号是否合乎编码标准";    }    virtual const std::string example() const override    {        return R"rst(.. code-block:: cpp        void example()        {        int a = 1;        if(a > 0) { // (左侧无空格或换行不合规        a = 10;        }                if (a > 0){ // )右侧无空格或换行不合规        a = 10;        }                if (a > 0)        {        a = 10;        }else { // }右侧无空格或换行不合规        a = -1;        }                if (a > 0)        {        a = 10;        } else{ // {左侧无空格或换行不合规        a = -1;        }        }        )rst";    }#endif        bool VisitIfStmt(IfStmt *node)    {        clang::SourceManager *sourceManager = &_carrier->getSourceManager();                SourceLocation begin = node->getIfLoc();        SourceLocation elseLoc = node->getElseLoc();        SourceLocation end = node->getEndLoc();                int length = sourceManager->getFileOffset(end) - sourceManager->getFileOffset(begin) + 1; // 计算该节点源码的长度        string sourceCode = StringRef(sourceManager->getCharacterData(begin), length).str(); // 从起始地位按指定长度读取字符数据//        printf("%s\n", sourceCode.c_str());                // 查看 if 左括号        std::size_t found = sourceCode.find("if (");        if (found==std::string::npos) {//            printf("if ( 格局不正确\n");            AppendToViolationSet(node, Description());        }                // 查看 if 右括号        found = sourceCode.find(") {");        if (found==std::string::npos) {            found = sourceCode.find(")\n");            if (found ==std::string::npos) {//                printf("if 右括号 格局不正确\n");                AppendToViolationSet(node, Description());            }        }                // 没有 else 分支就不再进行查看        if (!elseLoc.isValid()) {            return true;        }                // 查看 else 左括号        found = sourceCode.find("} else");        if (found==std::string::npos) {            found = sourceCode.find("}\n");            if (found==std::string::npos) {//                printf("} else 格局不正确\n");                AppendToViolationSet(node, Description());            }        }                // 查看 else 右括号        found = sourceCode.find("else {");        if (found==std::string::npos) {            found = sourceCode.find("else\n");            if (found==std::string::npos) {//                printf("else { 格局不正确\n");                AppendToViolationSet(node, Description());            }        }                return true;    }        // 将违例信息追加进后果集    bool AppendToViolationSet(IfStmt *node, string description) {        addViolation(node, this, description);    }        string Description() {        return "格局不正确";    }};static RuleSet rules(new KirinzerTestRule());

调试规定

依据后面的所学到的内容,咱们晓得了规定的理论体现模式为 dylib 文件。那么如果编写 cpp 的时候没方法调试,那真的是噩梦个别的体验。将咱们当初遇到的问题,如何调试 oclint 规定?

1 首先须要一个 Xcode 工程。

oclint 工程应用 CMakeLists 来保护依赖关系。咱们也可利用 CMake 来将 CMakeLists 生成 xcodeproj。你能够对每个文件夹生成一个 Xcode 工程,在这里咱们对 oclint-rules 生成对应的 Xcode 工程。

// 在OCLint源码目录下建设一个文件夹,我这里命名为oclint-xcoderulesmkdir oclint-xcoderulescd oclint-xcoderules// 执行如下命令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

2 Xcode 工程创立好之后,咱们须要对指定的 Scheme 增加启动参数。并且在 Scheme 的 Info 一栏抉择 Executable ,抉择上文中编译实现的 oclint 可执行文件。

Tip: 编译生成的oclint可执行文件在根目录下 build/oclint-release/bin 目录下,以最新版的 oclint 20.11 为例,生成的文件名为 oclint-20.11,会被 Finder 辨认为 Document 类型。(.11被辨认为了后缀),尽管并不影响在终端的间接调用,然而咱们后续的调试中会须要在 Xcode 中通过 Finder 来选取这个可执行文件,然而因为类型被辨认谬误,会导致无奈点击选中。所以在这里咱们就删除小数点,批改可执行文件名为 oclint-2011 并且没有任何后缀即可。(留神批改的时候,右键getInfo,在文件名和扩展名那一栏来批改,还有留神是否暗藏了拓展名)。

启动参数如下:
(第一个参数是规定加载门路,第二个是测试规定用文件)

>-R=/Users/developer/TempData/oclint/oclint-xcoderules/rules.dl/Debug /Users/developer/TempData/oclint/oclint-xcoderules/test2.m -- -x objective-c -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

筹备实现后即可运行规定,在控制台中能够输入你的规定运行的后果以及调试信息。

应用规定

应用 Xcode 编写的规定实现编译后,能够在 Xcode 的 Products group 中找到相应的 dylib 文件。

默认状况下,规定将从$(/path/to/bin/oclint)/../lib/oclint/rules目录中加载,咱们将其命名为“ 规定搜寻门路”或“ 规定加载门路”。规定搜寻门路由一组动静库组成,这些库在Linux,macOS和 Windows中具备扩展名 so, dylib 以及 dll。

通过将新规定拖放到规定加载门路中,能够立刻应用它们。 因而,只须要将咱们自定义规定生成的 dylib 放入默认的规定加载目录即可。当然这里的规定目录也是能够配置的。一个我的项目能够应用多个规定搜寻门路,能够为不同的我的项目指定不同的规定加载门路。

更多具体的配置参考这里的官网文档:

抉择OCLint查看规定

总结

应用动态代码查看工具,能够高效的查看出代码中的潜在问题,在做继续的业务交付过程中,进步开发同学们对于编码标准的器重,避免代码的劣化,缩小一些因为大意导致的谬误。心愿本文提及的动态查看工具,以及自定义规定的编写的阐明,能帮忙大家写出更高质量,更优雅,更好看的代码。

参考资料

简述 LLVM 与 Clang 及其关系
Clang Tutorial
Clang Users Manual
oclint-docs v20.11