20分钟聊聊clang-tidy那些事

在1月初凯新有过在各算法/公共模块引入 Clang-tidy 的提议 ,在3月初我向大家分享了一个关于完善算法/业务项目CI流程的静态代码检查的文档,其中都提到了一个工具,那就是clang-tidy,今天我便带大家聊一聊clang-tidy。

Clang-tidy

大家应该多少对clang-tidy这个工具有一定的了解,它是一个基于clang的静态代码分析框架。同时它是一个功能非常强大的lint工具。绝大部分lint工具只能在出现问题的地方给出提示,之后需要人为修改,而clang-tidy拥有自动修复功能(当然怎么修复需要相应 checker 作者提供)。并且clang-tidy采用模块化设计,非常容易扩展。如果用户想往clang-tidy添加一个新的检测功能,只需要编写一个clang-tidy checker实现(如何编写一个clang-tidy check以后将会有一章详细介绍),每一个checker检测一种问题,例如检测某个违反Code style的模式,检测某些API不正确使用的方法等等。

那么提到了Code Style,我们不得说一说Google Code Style,这是目前使用最为广泛的一种代码规范。为此Google Code style也提供了一个cpplint脚本,用于检测代码中违反code style的地方。同时clang-tidy也支持检测代码中违反google code style的地方(google-*check)。那么可能会有人问他们之间有什么区别呢?

cpplint是一个python的脚本,它是采用正则表达式匹配出违反code style的代码, 所以它能检测的功能会受限于正则表达式,它不能够检测所有的违反code style的地方,并且还会有False positive和True negative;而clang-tidy则是基于抽象语法树(AST)对源文件进行分析,相比之下,是在分析的结果更加准确,能检测的问题也更多。

OK 到这里我们应该对clang-tidy 进行静态代码分析的原理有了一个大致的框架它基于clang,基于抽象语法树AST实现的。那clang和AST是什么?

Clang

众所周知clang是一个编译器,是一个以LLVM为后端的编译前端

那么clang和llvm还有什么其他关系呢?

这里我们可能需要说一下广义的llvm和狭义上的llvm了。在LLVM整体架构中,前端用的是clang,因此广义的LLVM是指整个LLVM架构,而狭义的LLVM指的是LLVM后端(包含代码优化和目标代码生成)。

在LLVM架构中,源代码(c/c++)经过clang生成中间代码,之后中间代码通过优化器进行一系列的优化,最后通过后端生成机器码。而在经过前端生成中间代码过程中,前端会对代码进行词法分析、语法分析以及语义分析。而在语法分析过程中变为生成一棵语法树,也就是我们说的AST。

int add(int a, int b){
    typedef long long ll;
    return a + b;
}
// 我们可以使用-fsyntax-only -Xclang -ast-dump 打印出源码文件的ast
// clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

