首页
/ llvm-project 中编写 clang-tidy 自定义检查的完整开发指南:从 add_new_check.py 骨架到源码级原理

llvm-project 中编写 clang-tidy 自定义检查的完整开发指南:从 add_new_check.py 骨架到源码级原理

2026-09-08 11:50:10作者:秋泉律Samson

导读

本文以 llvm-project 仓库(LLVM 官方 monorepo)中 clang-tools-extra/docs/clang-tidy/Contributing.rst(《Getting Involved》)为核心脉络,系统讲解如何为 :program:clang-tidy 开发、注册、配置、测试并提交一个全新的自定义 lint 检查。读完本文,你将掌握选择检查实现形态(Clang diagnostic / 静态分析器 / clang-tidy check)的决策依据、借助 add_new_check.py 生成检查骨架与测试的完整流程、AST Matcher 与 PPCallbacks 两种插件式分析机制、check 配置项与 storeOptions 的工作方式,以及 check_clang_tidy.py 测试规范与提交 Pull Request、加载 out-of-tree 插件、在 LLVM 全量源码上运行与性能分析等实战方法。

1. 为什么选择 clang-tidy:扩展点的定位与取舍

clang-tidy 本身内置了大量检查,同时可以运行 Clang 静态分析器(clang-analyzer-*)的检查,但它真正的威力在于能够以极低成本编写自定义检查:检查被组织成模块(module),通过极少的代码改动(甚至零改动)就能链接进 clang-tidy 主程序。

检查可以在两个层面接入分析管道:

  • 预处理器层(PPCallbacks):通过 PPCallbacks 钩子观察宏展开、include 指令等预处理事件;
  • AST 层(AST Matchers):通过 AST Matchers 声明式地描述要匹配的语法模式。

当发现问题时,检查以类似 Clang 诊断的方式上报告警,并可附带 fix-it 提示(自动修复建议)。clang-tidy 提供的这套接口让"用几行代码写出有用且精确的检查"成为可能——这正是本指南要展开的内容。

在动手之前,需要先判断你的想法应该落在哪一层,文档给出了三条清晰的分流标准:

实现形态 适用场景
Clang diagnostic 检查足够通用、针对"大概率是 bug"的代码模式、可以有效实现且误报率极低时,优先放进 Clang 前端核心诊断;而非风格/可读性问题。
Clang static analyzer check 检查需要控制流分析(如路径敏感分析、跨语句数据流),应实现为静态分析器检查。
clang-tidy check 面向 linter 风格的检查、与特定编码风格强相关(Google、LLVM、CERT 等)、处理可读性问题等的检查,最适合作为 clang-tidy 检查。

仓库佐证:在 clang-tools-extra/clang-tidy/ClangTidyCheck.hClangTidy.h 中可以看到面向检查作者与使用者的核心接口定义;模块系统在 ClangTidyModule.h 中声明。

2. 准备工作:工作区与构建配置

如果你是 LLVM 开发新手,应先阅读 LLVM 官方的《Getting Started with the LLVM System》《Using Clang Tools》与《How To Setup Clang Tooling For LLVM》,用 CMake 完成 LLVM、Clang 与 Clang Extra Tools 的检出和构建。

使用 CMake 配置构建时,务必同时启用 clangclang-tools-extra 两个 project,clang-tidy 才会被构建。需要注意的相关配置项还包括:

  • 因为新检查会附带文档,建议安装 Sphinx 并在 CMake 配置中启用文档生成;
  • 为了节省核心 Clang 库的编译时间,可以只在 CMake 配置中启用 X86 target;
  • 若设置 CLANG_TIDY_ENABLE_STATIC_ANALYZER=NO,构建出的 clang-tidy 将不支持 clang-analyzer-*mpi-* 检查;
  • 若设置 CLANG_TIDY_ENABLE_QUERY_BASED_CUSTOM_CHECKS=NO,构建出的 clang-tidy 将不支持基于 query 的自定义检查。

在仓库当前的源码树中,clang-tidy 的核心实现位于 clang-tools-extra/clang-tidy,其构建开关在 clang-tidy/CMakeLists.txtclang-tidy-config.h.cmake 中体现。

3. 仓库目录结构:一份 clang-tidy 的"地图"

理解 clang-tidy 的源码布局是编写检查的第一步。以仓库根目录为参照,其结构如下(相对于原文档给出的 llvm/clang-tools-extra 前缀,本文统一从仓库根换算为 clang-tools-extra/):

