Clang-Tidy

clang-tidy is a static analysis tool that detects common programming errors, enforces coding standards, and suggests modern C++ improvements. It runs as part of CI on pull requests targeting main. CI checks eligible changed C/C++ files with --warnings-as-errors='*', so findings fail the clang-tidy job. The workflow currently describes this job as non-required.

This guide explains how to run clang-tidy locally so you can fix issues before pushing.

What clang-tidy checks

FoundationDB configures 55 named checks in the .clang-tidy file at the repository root. The active set depends on the clang-tidy version and can be inspected with clang-tidy --list-checks. The intent is to enable more as we go forward. Here are some example rules:

  • 36 Bugprone rules – catch potential runtime errors, including unsafe self-assignment, forwarding constructors that hide copy or move constructors, narrow accumulation initializers, mismatched argument comments, obvious infinite loops, chained comparisons, swapped arguments, integer division in floating-point calculations, missed base-class copy construction, repeated macro argument evaluation, near-miss virtual overrides, dangling returned references, incorrect erase/remove calls, and incorrect POSIX error checks

  • 1 C++ Core Guidelines rule – catch unsafe captures in coroutine lambdas (cppcoreguidelines-avoid-capturing-lambda-coroutines)

  • 2 Misc rules – catch redundant expressions and RAII objects held across coroutine suspension points

  • 4 Modernize rules – encourage modern C++ practices (e.g., modernize-use-auto, modernize-use-override)

  • 5 Performance rules – avoid unnecessary copies, hidden range-loop conversions, repeated vector growth in simple loops, pointless moves, and move constructors that copy movable members (performance-for-range-copy, performance-implicit-conversion-in-loop, performance-inefficient-vector-operation, performance-move-const-arg, performance-move-constructor-init)

  • 7 Readability rules – improve code clarity (e.g., readability-container-contains, readability-container-size-empty)

misc-coroutine-hostile-raii checks Flow’s blocking MutexHolder and ThreadSpinLockHolder guards as well as std::lock_guard and std::scoped_lock. These guards must leave scope before a coroutine suspension point; asynchronous locks designed to span suspension are not included.

bugprone-unhandled-self-assignment retains its default restriction to types with suspicious fields. Reference-counted assignments that acquire the incoming reference before releasing the old one use documented, check-specific NOLINTNEXTLINE annotations where the checker cannot recognize their safety.

Basic examples of clang-tidy style and performance improvement changes:

// Use empty() instead of size() == 0
-if (str.size() == 0) {
+if (str.empty()) {

// Use contains() instead of count() > 0
-if (classPath.count(path) > 0) {
+if (classPath.contains(path)) {

// Avoid unnecessary copies in range-for loops
-for (auto state : states)
+for (const auto& state : states)

// Remove unnecessary const on return types
-const Key keyServersKey(const KeyRef& k) {
+Key keyServersKey(const KeyRef& k) {

clang-tidy vs Clang-Format

Both run in CI but serve different purposes:

Feature

clang-format (Code Style)

clang-tidy (Code Quality)

Focus

Whitespace, indentation, braces

Bugs, performance, modern practices

Speed

Very fast (syntax-only)

Slower (requires compilation database)

Action

Modifies files in-place (-i)

Reports violations (read-only)

Example

if(x){...} to if (x) { ... }

vec.size() == 0 to vec.empty()

Running clang-tidy locally

Step 1: Generate the compilation database

clang-tidy needs a compile_commands.json file to understand include paths and macros. Add -DCMAKE_EXPORT_COMPILE_COMMANDS=ON when you configure your build:

cmake -B ~/build_output -S . -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

Then symlink it to your source root:

ln -sf ~/build_output/compile_commands.json .

Note

A full build is recommended so that generated headers exist and include paths resolve correctly. Without a build, clang-tidy may report false errors on files that depend on generated code. If your compile_commands.json was generated on a different machine (e.g., Okteto), fix the paths with:

sed -i '' 's|/root/src/foundationdb/|/Users/YOU/checkouts/foundationdb/|g' compile_commands.json

Step 2: Locate the helper script

clang-tidy-diff.py runs clang-tidy only on the lines changed in your diff, avoiding noise from existing code you did not touch.

On macOS (with Homebrew LLVM):

export TIDY_DIFF=$(find $(brew --prefix llvm)/share/clang -name "clang-tidy-diff.py")

On Linux:

export TIDY_DIFF=$(find /usr/lib/llvm-*/share/clang -name "clang-tidy-diff.py" | head -n 1)

To make this permanent, add to your ~/.bashrc or ~/.zshrc:

alias fdb-tidy='python3 $(find $(brew --prefix llvm 2>/dev/null || echo "/usr/lib/llvm-*") -name "clang-tidy-diff.py" | head -n 1) -p 1 -path .'

Step 3: Run against your changes

Check all changes between your branch and main:

git diff -U0 origin/main...HEAD | python3 "$TIDY_DIFF" -p 1 -path .

# Or with the alias:
git diff -U0 origin/main...HEAD | fdb-tidy

Check a specific commit:

git show -U0 <commit_hash> | python3 "$TIDY_DIFF" -p 1 -path .

Important

Use -U0 (no context lines). Without it, clang-tidy may lint surrounding lines that you did not change, reporting pre-existing issues.

Understanding the flags

The clang-tidy-diff.py script has different flag conventions from clang-tidy itself:

  • -p 1 – strip one directory prefix from git diff paths (a/src/main.cpp becomes src/main.cpp)

  • -path . – directory containing compile_commands.json

  • -quiet – suppress per-file warning count summaries

  • -j N – run N clang-tidy instances in parallel

Warning

In the standard clang-tidy command, -p points to the build directory. In clang-tidy-diff.py, -path points to the build directory and -p is for path stripping.

Running clang-tidy during builds (CMake integration)

FoundationDB’s CMake build supports running clang-tidy automatically during C/C++ compilation:

cmake -S . -B build -G Ninja -DUSE_CLANG_TIDY=ON
ninja -C build fdbserver

This runs clang-tidy on every file as it compiles. It is slower than using clang-tidy-diff.py on your diff but catches issues in all compiled code.

Optional CMake variables:

  • CLANG_TIDY – path to the clang-tidy executable (auto-detected by default)

  • CLANG_TIDY_EXTRA_ARGS – additional space-separated arguments passed to clang-tidy

Known limitations

Build-integrated clang-tidy skips bundled external-library targets, including crc32, libb64, md5, libeio, and libcoroutine.

cppcoreguidelines-slicing is not enabled: Standalone<T> inherits T and owns its bytes in an Arena, while passing a borrowed T view by value is an intentional FoundationDB pattern. The check cannot distinguish those borrows from unintended slicing; the borrow must still not outlive its owner.

readability-simplify-boolean-expr is not enabled because it diagnoses ordinary uses of FoundationDB’s ASSERT macro after macro expansion.

Quick reference

git diff -U0 origin/main...HEAD | python3 "$TIDY_DIFF" -p 1 -path .

GCC-built compile_commands.json with clang-tidy. If your compile_commands.json was generated with GCC, it may contain GCC-specific flags that clang-tidy (which uses the clang frontend) does not recognize. Add -extra-arg=-Wno-unknown-warning-option to suppress these errors:

git diff -U0 origin/main...HEAD | python3 "$TIDY_DIFF" -p 1 -path . -extra-arg=-Wno-unknown-warning-option

Quick reference