clang-tidy infrastructure
Table of Contents
1. Summary
clang-tidy is the static analyser of the LLVM toolchain. clang-format sets
the layout of the code; clang-tidy reads its meaning. The one check the
project runs today is misc-include-cleaner: every file includes the header
for each symbol it uses, and no header it does not use. A run is on demand,
outside the regular build, through
build/scripts/check_includes.sh, so a
normal build never pays for it. A finding in a generated file is fixed in its
codegen template, never in the file. Return to
Knowledge.
2. Detail
2.1. What it is for
A file that uses a symbol without its header compiles only while some other
header brings that symbol in. When that other header changes, the file stops
compiling. The compilers do not agree on which headers they pull in, so the
fault shows on one platform only: std::to_string without <string> builds
with libstdc++ and fails with MSVC. An unused include costs build time and
hides the real dependencies of a file.
misc-include-cleaner finds both faults. It reports a symbol with no direct
include ("no header providing X is directly included") and an include that
provides no symbol ("included header X is not used directly").
The broader check set in .clang-tidy at the repository root
(bugprone-*, modernize-* and others) is not run yet. A run with that set
produces thousands of findings, so it needs a curated introduction of its
own.
2.2. How to run it
The script reads a preset's compile database and runs clang-tidy over every translation unit in it:
./build/scripts/check_includes.sh linux-clang-debug-ninja
The preset defaults to linux-clang-debug-ninja, the one the nightly
workflow builds. The configure step must have run first, because the script
reads build/output/<preset>/compile_commands.json and it compiles nothing.
The file list comes from the compile database and not from find, so a
source the preset does not build is skipped instead of failing the run.
To fix the findings rather than report them, pass --fix. The nightly
include workflow uses that mode and opens a pull request with the result.
Parallelism is capped at four processes, because clang-tidy serialises fix
application on shared files.
Generated files are skipped and named. Their includes come from the archetype
templates under projects/ores.codegen/library/templates/, so a fix applied
to the checked-in output is overwritten by the next regeneration and rejected
by the codegen drift gate.
2.3. Generated code
Most C++ in the tree is generated, and the drift gate fails on any edit to a
generated file. A finding in a file with the AUTO-GENERATED marker is
therefore a defect in its template:
- An include that every file of a template needs goes into the template unconditionally.
- An include that some files need goes behind a context flag that
core.pycomputes. The flag mirrors the template branches that spell the symbol, so the include appears exactly when one of them is rendered. - A header the check cannot see as used, such as one that only specialises a
trait the file instantiates, keeps its include with
// IWYU pragma: keep.and a comment that says why.
After a template change, regenerate the components and run the check again; it is the test of the change.
2.4. Known noise
A translation unit built with a precompiled header can confuse the check: it
suggests cmake_pch.hxx as an include and reports used headers as unused.
Where that happens, check the affected component with the precompiled header
turned off, and treat the finding as noise until then.
2.5. Internal headers
The check suggests the header that defines a symbol, which is not always the
header to include. A template whose definitions live in an _impl.hpp beside
its public header is the common case: the check named
domain_service_runner_impl.hpp instead of domain_service_runner.hpp, and
the call that relied on a default argument stated only in the public header
no longer compiled. Two guards stop it:
- An internal header of ours names its public header with
// IWYU pragma: private, include "<public header>", which the check honours. - A library header that cannot be marked keeps its include with
// IWYU pragma: keep.and a comment that says why.
A fix the tool proposes is a proposal: build it before it merges.
2.6. Why not a CMake target
Three ways to run clang-tidy from CMake were weighed, and the project uses none of them. One script that reads the compile database runs the check on request, leaves a normal build unchanged, and gives the check one home.
CMAKE_CXX_CLANG_TIDY. CMake runs clang-tidy beside the compile of each translation unit. The build graph schedules the work, so the-jof the build applies and only changed files are checked again. The cost decided against it: clang-tidy becomes part of each compile rule, so turning it on, or changing the checks, recompiles the whole tree, and every following compile is slower. It is the tool for a build that always lints, such as a dedicated CI preset, not for a run on request.- A tidy target per CMake target. Each library gets a
<target>_clang-tidytarget with one command per source file, and an aggregate target depends on all of them. Qt's<target>_qmllinttargets take this shape. The build graph schedules the targets, so the-jof the build applies across them, but the files inside one target run one after another, and every run checks every file again.ores.refdata.corehas about a thousand files, so its target would hold up the end of each run. The CMake maintainers state that CMake has no built-in separate analysis target (CMake Discourse). - One command that reads the compile database. A script or a driver such
as LLVM's
run-clang-tidyruns clang-tidy over the files in the database in parallel, filters them with--header-filter, and applies fixes safely. Nothing compiles, and a normal build is unchanged. This is the shape the project uses, throughbuild/scripts/check_includes.sh.
3. See also
- C++ code style: clang-format configuration — the layout half of the same toolchain.
- CI workflows and local checks — the nightly include workflow among the other checks.
- ores.codegen — the generator whose templates own the generated files.
- CMake setup — presets, the build directory and
compass build. - clang-tidy documentation and the misc-include-cleaner check.