clang-tools-extra/clang-tidy/          # clang-tidy 核心
|-- ClangTidy.h                        # 面向使用者的接口
|-- ClangTidyCheck.h                   # 面向检查(check)的接口
|-- ClangTidyModule.h                  # clang-tidy 模块(module)接口
|-- add_new_check.py                   # 新建检查的自动化脚本
|-- rename_check.py                    # 重命名既有检查的脚本
|-- google/                            # Google 风格模块
|   |-- GoogleTidyModule.cpp / .h      # 模块注册
|   |-- <各检查>Check.cpp / Check.h
|-- llvm/                              # LLVM 风格模块
|-- objc/                              # Objective-C 模块
|-- ...
|-- tool/                              # clang-tidy 可执行程序入口源码
|   |-- run-clang-tidy.py
|   |-- clang-tidy-diff.py
|-- utils/                             # 检查共用工具库(如 TransformerClangTidyCheck.h)
clang-tools-extra/test/clang-tidy/     # 集成测试(lit)
clang-tools-extra/unittests/clang-tidy/  # 单元测试

在仓库中可以看到大量与模块同名的子目录:abseil/altera/android/boost/bugprone/cert/cppcoreguidelines/google/llvm/llvmlibc/misc/modernize/objc/performance/readability/ 等。这些目录名称与用户面向的检查组(check group)名称一致,每个目录中都包含对应的 *TidyModule.cpp/.h 与成对的 *Check.cpp/.h 文件,例如 google/GoogleTidyModule.cpp。可执行程序相关辅助脚本 run-clang-tidy.pyclang-tidy-diff.py 位于 clang-tidy/tool 下。

4. 编写一个 clang-tidy Check:从骨架到完整实现

4.1 用 add_new_check.py 一键生成骨架

决定好模块与检查名后,官方推荐直接运行 clang-tidy/add_new_check.py 脚本(仓库内完整实现位于 clang-tools-extra/clang-tidy/add_new_check.py),它会自动完成检查的创建、CMake 文件更新与测试生成。其命令行参数在脚本的 argparse 部分(main 函数入口)定义:

  • 位置参数 module:新检查所属模块目录(如 readability);
  • 位置参数 check:新检查名称(如 awesome-function-names);
  • --language LANG:检查适用的语言,可选 c / c++ / objc / objc++,默认 C++;
  • --standard:限定语言标准版本(如 c++17c11),会据此生成 isLanguageVersionSupported 的对应判断;
  • --description / -d:检查的一句话描述(默认 FIXME: Write a short description),会同步写入头文件注释与 Release Notes;
  • --update-docs:只重建文档清单列表后退出。

脚本内部(从 write_headerwrite_implementationadapt_modulewrite_testwrite_docs 等函数可以看到)会依次完成:

  1. 在指定模块目录内创建检查类对应的 .h.cpp,并把 .cpp 加入该模块的 CMakeLists.txt(按字典序插入,见 adapt_cmake);
  2. 在模块的 *TidyModule.cpp 中登记检查的注册语句(见 adapt_module,插入 registerCheck<...>(...));
  3. test/clang-tidy/checkers/<module>/ 下创建 lit 测试文件(该脚本将测试写到相对于模块的 ../../test/clang-tidy/checkers/<module>/<check>.<ext>,即仓库的 clang-tools-extra/test/clang-tidy/checkers 目录);
  4. 创建检查文档文件并加入 docs/clang-tidy/checks/list.md 的清单与 Release Notes。

一个值得注意的细节:在脚本的 main 流程中,若模块名为 llvm,其 C++ 命名空间会被特意映射为 llvm_check,以避免与全局广泛使用的 llvm 命名空间冲突;其他模块直接以模块名作为命名空间。

默认生成的新检查只作用于 C++ 代码;如需不同语言选项,使用脚本的 --language 参数。例如,创建名为 readability-awesome-function-names 的检查:

$ clang-tidy/add_new_check.py readability awesome-function-names

4.2 解读自动生成的检查类骨架

脚本生成的检查类头文件模板如下(注意 #include 的是本模块外一层的公共基类头文件):

#include "../ClangTidyCheck.h"

namespace clang::tidy::readability {

class AwesomeFunctionNamesCheck : public ClangTidyCheck {
public:
  AwesomeFunctionNamesCheck(StringRef Name, ClangTidyContext *Context)
      : ClangTidyCheck(Name, Context) {}
  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
    return LangOpts.CPlusPlus;
  }
};

} // namespace clang::tidy::readability

