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
// http://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