`-FunctionDecl 0x1398df030 <test.cpp:1:1, line:4:1> line:1:5 add 'int (int, int)'
  |-ParmVarDecl 0x1398deed8 <col:9, col:13> col:13 used a 'int'
  |-ParmVarDecl 0x1398def58 <col:16, col:20> col:20 used b 'int'
  `-CompoundStmt 0x1398df250 <col:22, line:4:1>
    |-DeclStmt 0x1398df198 <line:2:5, col:25>
    | `-TypedefDecl 0x1398df140 <col:5, col:23> col:23 ll 'long long'
    |   `-BuiltinType 0x139840750 'long long'
    `-ReturnStmt 0x1398df240 <line:3:5, col:16>
      `-BinaryOperator 0x1398df220 <col:12, col:16> 'int' '+'
        |-ImplicitCastExpr 0x1398df1f0 <col:12> 'int' <LValueToRValue>
        | `-DeclRefExpr 0x1398df1b0 <col:12> 'int' lvalue ParmVar 0x1398deed8 'a' 'int'
        `-ImplicitCastExpr 0x1398df208 <col:16> 'int' <LValueToRValue>
          `-DeclRefExpr 0x1398df1d0 <col:16> 'int' lvalue ParmVar 0x1398def58 'b' 'int'

AST

从上可以看出,每一行包括AST node的类型,行号、列号以及类型的信息。在AST中有上千种node类型,我们这边简单看下常见的四种node:

  • Type,表示一个typedef
  • Decl,它表示程序中的一个声明,在其下面存在很多派生类。比如常见的FunctionDecl
  • Stmt,这个类表示程序中的一个语句,比如我们的while语句WhileStmt
  • Expr,它表示程序中的一个表达式。

那么既然有了这样一棵树,我们就可以通过对其节点结构做文章,精确定位到声明语句、赋值语句、运算语句等等,实现对代码的静态检查。

Clang-tidy Checker

而clang-tidy 的一系列checker便是去做了这件事。它在干这活的时候主要分为两部分,静态分析引擎(GRExprEngine.cpp以及其它)和 多个静态检查器(*Checker.cpp),然后所有的检查器都是建立在静态分析引擎的基础之上。

那他们怎么工作的呢?

首先分析器是一个源码的模拟器,追踪代码可能的执行路径。程序状态(变量和表达式的值)被封装为 ProgramState 。程序中的位置被叫做 ProgramPoint 。state 和 point 的组合是 ExplodedGraph 中的节点。

分析器会沿着 ExplodedGraph 执行可达性分析(Reachability Analysis)。从具有 entry program point 和 initial state 的根节点开始,分析模拟每个单独表达式的转移。表达式分析会产生状态改变,使用更新后的 program point 和 state 创建新节点。当满足某些 bug 条件时(违反检测不变量,checking invariant),就认为发现了 bug 。

最终,每个单独检查器(Checkers)也通过操作分析状态来工作。分析引擎通过访问者接口(visitor interface)与之沟通。比如,PreVisitCallExpr() 方法被 GRExprEngine 调用,来告诉 Checker 我们将要分析一个 CallExpr ,然后这个检查器被请求检查任意前置条件,这些条件可能不会被满足。检查器不会做除此之外的任何事情:生成一个新的 ProgramState 和包含更新后的检查器状态的 ExplodedNode 。如果它发现了一个 bug ,它会把错误告诉 BugReporter 对象,提供引发该问题的路径上的最后一个 ExplodedNode 节点。

静态分析引擎按下图所示流程运行。所有 Checker 都由 CheckerManager 管理调度,而CheckerManager 由分析引擎内核管理。

Checker 内部沿 ExplodedGraph 构造节点,遇到违反约束条件的状态时汇报错误。

Checker DIY

OK 到这里我们已经大概了解了checker是怎么工作的了,那我们应该怎么去编写一个自定义的checker呢?为了在ParseAST阶段快速找到自己想处理的AST 节点,clang提供了两种方式:RecursiveASTVisitor和ASTMatcher,我们今天只讲下使用clang提供的用来匹配和遍历AST 的DSL --- ASTMatcher来自定义一个checker

我们在这里简单实现一个检查if条件中为非零的整型常量且body为空的checker

  1. 我们先构建一个demo源文件,这里为clang-demo.cpp
int deeprouteDemo(int a, int b) {
        if (1) {}
        if (0.1) {}
        if ('a') {}
        if (!1) {}
        if (true) {}
}
  1. 使用前面说的clang -fmodules -fsyntax-only -Xclang -ast-dump命令查看demo的AST
`-FunctionDecl 0x55984380ad38 <demo.cpp:1:1, line:7:1> line:1:5 deeprouteDemo 'int (int, int)'
  |-ParmVarDecl 0x55984380abb8 <col:19, col:23> col:23 a 'int'
  |-ParmVarDecl 0x55984380ac30 <col:26, col:30> col:30 b 'int'
  `-CompoundStmt 0x55984380b490 <col:33, line:7:1>
    |-IfStmt 0x55984380ae88 <line:2:2, col:10>
    | |-<<<NULL>>>
    | |-<<<NULL>>>
    | |-ImplicitCastExpr 0x55984380ae60 <col:6> 'bool' <IntegralToBoolean>
    | | `-IntegerLiteral 0x55984380ae40 <col:6> 'int' 1
    | |-CompoundStmt 0x55984380ae78 <col:9, col:10>
    | `-<<<NULL>>>
    |-IfStmt 0x55984380b2e0 <line:3:2, col:12>
    | |-<<<NULL>>>
    | |-<<<NULL>>>
    | |-ImplicitCastExpr 0x55984380aee0 <col:6> 'bool' <FloatingToBoolean>
    | | `-FloatingLiteral 0x55984380aec0 <col:6> 'double' 1.000000e-01
    | |-CompoundStmt 0x55984380b2d0 <col:11, col:12>
    | `-<<<NULL>>>
    |-IfStmt 0x55984380b358 <line:4:2, col:12>
    | |-<<<NULL>>>
    | |-<<<NULL>>>
    | |-ImplicitCastExpr 0x55984380b330 <col:6> 'bool' <IntegralToBoolean>
    | | `-CharacterLiteral 0x55984380b318 <col:6> 'char' 97
    | |-CompoundStmt 0x55984380b348 <col:11, col:12>
    | `-<<<NULL>>>
    |-IfStmt 0x55984380b3f8 <line:5:2, col:11>
    | |-<<<NULL>>>
    | |-<<<NULL>>>
    | |-UnaryOperator 0x55984380b3c8 <col:6, col:7> 'bool' prefix '!'
    | | `-ImplicitCastExpr 0x55984380b3b0 <col:7> 'bool' <IntegralToBoolean>
    | |   `-IntegerLiteral 0x55984380b390 <col:7> 'int' 1
    | |-CompoundStmt 0x55984380b3e8 <col:10, col:11>
    | `-<<<NULL>>>
    `-IfStmt 0x55984380b458 <line:6:2, col:13>
      |-<<<NULL>>>
      |-<<<NULL>>>
      |-CXXBoolLiteralExpr 0x55984380b430 <col:6> 'bool' true
      |-CompoundStmt 0x55984380b448 <col:12, col:13>
      `-<<<NULL>>>
  1. clang-tidy 在LLVM/clang-tools-extra/clang-tidy下提供了一个名为add_new_check 的脚本文件(用 Python 编写),用以快速生成新的 checker,我们可以借助他来 init