要点如下:

  • 构造函数接收 NameContext,必须原样转发给 ClangTidyCheck 构造函数;其中 Context 是访问选项(Options)、诊断报告等全局能力的总入口。
  • isLanguageVersionSupported(可选覆写)用于限定检查生效的语言与标准版本;add_new_check.py--language / --standard 参数正是生成该函数返回表达式(如 C 语言为 !LangOpts.CPlusPlus,并可按 C11C17CPlusPlus11…… 追加条件,可对照脚本中 cpp_language_to_requirementsc_language_to_requirements 两张映射表)。
  • 若需在 AST 层分析,覆写 registerMatcherscheck;若需分析预处理器层,则应覆写 registerPPCallbacks 方法——注意 add_new_check.py 生成的起点骨架不会生成 registerPPCallbacks 覆写,需要自行添加。

registerMatchers 中创建 AST Matcher(语法细节参考 Clang 官方《AST Matchers》与《AST Matcher Reference》),描述你想要在 AST 中发现的模式;匹配结果被交给 check 方法,在那里做进一步检查并上报诊断。

以生成的 .cpp 为模板,完整的示例实现:

using namespace clang::ast_matchers;

void AwesomeFunctionNamesCheck::registerMatchers(MatchFinder *Finder) {
  Finder->addMatcher(functionDecl().bind("x"), this);
}

void AwesomeFunctionNamesCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
  if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().startswith("awesome_"))
    return;
  diag(MatchedDecl->getLocation(), "function %0 is insufficiently awesome")
      << MatchedDecl
      << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
}
  • Finder->addMatcher(..., this) 把 matcher 注册进 MatchFinder,bind("x") 给匹配节点命名;
  • check 中通过 Result.Nodes.getNodeAs<FunctionDecl>("x") 取出匹配节点;%0 是诊断信息中参数的占位符,由后续 << MatchedDecl 传入(Clang 会把 NamedDecl 格式化输出为函数名);
  • FixItHint::CreateInsertion(loc, "awesome_") 声明一个在 loc 处插入 awesome_ 前缀的自动修复建议;同时上报的警告会被 clang-tidy 记录,并可用 -fix 一键应用。

需要说明的是:clang-tidy 的官方文档示例曾引用 google/ExplicitConstructorCheck 作为参考实现;当前仓库的 google 目录已不包含该检查,想研读真实模块内样例时可参考同目录下现存的检查对,例如 google/UsingNamespaceDirectiveCheck.cpp,它们遵循同样的「registerMatchers 声明匹配 + check 上报诊断」模式。

4.3 诊断报告与 fix-it 的挂载机制

文档强调:clang-tidy 检查通过 diag(...) 上报问题,行为与 Clang 自身诊断一致——诊断消息关联到源码位置,可携带参数与多个 fix-it。需要与宏或预处理指令交互时,则覆写 registerPPCallbacks,利用 PPCallbacks 观察预处理阶段的事件(例如记录宏定义位置供后续 AST 匹配时判断节点是否来自宏展开)。clang-tidy 在报告阶段使用 ClangTidyDiagnosticConsumer(见 ClangTidyDiagnosticConsumer.cpp)收集并去重诊断。

4.4 开发者常用辅助工具

开发 clang-tidy 检查时以下工具尤为有用:

  • add_new_check.py——自动化"新增检查"全流程(见上);
  • rename_check.py——如脚本名所示,重命名既有检查;
  • pp-trace——记录某个源文件上的 PPCallbacks 方法调用,是理解预处理器机制不可或缺的工具;
  • clang-query——交互式原型化 AST matcher、探索 Clang AST 的利器;
  • clang-check 的 -ast-dump(可搭配 -ast-dump-filter)——便捷地转储一段 C++ 程序的 AST。

5. 检查开发技巧:文档指引、Transformer 与增量式开发

5.1 有用的背景文档速览

