10 个版本
| 0.3.0 | 2024 年 3 月 8 日 |
|---|---|
| 0.2.5 | 2023 年 12 月 1 日 |
| 0.2.4 | 2023 年 10 月 6 日 |
| 0.2.1 | 2023 年 5 月 21 日 |
| 0.1.7 | 2023 年 3 月 31 日 |
#695 in 解析器实现
4.5MB
1.5K SLoC
run-clang-tidy
CLI 应用程序,用于对一组文件运行 clang-tidy,这些文件使用 .clang-tidy 文件中的 globs 指定。
快速入门
执行此操作的最低命令如下
$ run-clang-tidy path/to/tidy.json
执行 run-clang-tidy --help 以获取更多详细信息,或 run-clang-tidy schema 以获取配置文件的完整模式描述。
对急于使用用户的提示
- 除非在配置文件中更改设置 (在配置文件中),否则将排除隐藏的路径和文件。
- 此工具假设您已安装
clang-tidy并将其添加到您的路径中。您可以在 配置文件 中指定命令,或作为 命令行参数。 - 可以使用 glob 或 Unix 风格路径语法 指定路径。
- 如果指定了
-j选项,则分析将 并行执行。 - 如果您的应用程序无法编译,则分析将失败。请熟悉
clang-tidy。 - 大多数现代构建系统都支持生成执行
clang-tidy所需的编译数据库。可以使用cmake创建,或者您也可以像在示例makefile中演示的那样,使用纯make自行构建。 - 阅读构建根和
compile_commands.json获取有关编译数据库的详细信息。
内容
JSON配置文件
此CLI工具的核心是一个.json配置文件,该文件指定了所有应进行分析的文件的位置。我们将使用一个示例文件,逐步构建它以解释各个字段。.json文件的架构也在schema子命令(执行run-clang-tidy schema)中进行了记录。要开始,我们创建一个包含空对象的空.json文件。
{
}
添加路径
此配置文件中唯一真正需要的字段是paths和buildRoot。路径必须始终在配置文件中指定,而构建根文件夹可以作为命令行参数提供。有关构建根目录的必要信息将在构建根和compile_commands.json部分提供。
目前我们正在查看路径:**paths字段包含相对于** tidy.json **配置文件父目录的路径或** globs。考虑以下文件夹结构
ProjectRoot
├── .clang-tidy
│
├── _bld_
│ └── compile_commands.json
│
├── Some
│ └── Path
│ ├── header h
│ └── source.c
│
└── Settings
├── tidy.json
└── <...>
在tidy.json配置文件中,两个文件和构建根目录的路径需要指定如下
{
"paths": [
"../Some/Path/header h",
"../Some/Path/source.c",
],
"buildRoot": "../_bld"
}
备注:此工具是为软件开发人员制作的,因此任何用户都应该知道路径本身可能相当复杂:考虑链接,加入字符编码,你就能明白。所以,如果任何人在他们的路径中使用表情符号或其他超现实的东西,他们可以在出现问题时为此存储库做出贡献,但并不是所有场景都能或将被测试。
显然,没有人想手动指定所有路径,这就是为什么此工具支持使用Unix风格的**globs**。以下模式都将解析为相同的路径,但仅提供参考
{
"paths": [
"../**/*.[ch]",
"../Some/*/*.*",
],
"buildRoot": "../_bld"
}
假设您已安装 clang-tidy 并在源代码的一个父目录中(例如,在 项目根目录)有一个 .clang-tidy 文件,那么您只需要做以下操作:
$ run-clang-tidy path/to/tidy.json
请注意,工具的工作目录无关紧要,因为所有路径都是相对于提供的 tidy.json 指定的。目前,您需要了解的就是这些,我们将在稍后详细介绍支持的场景,并继续探索 .json 文件中的配置选项。
构建根和compile_commands.json
现代构建系统(如 cmake 或 clang)支持生成 编译数据库。这个编译数据库就是一个 .json 文件(通常命名为 compile_commands.json),它位于构建的根目录中,包含构建过程中所有调用的命令及其所有参数。
备注:任何静态分析器都需要知道所有编译选项和要分析的文件。因此,在尝试分析代码之前,确保您的代码库可构建非常重要,尽管
clang-tidy是更宽容的工具之一。
备注:如果您使用的是传统的
make,可以在 示例 makefile 中看到生成此文件的简单方法,针对的目标是build-data。
此编译数据库是 clang-tidy 的主要输入。因此,必须通过配置文件中的 buildRoot 或命令行参数 --build-root 指定包含此文件的文件夹的路径。如果指定在配置文件中,则路径相对于配置文件解析。作为命令行参数,必须提供对 run-clang-tidy 调用的绝对或相对路径。
Glob-和路径语法
此工具使用 globset Rust 库解析 glob。因此,它也依赖于其 语法。在此我们借用解释。当使用 glob 时,支持 标准的 Unix-style glob 语法。
?匹配任意单个字符。它不匹配路径分隔符。*匹配零个或多个字符,但不跨目录边界匹配,即不匹配路径分隔符。您必须使用**来实现这一点。**递归匹配目录,如果使用不带路径分隔符,则表示“匹配所有内容”。{a,b}匹配a或b,其中a和b是任意的 glob 模式。不支持嵌套{...}。[ab]匹配a或b,其中a和b是 字符。使用[!ab]来匹配除a和b之外的任何字符。- 元字符如
*和?可以使用字符类表示法进行转义。例如,[*]匹配*。 - 反斜杠
\将在 glob 中转义所有元字符,但由于 glob 定义在.json配置文件中,因此必须指定为双反斜杠\\。如果它前面是非元字符,则斜杠将被忽略。
对于 Windows 路径,所有 glob 都不区分大小写。
注意:由于在
.json文件中必须转义反斜杠,并且 glob 中的反斜杠的行为取决于后面的字符是否是元字符,因此强烈建议在所有平台上使用正斜杠/作为路径分隔符。在 Windows 上,可以使用\\作为路径分隔符,但前提是它后面不是元字符。
预过滤
默认情况下,此工具将 排除 所有隐藏文件和文件夹,其搜索。此行为可以通过字段 filterPre 进行配置。此字段设置了一个在递归搜索文件时应用的过滤器,因此是在将文件与字段 paths 中提供的 glob 进行匹配之前进行的。此类过滤器的典型模式是排除由版本控制系统使用的文件夹,例如 .git(或 .svn)文件夹。
对于此字段,您仍然可以使用 glob,但请注意,此类过滤器也应用于目录,因此如果过滤器匹配,则目录甚至不会被搜索,这使得使用,例如 ** 后的名字变得不必要。以下示例显示了一个配置为排除 .git 文件夹中的所有文件,并且也排除所有隐藏文件和目录的预过滤器。
{
"paths": [
"../**/*.[ch]",
"../Some/*/*.*",
],
"filterPre": ["**/.git", ".*"],
"buildRoot": "../_bld"
}
如果不应跳过任何隐藏文件夹,只需将此字段设置为空列表 []。
后过滤
使用前面的配置文件,我们匹配了所有文件和文件夹,除了隐藏文件。有时,在匹配所有路径之后应用过滤器 之后 也很有用,例如,排除多次出现的特定文件名,或简化 paths 字段中的模式。这可以通过 filterPost 实现
{
"paths": [
"../**/*.[ch]",
"../Some/*/*.*",
],
"filterPre": ["**/.git", ".*"],
"filterPost": ["FreeRTOS.h", "**/Hal*/**"],
"buildRoot": "../_bld"
}
在上面的示例中,任何路径中的 Hal* 文件夹都将被过滤,而无需为 paths 创建复杂的 glob。
指定一个.clang-tidy文件和一个根目录
如果在您的项目根目录中没有放置 .clang-tidy 文件(假设存在),则执行 run-clang-tidy 而不带任何额外的命令行参数(下文解释)不会产生期望的结果——相反,因为 clang-tidy 会检查任何根文件夹,直到可能遇到 .clang-tidy 文件。因此,配置文件允许使用 tidyFile 字段指定 tidy 文件,并使用 tidyRoot 指定所有路径的公共根目录
ProjectRoot
├── _bld_
│ └── compile_commands.json
│
├── Some
│ └── Path
│ ├── header.h
│ └── source.c
│
└── Settings
├── tidy.json
├── tidy.clang-tidy
└── <...>
{
"paths": [
"../**/*.[ch]",
"../Some/*/*.*",
],
"filterPre": ["**/.git", ".*"],
"filterPost": ["FreeRTOS.h", "**/Hal*/**"],
"tidyFile": "./tidy.clang-tidy",
"tidyRoot": "../",
"buildRoot": "../_bld"
}
tidyFile 的名称 或扩展名 必须是 .clang-tidy。这允许您在同一个目录中存储多个 .clang-tidy 文件,例如,driver.clang-tidy 和 application.clang-tidy。
在分析文件时,run-clang-tidy 将
- 将提供的 tidy 文件复制到指定的根目录(如果需要,重命名为
.clang-tidy), - 对所有解析路径执行
clang-tidy, - 最后删除临时文件。
只有当您终止工具的执行(例如,通过 CTRL+C)时,它才无法删除临时文件。
注意:指定根目录是必要的,因为无法确定所有路径的共同分母。此外,终止工具的执行将防止删除临时文件,因此可能会使您的开发空间混乱,因为添加新的 glob 或路径可能会导致不同的根目录。
注意:工具将检查在
tidyRoot中是否存在 内容不同的.clang-tidy文件——如果存在,将终止并显示错误。如果内容匹配,则工具不会复制或删除任何文件,并且就像没有指定tidyRoot和tidyFile一样执行。
如果提供,则配置文件中的 tidyFile 将由 --tidy 命令行参数替换。
指定clang-tidy命令
默认情况下,工具尝试使用 clang-tidy 命令来分析所有解析路径。如果此命令不在您的路径中,或者您使用不同的名称(例如,clang-tidy-10)作为可执行文件,则您需要通过命令行参数 --command 或使用配置文件中的 command 字段指定命令或可执行文件的完整路径
{
"paths": [
"../**/*.[ch]",
"../Some/*/*.*",
],
"filterPre": ["**/.git", ".*"],
"filterPost": ["FreeRTOS.h", "**/Hal*/**"],
"tidyFile": "./tidy.clang-tidy",
"tidyRoot": "../",
"buildRoot": "../_bld",
"command": "/path/to/clang-tidy"
}
与 'paths' 字段中的模式不同,命令可以指定为相对于配置文件的路径、绝对路径或简单的可执行文件名。
与 tidyFile 字段类似,如果提供,则此配置将由 --command 命令行参数替换。当指定为命令行参数的相对路径时,路径相对于当前 工作目录 解析。
注意:确保您的 tidy 文件与您使用的
clang-tidy版本兼容非常重要。这是为什么clang-tidy不与该工具一起安装的主要原因。
注意:配置文件旨在跨平台。因此,允许省略
clang-tidy可执行文件的.exe扩展名。这也适用于--command参数。
命令行参数
所有可用的命令行参数都应该通过工具本身充分描述,当提供以下选项时 -h, --help, help。此外,可以通过使用 schema 子命令显示配置文件的 JSON 架构。此 JSON 架构还包含上述描述的每个选项的说明。
$ run-clang-tidy --help
$ run-clang-tidy help
$ run-clang-tidy schema
以下简要描述了最重要的选项。
详细程度和--quiet
最好通过使用 -v 选项来配置详细程度。
-v是默认选项;工具将提供带有进度条的“美化打印”输出(由 rust crate indicatif 实现)。
“美化”输出仅在
-v日志级别时可用,对于任何其他日志级别,工具将切换到调试风格输出。此类输出不适用于重定向到文件,因为进度条将重写前面的行。请改用-vv调试选项。
-
-vv切换到“调试”日志级别,提供时间戳和纯顺序输出:不覆盖任何行,每条消息都记录在新行上。 -
-vvv及以上切换到“跟踪”日志级别,可能包含更多(可能无关紧要)的消息。这主要用于在发现问题时调试工具。
要关闭所有类型的输出(除了错误消息),请使用 --quiet 选项。这将覆盖 --verbosity 级别。
加快执行速度
默认情况下,工具将逐个处理每个解析的路径。对于大型项目来说,这可能会相当慢。命令行选项 -j, --jobs 允许指定分析应使用的作业数量。
- 如果没有指定值,例如,
run-clang-tidy tidy.json -j,则将使用所有可用的逻辑核心进行分析。 - 如果指定了值,例如,
run-clang-tidy tidy.json -j 3,则工具将只启动指定数量的作业。
注意:在较慢的机器上,当以正常日志级别执行时,进度条可能会闪烁,因为终端可能无法足够快地重绘新行。目前还没有解决办法。
指定替代tidy文件和命令
命令行选项 --tidy 和 --command 允许指定 .clang-tidy 文件和用于执行 clang-tidy 的命令。请参阅 .json 配置文件中关于 字段 tidyFile 和 command 的描述。
注意:指定
--tidy需要配置字段tidyRoot。
指定替代构建根
包含编译数据库的 构建根 通常不是固定的;每次构建可能使用不同的输出文件夹,工具可能安装在不同的目录中(例如,作为 CI 链的一部分执行)。
因此,命令行选项 --build-root 允许在调用此脚本时指定构建目录,覆盖例如配置文件 .json 中指定的默认目录。
抑制警告
默认情况下,clang-tidy 生成的警告会在每次运行时输出,除非使用命令行选项 --suppress-warnings。
注意:
clang-tidy的警告不会影响run-clang-tidy的返回码,无论这些警告是否包含在输出中。使用您的.clang-tidy文件将警告转换为错误,例如通过指定WarningsAsErrors。
应用修复
对于某些检查,clang-tidy 支持使用 -fix 选项应用修复。此包装器的命令行选项 --fix 启用 -fix 和 -fix-errors,以确保始终应用修复。
注意: 使用
-fix-errors还确保编译器 警告(例如烦人的 "system-header" 警告)不会阻止clang-tidy应用修复。
如果在 clang-tidy 发现问题并应用修复后,执行仍然会报告失败的执行。您需要再次执行 clang-tidy 以确保没有更多的发现或需要应用的修复。
请注意,可能需要多次运行带有 --fix 选项的 clang-tidy 才能真正修复问题,因为运行 clang-tidy 只应用一次修复的 "迭代"。例如,在测试文件 module_fix.h 中的以下定义仅在第三次执行后报告成功。
// This macro triggers "bugprone-macro-parentheses", which is fixable.
#define MODULE_FIX_EXPRESSION(a, b) a + b
// The first execution with '--fix' applies the following fix, which is still not correct.
#define MODULE_FIX_EXPRESSION(a, b) (a + b)
// A second execution with '--fix' applies the following fix. This second execution
// still reports an exit code != 0, since "bugprone-macro-parentheses" was still detected.
#define MODULE_FIX_EXPRESSION(a, b) ((a) + (b))
// Only after the third execution clang-tidy reports success.
用例
由于该工具的特性,即底层 clang 工具,当执行 clang-format 时,使用案例非常相似,对于 clang-format 存在一个专门的包装器。请参阅 run-clang-format 文档中的相应部分。
陷阱
多个.clang-tidy文件
如果不同文件夹中存在其他 .clang-tidy 文件,.clang-tidy 将始终使用在从要分析文件返回时找到的第一个文件。例如,对于 source.c,将使用文件 ProjectRoot/Some/.clang-tidy。
ProjectRoot
│
├── Some
│ ├── .clang-tidy
│ └── Path
│ ├── header.h
│ └── source.c
│
└── .clang-tidy
当使用以下配置执行工具时,Some/Path 中的文件将使用 Some/.clang-tidy 分析,而不是配置的 tidy 文件,因为此工具不会扫描任何路径以查找现有的 .clang-tidy 文件。
{
"paths": [
"./Some/**/*.[ch]",
],
"tidyFile": ".clang-tidy",
"tidyRoot": "../"
}
clang-tidy持续分析被排除的头文件
在使用 clang-tidy 分析源文件时似乎存在一些问题,因为它也会部分分析包含的头文件。例如,假设存在以下头文件 module_untidy.h 并使用过滤器或 glob 被排除在分析之外,即 clang-tidy 永远不会为头文件 module_untidy.h 调用。
#pragma once
// This definition violates the
// readability-uppercase-literal-suffix rule
// The `u` suffix of `2uL` must be uppercase, see
// https://clang.llvm.net.cn/extra/clang-tidy/checks/readability-uppercase-literal-suffix.html
#define MODULE_UNTIDY_SMTH 2uL
此头文件可能在以下 main.c 文件中使用,该文件将执行 clang-tidy。在这里,由于 MODULE_UNTIDY_SMTH 在 main.c 内部展开,clang-tidy 仍然会生成错误。
#include "module_untidy.h"
int main (int argc, const char *argv[]) // NOLINT : unused argument argv
{
// MODULE_UNTIDY_SMTH is expanded here and violates
// readability-uppercase-literal-suffix rule
unsigned int some_variable = MODULE_UNTIDY_SMTH;
return (int) some_variable;
}
对于这种特定的用法,即宏在一个分析文件中展开,除了在你的代码中添加 NOLINT 排除(请参阅有关抑制不期望的诊断的文档)外,没有其他方法可以告诉 clang-tidy 忽略此发现。
对于其他场景,可能只需要从你的 .clang-tidy 配置文件中 排除 HeaderFilterRegex 设置:此包装器运行 clang-tidy 而不带 -header-filter 参数,因此除非在 .clang-tidy 文件中指定,否则它将不会使用。另外,请注意,clang-tidy 在其正则表达式中使用 posix ERE 风味。
某些文件似乎没有正确分析
当使用 globs 添加路径时,run-clang-tidy 将解析 globs 为它找到的所有文件,无论它们是否是构建的一部分(请参阅 compile_commands.json)或不是。因此,它将为不属于构建的文件调用 clang-tidy。这有两个影响:
-
clang-tidy仍会尝试编译文件,即使它们不是构建的一部分。如果文件无法编译(例如,由于缺少包含路径或函数声明和函数定义的类型冲突),则会产生错误。 -
对于可以成功编译的文件,由于
clang-tidy会跳过实际的分析步骤,因此不会生成错误或警告。这些文件仍会在run-clang-tidy的执行中被列出。
备注:您可以通过不排除此工具测试中的 module_unused.h 来重现此问题。此头文件在构建中未使用,并且
e_module_unused_a_enum违反了.clang-tidy文件中定义的命名约定。
这可能在 run-clang-tidy 的未来版本中修复,即工具可能会搜索编译数据库,并尝试找出哪些文件真正是构建的一部分。如果 globs 解析到不属于构建的文件,它应该生成错误(或忽略它,请参阅以下路线图)。
路线图
-
可能添加了一个
--fix命令行选项,允许使用-fix选项调用clang-tidy:clang-tidy能够通过自身修复一些违规行为,使用此命令行选项将指示clang-tidy在分析时就地修复文件。 -
更新默认行为或添加一个
--strict选项,以避免为不属于编译数据库的文件运行分析。
依赖关系
~14–26MB
~385K SLoC