./add_new_check.py misc checker-name

init之后它会替我们创建并更新一系列文件,短期我们只需要关心./misc/check-name.h./misc/check-name.cpp便可以,他们是实现我们自定义checker检查的主要文件。

而我们需要做的是在./misc/check-name.cpp中实现两个虚函数registerMatcherscheck

  1. 补充registerMatcher函数,编写匹配我们自定义规则的DSL,编写时可以使用clang-query进行验证DSL是否正确
void DeeprouteChecker1Check::registerMatchers(MatchFinder *Finder) {
  // FIXME: Add matchers.
  Finder->addMatcher(ifStmt(isExpansionInMainFile(),
                              hasCondition(
                                implicitCastExpr(
                                  hasDescendant(
                                    integerLiteral(
                                      unless(equals(false)))))), 
                              hasThen(
                                compoundStmt(
                                  statementCountIs(0)))
                            ).bind("deeproute-checker-1"), this);
}

验证Matcher正确性

$ clang-query clang-demo.cpp

$ match ifStmt(isExpansionInMainFile(),hasCondition(implicitCastExpr(hasDescendant(integerLiteral(unless(equals(false)))))), hasThen(compoundStmt(statementCountIs(0)))).bind("deeproute-checker-1")

Match #1:

/tmp/demo.cpp:2:2: note: "deeproute-checker-1" binds here
        if (1) {}
        ^~~~~~~~~
/tmp/demo.cpp:2:2: note: "root" binds here
        if (1) {}
        ^~~~~~~~~
1 match.
  1. 补充check函数

我们根据注册的matcher所绑定的节点名称进行访问,判断是否出现我们符合我们匹配要求的节点,并进行相应的警告打印

void DeeprouteChecker1Check::check(const MatchFinder::MatchResult &Result) {
  // FIXME: Add callback implementation.
  if (const auto *MatchedDecl = Result.Nodes.getNodeAs<IfStmt>("deeproute-checker-1")) {
    diag(MatchedDecl->getIfLoc(), "The condition is an integer constant and the body is empty")
        << FixItHint::CreateInsertion(MatchedDecl->getLParenLoc(), "true ");
  }
}
  1. 编译clang-tidy
cd llvm-project
mkdir build
cmake -GNinja  -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
    -DLLVM_ENABLE_ASSERTIONS=On \
    -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \
    -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON \
    -DLLVM_PARALLEL_LINK_JOBS=2 -DLLVM_BUILD_EXAMPLES=ON \
    -DLLVM_INCLUDE_EXAMPLES=ON ../llvm
ninja clang-tidy -j1

参考文档

  1. clang-tidy-check.md · jahentao/学习LLVM - Gitee.com
  2. https://zhuanlan.zhihu.com/p/369254889
  3. https://code.woboq.org/llvm/clang/
  4. AST Matcher Reference
Last modification:April 6th, 2023 at 11:26 am