写检查前建议先在 LLVM/Clang 代码库中定向阅读:

  • LLVM 层被 Clang 大量使用的支持类,如 StringRefSmallVector 等,见《LLVM Programmer's Manual》中「Important and useful LLVM APIs」「Picking the Right Data Structure for the Task」两节;LLVM/ADT/STLExtras.h 里提供了作用于 LLVM 容器的实用 STL 算法变体(如 llvm::all_of)。这些类都可在 doxygen 中检索,无需强记。
  • Clang 层:诊断、fix-it 与源码位置相关的机制由 The Clang "Basic" Library 描述;token、词法与预处理器由 The Lexer and Preprocessor Library 描述;C++ 源码语句如何在 AST 中表示由 The AST Library 描述——以上三个主题都收录在《"Clang" CFE Internals Manual》中。
  • 绝大多数检查经由 AST 与 C++ 源码交互:源文件先被词法分析、预处理,再解析成 AST;AST 完整构建后,clang-tidy 把检查注册的 matcher 应用其上,命中节点即回调 check。对预处理器的监视独立于 AST 构建,但检查可以在预处理阶段收集信息、供后续 AST 匹配时使用。
  • C++ 源码的每个句法(甚至语义)元素在 AST 中都对应不同类,通过组合 AST matcher 函数筛选感兴趣的片段——建议仔细研读官方《AST Matcher Reference》理解不同 matcher 函数之间的关系。

5.2 用 Transformer 库编写重写型检查

Transformer 库允许把源码变换表达为一条 RewriteRule,并提供了组合源码编辑的函数。除非需要底层源码位置操作,否则应考虑用 Transformer 库来写检查,细节参见《Clang Transformer Tutorial》。

add_new_check.py 生成的代码,改为使用 Transformer 库需要做四处修改:

  1. #include "../ClangTidyCheck.h" 换成 #include "../utils/TransformerClangTidyCheck.h"
  2. 把检查的基类从 ClangTidyCheck 改为 TransformerClangTidyCheck
  3. 删除类中对 registerMatcherscheck 的覆写;
  4. 编写一个创建 RewriteRule 的函数,并在构造函数中把它传给 TransformerClangTidyCheck 的构造函数。

从仓库源码 TransformerClangTidyCheck.h 可以看到:该基类同时覆写了 registerPPCallbacksregisterMatchers(final)与 check(final),把 matcher/check 生命周期内部化;它还识别一个名为 IncludeStyle 的 clang-tidy 选项(取值 llvmgoogle,默认 llvm),影响规范头文件的区分方式。规则中每个 case 都必须带非空 Explanation,因为它同时充当诊断文案。

5.3 增量式开发流程与 clang-query 原型验证

推荐从简单用例出发、逐步增加复杂度,add_new_check.py 生成的测试文件正是起点。大致流程:

  1. 为检查编写一个测试用例;
  2. 用 clang-query 在测试文件上原型化 matcher;
  3. 把验证过的 matcher 固化进 registerMatchers
  4. check 中上报所需诊断与 fix-it;
  5. 为用例补充 CHECK-MESSAGESCHECK-FIXES 注释以验证诊断与修复;
  6. 构建 check-clang-tools target 确认测试通过;
  7. 循环往复,直到检查的所有方面都有测试覆盖。

clang-query 支持把复杂匹配表达式拆解并命名:

clang-query> let c1 cxxRecordDecl()
clang-query> match c1

此外,在一个 matcher 的左括号后按 Tab 会提示可与前一个 matcher 链式组合的候选 matcher(部分可用 matcher 可能不在提示列表里;注意 Tab 补全目前不支持 Windows)。就像把巨型函数拆成带语义命名的小函数有助于理解算法一样,把复杂 matcher 拆成带语义命名的小 matcher 也有助于理解与复用:交互式验证成功后,C++ API matcher 通常与交互版本一致或相近,可以用局部变量保留这些命名。

5.4 创建私有 matcher 与单元测试辅助代码

当现有 AST matcher 无法表达所需的具体 AST 特征时,可以用与公共 matcher 相同的基础设施创建自己的私有 matcher。它的好处是:把复杂的"手工 AST 遍历"逻辑下沉到 matcher 里,check 中只需按绑定名取用所需节点。

私有 matcher 等"辅助支撑代码"非常适合用单元测试覆盖——比 FileCheck 集成测试更易测。仓库中公共 AST matcher 类的单元测试位于 ASTMatchersTests target,是学习测试惯用法的好样板。clang-tidy 自身的单元测试通过构建 ClangTidyTests target 运行;需要提醒的是,LLVM/Clang 中测试类 target 会被排除在 IDE CMake 生成器的 "build all" 之外,必须显式指定 target 才会真正构建。

5.5 让检查足够健壮

