#config-file #json-file #json-configuration #file-path #json-path #root-directory #clang-tidy

bin+lib run-clang-tidy

跨平台 CLI 封装,用于执行基于 JSON 输入文件中指定的路径或通配符的 clang-tidy 命令

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

Rust 1.5K SLoC // 0.1% comments Python 111 SLoC // 0.1% comments C 84 SLoC // 0.5% comments Shell 50 SLoC // 0.3% comments Batch 13 SLoC

run-clang-tidy

Build status

CLI 应用程序,用于对一组文件运行 clang-tidy,这些文件使用 .clang-tidy 文件中的 globs 指定。

快速入门

执行此操作的最低命令如下

$ run-clang-tidy path/to/tidy.json

执行 run-clang-tidy --help 以获取更多详细信息,或 run-clang-tidy schema 以获取配置文件的完整模式描述。

对急于使用用户的提示

内容

JSON配置文件

此CLI工具的核心是一个.json配置文件,该文件指定了所有应进行分析的文件的位置。我们将使用一个示例文件,逐步构建它以解释各个字段。.json文件的架构也在schema子命令(执行run-clang-tidy schema)中进行了记录。要开始,我们创建一个包含空对象的空.json文件。

{
}

添加路径

此配置文件中唯一真正需要的字段是pathsbuildRoot。路径必须始终在配置文件中指定,而构建根文件夹可以作为命令行参数提供。有关构建根目录的必要信息将在构建根和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

现代构建系统(如 cmakeclang)支持生成 编译数据库。这个编译数据库就是一个 .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} 匹配 ab,其中 ab 是任意的 glob 模式。不支持嵌套 {...}
  • [ab] 匹配 ab,其中 ab字符。使用 [!ab] 来匹配除 ab 之外的任何字符。
  • 元字符如 *? 可以使用字符类表示法进行转义。例如,[*] 匹配 *
  • 反斜杠 \ 将在 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-tidyapplication.clang-tidy

在分析文件时,run-clang-tidy

  • 将提供的 tidy 文件复制到指定的根目录(如果需要,重命名为 .clang-tidy),
  • 对所有解析路径执行 clang-tidy
  • 最后删除临时文件。

只有当您终止工具的执行(例如,通过 CTRL+C)时,它才无法删除临时文件。

注意:指定根目录是必要的,因为无法确定所有路径的共同分母。此外,终止工具的执行将防止删除临时文件,因此可能会使您的开发空间混乱,因为添加新的 glob 或路径可能会导致不同的根目录。

注意:工具将检查在 tidyRoot 中是否存在 内容不同的 .clang-tidy 文件——如果存在,将终止并显示错误。如果内容匹配,则工具不会复制或删除任何文件,并且就像没有指定 tidyRoottidyFile 一样执行。

如果提供,则配置文件中的 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 配置文件中关于 字段 tidyFilecommand 的描述。

注意:指定 --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_SMTHmain.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-tidyclang-tidy 能够通过自身修复一些违规行为,使用此命令行选项将指示 clang-tidy 在分析时就地修复文件。

  • 更新默认行为或添加一个 --strict 选项,以避免为不属于编译数据库的文件运行分析。

依赖关系

~14–26MB
~385K SLoC