OCLint 入门到实战(下):自定义规则

2,878 阅读2分钟

关键词

oclint, cmake, xcode, rule

预计阅读时间

20-30 min

准备开发环境

本文基于以下环境完成

macOS Big Sur 11.1

Xcode 12.0.1

Apple clang version 12.0.0 (clang-1200.0.32.2)

cmake 3.19.2

OCLint 20.11

下载 oclint 代码

仓库地址 github.com/oclint/ocli…

在自定义目录下克隆仓库

$ git clone https://github.com/oclint/oclint.git

克隆完成后, 进入oclint-scripts 目录下 ./make 即可开始编译,当然在这个过程中,也会下载依赖库的代码。 具体的时间看网络的情况(建议自备梯子)。 编译成功后就可以准备规则的编写了!

创建规则——scaffoldRule 脚本

这是由 oclint 提供的一个脚手架。 相关介绍如下 docs.oclint.org/en/stable/d…

可以使用该脚本可以方便的创建自定义规则。

下面我们来看一个实例,在 oclint-scripts 目录下

./scaffoldRule KirinzerTest -c controversial -t ASTVisitor
// 生成一个名为 KirinzerTestRule 类型是 ASTVisitor 的规则模板

如果有错误提示,例如如下的提示 FileNotFoundError: [Errno 2] No such file or directory: '/Users/developer/Projects/oclint/oclint-> scripts/../llvm/tools/clang/include/clang/Basic/StmtNodes.td' 那么,需要将 llvm 拷贝至相应的目录下面。

我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/controversial/自动帮我们生成一个模板代码,并且加入编译路径中。

在 oclint-rules/rules/controversial 目录下,会生成两个文件

  • KirinzerTestRule.cpp
  • CMakeLists.txt
// CMakeLists.txt 是对规则 KirinzerTestRule 的编译描述,由make程序在编译时使用。KirinzerTestRule.cpp 的内容之后再分析

编写规则

通过阅读 oclint 的官方文档,以及阅读 clang AST 的介绍。现在我们已经知道了,oclint 的大致工作方式。首先通过调用 clang 的 api 把源文件一个个的生成对应的 AST;其次遍历 AST 中的每个节点,并根据相应的规则将违例情况写入违例结果集;最后根据配置的报告类型,将违例结果输出成指定的报告格式。

先上一个 oclint 编写思路的脑图,有个初步的印象即可。 04-3.png

按照上文,我们现在已经得到了一个 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,如果没看过建议先看看第一部分)

屏幕上一连串花花绿绿的字符闪过,最后停在了这里! 没错,这就是正是我们需要找的。Screen Shot 2021-02-02 at 11.56.41 AM.png

可以很清楚的看到,最上方的变量声明 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-xcoderules
mkdir oclint-xcoderules
cd 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
  1. 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

准备完成后即可运行规则,在控制台中可以输出你的规则运行的结果以及调试信息。 Screen Shot 2021-02-02 at 1.52.57 PM.png

使用规则

使用xcode 编写的规则完成编译后,可以在 xcode 的 Products group 中找到相应的 dylib 文件。

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

通过将新规则拖放到规则加载路径中,可以立即使用它们。 因此,只需要将我们自定义规则生成的 dylib 放入默认的规则加载目录即可。当然这里的规则目录也是可以配置的。一个项目可以使用多个规则搜索路径,可以为不同的项目指定不同的规则加载路径。

更多详细的配置参考这里的官方文档: 选择OCLint检查规则

接下来该做什么

本文已经是 OCLint 系列的最后一篇了,相信看完这 3 篇文章后,你对于 oclint 已经有了一定程度的了解。

使用静态代码检查工具,可以高效的检查出代码中的潜在问题。在做持续的业务交付过程中,提高开发同学们对于编码规范的重视,防止代码的劣化,减少一些由于粗心导致的错误。

希望本文提及的静态检查工具,以及自定义规则的编写的说明,能帮助大家写出更高质量,更优雅,更美观的代码。

source code and dylib

oclint-custom-rules

参考资料

compilation-options

Oclint-docs v20.11

Clang Users Manual