覆盖基本「happy path」后,建议用尽可能多的边界场景"折磨"检查。在大型代码库(如 Clang/LLVM 自身)上试跑是发现 matcher 遗漏的好办法;但 LLVM 代码库未必足够——它是按特定编码风格与质量标准演进的,测试语料越大,社区对检查有效性及误报率的信心越强。文档给出如下建议:

  • 创建包含被匹配代码的头文件用例;
  • 用 clang-tidy 手工验证 fix-it 在头文件上的应用是否正确(在 check_clang_tidy.py 支持自动校验前需手工完成);
  • 定义包含被匹配代码的模板类模板特化用例;
  • 同时在 Windows 与 Linux 下测试;例如 fix-it 插入换行时应使用源文件已有的换行风格而非硬编码 \n,可用 SourceManager::getBufferData(FileID).detectEOL() 探测;
  • 警惕高误报率:理想情况下检查零误报,但 AST 匹配不敏感于控制流/数据流,出现一定误报在所难免——误报率越高越难被采用,应为用户管理误报提供机制。两种主要机制:支持"免打扰"代码模式(如允许显式 (void) 强转来静默未使用变量告警,只要该模式能清晰表达程序员意图)与 检查配置选项(允许用户选择更激进的检查行为,同时不为常见的高置信场景增加负担)。

5.6 为检查撰写文档

add_new_check.py 会在 Release Notes、检查清单与 docs/clang-tidy/checks/<module>/<check>.md 处创建条目。建议:用一句话写清检查作用,这句话应同时出现在 Release Notes、头文件 doxygen 注释首句与检查文档首句中(注意在总结中避免使用 "this check" 这类措辞)。

如果检查涉及已发布的编码指南(C++ Core Guidelines、SEI CERT 等)或风格规范,应在文档中给出相应章节链接;同时应提供足够的诊断与 fix-it 示例,让用户能直观理解运行后代码会发生什么变化;存在例外或局限时务必详述。需要注意一个法律/许可约束:直接引用 MISRA 或 AUTOSAR 指南的检查不会被接受;仅与之重叠、但不声称实现或链接 MISRA/AUTOSAR 规则的通用检查则是可以的。

构建 docs-clang-tools-html target 会运行 Sphinx 生成 HTML 文档到构建树中;请确认新检查正确出现在 Release Notes 与检查清单中,且文档格式结构无误。

6. 注册你的检查:模块机制与链接锚点

(日常开发中 add_new_check.py 会替你完成在既有模块中的注册;只有当你需要创建全新模块或了解注册细节时才需手动操作,以下内容照抄自原文档并保留完整代码。)

检查需在对应模块中以独立名字注册:

class MyModule : public ClangTidyModule {
 public:
  void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override {
    CheckFactories.registerCheck<ExplicitConstructorCheck>(
        "my-explicit-constructor");
  }
};

随后用静态初始化变量把模块注册进 ClangTidyModuleRegistry

static ClangTidyModuleRegistry::Add<MyModule> X("my-module",
                                                "Adds my lint checks.");

由于 LLVM 构建系统中模块分散编译为独立目标文件,需要用如下"锚点 hack"保证模块真正被链接进 clang-tidy 可执行程序。在该注册变量附近添加:

// This anchor is used to force the linker to link in the generated object file
// and thus register the MyModule.
volatile int MyModuleAnchorSource = 0;

并在 clang-tidy 主程序(或链接了 clang-tidy 库的二进制)的主翻译单元中(即 ClangTidyForceLinker.h)加入对侧锚点:

// This anchor is used to force the linker to link the MyModule.
extern volatile int MyModuleAnchorSource;
static int MyModuleAnchorDestination = MyModuleAnchorSource;

7. 配置检查:Options 的读取与 storeOptions

若检查需要配置选项,可在构造函数里用 Options.get<Type>("SomeOption", DefaultValue) 读取检查专属选项,同时覆写 ClangTidyCheck::storeOptions 让这些选项可被发现。storeOptions 向 clang-tidy 声明该检查实现了哪些选项及当前值(例如 -dump-config 命令行选项就会用到它)。

