llvm-project 中编写 clang-tidy 自定义检查的完整开发指南:从 add_new_check.py 骨架到源码级原理
导读
本文以 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.h 与 ClangTidy.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 配置构建时,务必同时启用 clang 与 clang-tools-extra 两个 project,clang-tidy 才会被构建。需要注意的相关配置项还包括:
- 因为新检查会附带文档,建议安装 Sphinx 并在 CMake 配置中启用文档生成;
- 为了节省核心 Clang 库的编译时间,可以只在 CMake 配置中启用
X86target; - 若设置
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.txt 与 clang-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.py、clang-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++17、c11),会据此生成isLanguageVersionSupported的对应判断;--description/-d:检查的一句话描述(默认FIXME: Write a short description),会同步写入头文件注释与 Release Notes;--update-docs:只重建文档清单列表后退出。
脚本内部(从 write_header、write_implementation、adapt_module、write_test、write_docs 等函数可以看到)会依次完成:
- 在指定模块目录内创建检查类对应的
.h与.cpp,并把.cpp加入该模块的 CMakeLists.txt(按字典序插入,见adapt_cmake); - 在模块的
*TidyModule.cpp中登记检查的注册语句(见adapt_module,插入registerCheck<...>(...)); - 在
test/clang-tidy/checkers/<module>/下创建 lit 测试文件(该脚本将测试写到相对于模块的../../test/clang-tidy/checkers/<module>/<check>.<ext>,即仓库的 clang-tools-extra/test/clang-tidy/checkers 目录); - 创建检查文档文件并加入 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
要点如下:
- 构造函数接收
Name与Context,必须原样转发给ClangTidyCheck构造函数;其中Context是访问选项(Options)、诊断报告等全局能力的总入口。 isLanguageVersionSupported(可选覆写)用于限定检查生效的语言与标准版本;add_new_check.py的--language/--standard参数正是生成该函数返回表达式(如 C 语言为!LangOpts.CPlusPlus,并可按C11、C17、CPlusPlus11…… 追加条件,可对照脚本中cpp_language_to_requirements与c_language_to_requirements两张映射表)。- 若需在 AST 层分析,覆写
registerMatchers与check;若需分析预处理器层,则应覆写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 大量使用的支持类,如
StringRef、SmallVector等,见《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 库需要做四处修改:
- 把
#include "../ClangTidyCheck.h"换成#include "../utils/TransformerClangTidyCheck.h"; - 把检查的基类从
ClangTidyCheck改为TransformerClangTidyCheck; - 删除类中对
registerMatchers与check的覆写; - 编写一个创建
RewriteRule的函数,并在构造函数中把它传给TransformerClangTidyCheck的构造函数。
从仓库源码 TransformerClangTidyCheck.h 可以看到:该基类同时覆写了 registerPPCallbacks、registerMatchers(final)与 check(final),把 matcher/check 生命周期内部化;它还识别一个名为 IncludeStyle 的 clang-tidy 选项(取值 llvm 或 google,默认 llvm),影响规范头文件的区分方式。规则中每个 case 都必须带非空 Explanation,因为它同时充当诊断文案。
5.3 增量式开发流程与 clang-query 原型验证
推荐从简单用例出发、逐步增加复杂度,add_new_check.py 生成的测试文件正是起点。大致流程:
- 为检查编写一个测试用例;
- 用 clang-query 在测试文件上原型化 matcher;
- 把验证过的 matcher 固化进
registerMatchers; - 在
check中上报所需诊断与 fix-it; - 为用例补充
CHECK-MESSAGES与CHECK-FIXES注释以验证诊断与修复; - 构建
check-clang-toolstarget 确认测试通过; - 循环往复,直到检查的所有方面都有测试覆盖。
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-FIXES 或 CHECK-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-NAME 与 CHECK-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-later。add_new_check.py 生成的骨架默认使用 -or-later 形式。除非测试期望只在特定标准版本下出现的行为,否则优先写成 -std=<最低版本>-or-later。
8.4 高频陷阱:宏与模板
C++ 语言存在许多"暗角",要让检查(尤其带 fix-it 的)在所有情况下都完美并不容易,最常见的两类陷阱是宏与模板:
- 写在宏体/模板定义中的代码,其含义可能随宏展开/模板实例化而改变;
- 多次宏展开/模板实例化可能让同一段代码被检查多次(含义还可能不同),同一条警告可能被重复上报;clang-tidy 会去重完全相同的警告,但若警告稍有差异,全部都会展示给用户(并用于应用修复);
- 对宏体/模板定义做替换,对某些宏展开/模板实例化也许没问题,但很容易破坏另一些展开/实例化。
若需要多个文件来覆盖检查的各个方面,建议放进该模块 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_library 或 llvm_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 目录下各模块的现成检查对,为上述每一步都提供了可直接对照、可逐行研读的真实实现样例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00