class MyCheck : public ClangTidyCheck {
  const unsigned SomeOption1;
  const std::string SomeOption2;

public:
  MyCheck(StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      SomeOption1(Options.get("SomeOption1", -1U)),
      SomeOption2(Options.get("SomeOption2", "some default")) {}

  void storeOptions(ClangTidyOptions::OptionMap &Opts) override {
    Options.store(Opts, "SomeOption1", SomeOption1);
    Options.store(Opts, "SomeOption2", SomeOption2);
  }
  ...

假设检查注册名为 "my-check",则在 .clang-tidy 文件中按如下方式设置:

CheckOptions:
  my-check.SomeOption1: 123
  my-check.SomeOption2: 'some other value'

命令行指定检查选项时使用内联 YAML 格式:

$ clang-tidy -config="{CheckOptions: {a: b, x: y}}" ...

从源码实现看,选项系统位于 ClangTidyOptions.cpp / ClangTidyOptions.h,其中的 OptionsView/ClangTidyOptions::OptionMap 提供了 get<Type>store 等接口,是上面这些用法的底层支撑。

8. 测试检查:从 check-clang-tools 到 check_clang_tidy.py

8.1 运行测试 target

运行 clang-tidy 的全部测试,构建 check-clang-tools target。例如用 Ninja 生成器配置的构建:

$ ninja check-clang-tools

clang-tidy 检查可以用单元测试lit 测试两种方式测试。单元测试更适合严格校验复杂替换;lit 测试支持部分文本匹配与正则,更适合编写紧凑的诊断消息测试。

8.2 check_clang_tidy.py 测试框架

check_clang_tidy.py 脚本提供了一种便捷方式同时测试诊断消息与 fix-it(脚本位于 clang-tools-extra 的 test 基础设施中)。它从测试文件中过滤掉 CHECK 行,运行 clang-tidy,然后用两次独立的 FileCheck 调用验证:第一次以 CHECK-MESSAGES 前缀校验诊断消息,第二次以 CHECK-FIXES 前缀对应用 fix-it 之后的代码做校验。特别地,CHECK-FIXES: 可用"原样出现在修复后代码中"来断言某些代码未被 fix-it 修改。FileCheck 的完整指令集都可用(如 CHECK-MESSAGES-SAME:CHECK-MESSAGES-NOT:),尽管基本形态 CHECK-MESSAGES/CHECK-FIXES 通常已足够。注意 FileCheck 官方文档默认前缀是 CHECK,描述为 CHECK:CHECK-SAME:CHECK-NOT: 等,用于 clang-tidy 测试时把 CHECK 替换成 CHECK-FIXESCHECK-MESSAGES 即可。

check_clang_tidy.py 还附加一个约束检查:若文件中使用了 CHECK-MESSAGES:,则每条 warning/error 都必须有对应的 CHECK;也可改用 CHECK-NOTES:,若你还想额外保证所有 note 都被检查到。

使用方式:把带合适 RUN 行的 .cpp 文件放入 test/clang-tidy 目录,用 CHECK-MESSAGES:/CHECK-FIXES: 编写校验。建议把检查写尽可能具体,避免误匹配输入的其他部分;在测试代码中使用 [[@LINE+X]] / [[@LINE-X]] 行号替换与不重名的函数、变量名。下面是一个基本用例:

// RUN: %check_clang_tidy %s google-readability-casting %t

void f(int a) {
  int b = (int)a;
  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: redundant cast to the same type [google-readability-casting]
  // CHECK-FIXES: int b = a;
}

(RUN 行各字段依次是:被测文件 %s、检查全名、临时文件 %t。)

在同一个测试文件里校验多个场景时,用 -check-suffix=SUFFIX-NAME-check-suffixes=SUFFIX-NAME-1,SUFFIX-NAME-2,... 参数,并把指令替换为 CHECK-MESSAGES-SUFFIX-NAMECHECK-FIXES-SUFFIX-NAME

 // RUN: %check_clang_tidy -check-suffix=USING-A %s misc-unused-using-decls %t -- -- -DUSING_A
 // RUN: %check_clang_tidy -check-suffix=USING-B %s misc-unused-using-decls %t -- -- -DUSING_B
 // RUN: %check_clang_tidy %s misc-unused-using-decls %t
 ...
 // CHECK-MESSAGES-USING-A: :[[@LINE-8]]:10: warning: using decl 'A' {{.*}}
 // CHECK-MESSAGES-USING-B: :[[@LINE-7]]:10: warning: using decl 'B' {{.*}}
 // CHECK-MESSAGES: :[[@LINE-6]]:10: warning: using decl 'C' {{.*}}
 // CHECK-FIXES-USING-A-NOT: using a::A;$
 // CHECK-FIXES-USING-B-NOT: using a::B;$
 // CHECK-FIXES-NOT: using a::C;$

8.3 用 -std 控制被测语言标准

-std 标志控制测试在哪种 C/C++ 标准下编译,接受逗号分隔的标准列表,并支持 -or-later / -or-earlier 后缀:

  • -std=c++17用 C++17 运行测试;
  • -std=c++17-or-later:从 C++17 起对每个标准(当前为 C++17、C++20、C++23、C++26)分别运行一次。适用于应在所有现代标准下工作正常的检查;
  • -std=c++17-or-earlier:对到 C++17 为止的每个标准(当前为 C++98、C++11、C++14、C++17)分别运行。适用于应兼容所有旧标准的检查;
  • -std=c++14,c++17:分别用 C++14 与 C++17 各运行一次。

未指定 -std 时,check_clang_tidy.py 对 C++ 文件默认 c++11-or-later,对 C 文件默认 c99-or-lateradd_new_check.py 生成的骨架默认使用 -or-later 形式。除非测试期望只在特定标准版本下出现的行为,否则优先写成 -std=<最低版本>-or-later

8.4 高频陷阱:宏与模板

C++ 语言存在许多"暗角",要让检查(尤其带 fix-it 的)在所有情况下都完美并不容易,最常见的两类陷阱是模板

  1. 写在宏体/模板定义中的代码,其含义可能随宏展开/模板实例化而改变;
  2. 多次宏展开/模板实例化可能让同一段代码被检查多次(含义还可能不同),同一条警告可能被重复上报;clang-tidy 会去重完全相同的警告,但若警告稍有差异,全部都会展示给用户(并用于应用修复);
  3. 对宏体/模板定义做替换,对某些宏展开/模板实例化也许没问题,但很容易破坏另一些展开/实例化。

若需要多个文件来覆盖检查的各个方面,建议放进该模块 Inputs 目录下以检查命名的子目录,避免污染测试目录。若要验证检查与系统头文件的交互,仓库提供了一套模拟系统头文件,位于 checkers/Inputs/Headers 目录,lit 测试中可用变量 %clang_tidy_headers 引用其路径。

9. 提交 Pull Request:提交前自检

提交 PR 前,鼓励先在改动上运行 clang-tidy 与 clang-format,以保障代码质量、提前暴露问题。虽然 clang-tidy 目前并未在 CI 中强制启用,遵循这一实践有助于保持代码一致性、预防常见错误。一个有用的命令,用于检查已暂存(staged)的改动:

$ git diff --staged -U0 | ./clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py \
    -j $(nproc) -path build/ -p1 -only-check-in-db
$ git clang-format

clang-tidy-diff.py 位于 clang-tidy/tool(与 run-clang-tidy.py 同目录),其参数 -j 指定并行任务数、-path 指向构建目录、-p1 处理标准 diff 前缀、-only-check-in-db 只检查编译数据库中的文件。请注意部分警告可能是误报或需慎重权衡,使用自己的判断;对个别告警拿不准时,欢迎在 PR 中讨论。

10. Out-of-tree 检查插件

把检查作为插件在源码树外开发,大体遵循前述步骤(包括新建模块、做模块注册所需的各种 hack)。插件是共享库,其代码在 clang-tidy 构建系统之外;按其他 Clang 插件的做法与 LLVM 一起构建、链接即可。若用 CMake,调用 add_libraryllvm_add_library 时使用关键字 MODULE

插件通过 -load 传给 clang-tidy,同时还需给出要启用的检查名:

$ clang-tidy --checks=-*,my-explicit-constructor -list-checks -load myplugin.so

没有 ABI/API 稳定性承诺,插件必须用将加载它的那份 clang-tidy 版本编译。插件可以使用线程、TLS 或其他任何可从外部头文件访问、树内代码可用的设施。

测试 out-of-tree 检查可能需要从源码编译的 LLVM 安装中获取 llvm-lit;或者按 test-suite 指南获取 lit、拿到 FileCheck 二进制,再仿照 check_clang_tidy.py 写一份适配自己需求的版本。

11. 在 LLVM 全量源码上运行 clang-tidy:run-clang-tidy.py

在更大代码库上试跑是检验检查的最佳方式,LLVM/Clang 正是天然目标(源码已在手边)。最便捷的方式是借助编译命令数据库(compile command database):CMake 可自动生成 compile_commands.json。一旦就绪且 clang-tidy 在 PATH 中,即可对整个代码库运行分析:

clang-tidy/tool/run-clang-tidy.py

该脚本会以默认检查集对编译数据库中每个翻译单元执行 clang-tidy,并展示产生的警告与错误,还提供多个配置开关:

  • 覆盖默认检查集-checks 参数,格式与 clang-tidy 完全一致。例如 -checks=-*,modernize-use-override 只运行 modernize-use-override
  • 限定分析文件:提供一个或多个文件名的正则参数。run-clang-tidy.py clang-tidy/.*Check\.cpp 只分析 clang-tidy 检查文件。还可能需要用 -header-filter-exclude-header-filter 限制展示警告的头文件范围,二者行为与 clang-tidy 对应选项一致。
  • 应用修复-fix 把所有修改收集到临时目录后统一应用;再加 -format 会对改动行运行 clang-format。

12. 检查性能剖析:-enable-check-profile 与 JSON 输出

clang-tidy 可以为每个检查收集性能剖析信息,并对每个被处理的源文件(翻译单元)输出。

启用剖析信息收集使用 -enable-check-profile 参数,计时结果以表格形式输出到 stderr。示例输出:

$ clang-tidy -enable-check-profile -checks=-*,readability-function-size source.cpp
===-------------------------------------------------------------------------===
                          clang-tidy checks profiling
===-------------------------------------------------------------------------===
  Total Execution Time: 1.0282 seconds (1.0258 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
   0.9136 (100.0%)   0.1146 (100.0%)   1.0282 (100.0%)   1.0258 (100.0%)  readability-function-size
   0.9136 (100.0%)   0.1146 (100.0%)   1.0282 (100.0%)   1.0258 (100.0%)  Total

也可以把数据存为 JSON 文件以便后续处理:

$ clang-tidy -enable-check-profile -store-check-profile=. -checks=-*,readability-function-size source.cpp
$ # Note that there won't be timings table printed to the console.
$ ls /tmp/out/
20180516161318717446360-source.cpp.json
$ cat 20180516161318717446360-source.cpp.json
{
"file": "/path/to/source.cpp",
"timestamp": "2018-05-16 16:13:18.717446360",
"profile": {
  "time.clang-tidy.readability-function-size.wall": 1.0421266555786133e+00,
  "time.clang-tidy.readability-function-size.user": 9.2088400000005421e-01,
  "time.clang-tidy.readability-function-size.sys": 1.2418899999999974e-01
}
}

控制存储的参数只有一个:

  • -store-check-profile=<prefix>

    默认情况下报告以表格形式输出到 stderr;传入此选项后,每个翻译单元的剖析数据改为存为 JSON。若 prefix 不是绝对路径,则视为相对于运行 clang-tidy 所在目录;路径中所有 ... 会被折叠,符号链接会被解析。

    示例:假设源文件 example.cpp 位于 /source 目录。存储时只用输入文件名(而非源文件完整路径),并加当前时间戳前缀。

    • 指定 -store-check-profile=/tmp,剖析文件保存到 /tmp/<ISO8601-like 时间戳>-example.cpp.json
    • /foo 目录内运行 clang-tidy 并指定 -store-check-profile=.,剖析文件仍会保存到 /foo/<ISO8601-like 时间戳>-example.cpp.json

    这一能力对应仓库中的 ClangTidyProfiling.cpp,它实现了计时表输出与 JSON 落盘逻辑,供希望理解剖析实现细节的读者继续深入。

结语:从「会写」到「被社区接受」

围绕 Contributing.rst 所描述的完整闭环,一个高质量 clang-tidy 检查的诞生路径可以概括为:正确定位检查形态 → 用 add_new_check.py 生成模块化骨架 → 用 AST Matcher / PPCallbacks / Transformer 实现匹配与修复 → 通过 storeOptions 暴露可控选项 → 用 check_clang_tidy.py + -std 矩阵覆盖宏、模板、头文件等边界 → 提交前用 clang-tidy-diff.py 自检并在 PR 中沟通 → 面向社区接受度持续控制误报率。仓库内 clang-tidy/add_new_check.py(786 行,含 CMake/模块/ReleaseNotes/文档/测试五处自动修改)与 clang-tidy 目录下各模块的现成检查对,为上述每一步都提供了可直接对照、可逐行研读的真实实现样例。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391