Static Analysis Overview
CMake provides built-in support for running static analysis tools alongside compilation. The CMAKE_<LANG>_CLANG_TIDY, CMAKE_<LANG>_CPPCHECK, and CMAKE_<LANG>_INCLUDE_WHAT_YOU_USE variables run the corresponding tools on every source file during the build — no extra targets needed.
<LANG>_CLANG_TIDY and related properties execute the tool by prepending it to the compiler command. The analyzer sees the exact same flags the compiler uses, ensuring consistent diagnostics without maintaining a separate compilation database.
flowchart TD
A[Source Files] --> B{CMake Build}
B --> C[Compile]
B --> D[clang-tidy]
B --> E[cppcheck]
B --> F[IWYU]
C --> G[Object Files]
D --> H[Static Analysis Report]
E --> H
F --> I[Include Suggestions]
G --> J[Link]
J --> K[Binary]
K --> L{Run Tests}
L --> M[ASan/TSan/UBSan]
L --> N[Coverage via gcov]
M --> O[Runtime Report]
N --> P[Coverage Report]
The general approach is to enable analysis tools via CMake variables (globally) or target properties (per-target). Global variables apply to every target in the project, while properties give fine-grained control:
# Global: applies to all targets
set(CMAKE_CXX_CLANG_TIDY "clang-tidy;-checks=-*,readability-*")
# Per-target: overrides global for this target only
set_target_properties(my_lib PROPERTIES
CXX_CLANG_TIDY "clang-tidy;-checks=-*,bugprone-*,performance-*"
)
clang-tidy Integration
clang-tidy is a linter and static analysis framework built on LLVM. It provides hundreds of checks covering code correctness, modernization, readability, and performance. CMake integrates it natively via the CXX_CLANG_TIDY target property.
Setting Up .clang-tidy Configuration
Create a .clang-tidy file at your project root to define which checks to enable:
# .clang-tidy
---
Checks: >
-*,
bugprone-*,
performance-*,
readability-*,
modernize-*,
-modernize-use-trailing-return-type,
cppcoreguidelines-*,
-cppcoreguidelines-avoid-magic-numbers
WarningsAsErrors: 'bugprone-*'
HeaderFilterRegex: '.*'
FormatStyle: file
CheckOptions:
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
CMake Integration
cmake_minimum_required(VERSION 3.21)
project(MyProject LANGUAGES CXX)
# Find clang-tidy on the system
find_program(CLANG_TIDY_EXE NAMES "clang-tidy" REQUIRED)
# Option to enable/disable (off by default to not slow CI)
option(ENABLE_CLANG_TIDY "Run clang-tidy during compilation" OFF)
if(ENABLE_CLANG_TIDY)
# Use the .clang-tidy config file in the project root
set(CMAKE_CXX_CLANG_TIDY
"${CLANG_TIDY_EXE}"
"--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy"
)
message(STATUS "clang-tidy enabled: ${CLANG_TIDY_EXE}")
endif()
add_library(mylib src/mylib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
Per-Target Overrides
# Disable clang-tidy for generated code or third-party wrappers
set_target_properties(third_party_wrapper PROPERTIES
CXX_CLANG_TIDY "" # Empty string disables
)
# Stricter checks for core library
set_target_properties(core_lib PROPERTIES
CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-warnings-as-errors=*"
)
Try It: clang-tidy Modernize Check
Create a project with pre-C++11 code (raw pointers, NULL macros, old-style casts). Enable clang-tidy with modernize-* checks and observe the suggested fixes. Then add -fix to the clang-tidy command to auto-apply them.
cppcheck Integration
cppcheck is an open-source static analysis tool focused on finding undefined behavior, memory leaks, and coding errors that compilers miss. CMake supports it via CMAKE_CXX_CPPCHECK.
find_program(CPPCHECK_EXE NAMES "cppcheck")
option(ENABLE_CPPCHECK "Run cppcheck during compilation" OFF)
if(ENABLE_CPPCHECK AND CPPCHECK_EXE)
set(CMAKE_CXX_CPPCHECK
"${CPPCHECK_EXE}"
"--enable=warning,performance,portability"
"--suppress=missingIncludeSystem"
"--inline-suppr"
"--inconclusive"
"--template=gcc"
"--suppressions-list=${CMAKE_SOURCE_DIR}/cppcheck-suppressions.txt"
)
message(STATUS "cppcheck enabled: ${CPPCHECK_EXE}")
endif()
Suppression File
# cppcheck-suppressions.txt
# Suppress false positives in third-party headers
unmatchedSuppression
missingIncludeSystem
# Suppress specific warning in generated code
unusedFunction:*build/generated/*
CI Integration with XML Output
# Run cppcheck standalone for CI reporting
cppcheck --project=build/compile_commands.json \
--enable=all \
--xml \
--output-file=cppcheck-report.xml \
--suppress=missingIncludeSystem
# Convert to HTML for archiving
cppcheck-htmlreport --source-dir=. \
--title="MyProject" \
--file=cppcheck-report.xml \
--report-dir=cppcheck-html
include-what-you-use
include-what-you-use (IWYU) analyzes #include directives and reports which headers are unnecessary or missing. Removing unused includes reduces compilation time and makes dependencies explicit. CMake integrates IWYU via CXX_INCLUDE_WHAT_YOU_USE.
find_program(IWYU_EXE NAMES "include-what-you-use" "iwyu")
option(ENABLE_IWYU "Run include-what-you-use during compilation" OFF)
if(ENABLE_IWYU AND IWYU_EXE)
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE
"${IWYU_EXE}"
"-Xiwyu" "--mapping_file=${CMAKE_SOURCE_DIR}/iwyu.imp"
"-Xiwyu" "--no_fwd_decls"
)
message(STATUS "IWYU enabled: ${IWYU_EXE}")
endif()
Fixing Include Bloat
# Build with IWYU, capture suggestions
cmake --build build 2>&1 | tee iwyu-output.txt
# Auto-apply IWYU suggestions using the fix script
python3 fix_includes.py < iwyu-output.txt
# Or use iwyu_tool.py for parallel analysis
iwyu_tool.py -p build/ -- -Xiwyu --mapping_file=iwyu.imp | \
fix_includes.py --nosafe_headers
Runtime Sanitizers
AddressSanitizer (ASan)
AddressSanitizer detects memory errors at runtime: buffer overflows, use-after-free, double-free, and memory leaks. It requires both compile and link flags. The standard approach in CMake creates a reusable function:
# cmake/Sanitizers.cmake
function(enable_sanitizers target)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
# ASan and TSan are mutually exclusive
if(ENABLE_ASAN AND ENABLE_TSAN)
message(FATAL_ERROR "ASan and TSan cannot be used simultaneously")
endif()
set(SANITIZER_FLAGS "")
if(ENABLE_ASAN)
list(APPEND SANITIZER_FLAGS
-fsanitize=address
-fno-omit-frame-pointer
-fno-optimize-sibling-calls
)
endif()
if(ENABLE_TSAN)
list(APPEND SANITIZER_FLAGS -fsanitize=thread)
endif()
if(ENABLE_UBSAN)
list(APPEND SANITIZER_FLAGS
-fsanitize=undefined
-fno-sanitize-recover=all
)
endif()
if(SANITIZER_FLAGS)
target_compile_options(${target} PRIVATE ${SANITIZER_FLAGS})
target_link_options(${target} PRIVATE ${SANITIZER_FLAGS})
endif()
endif()
endfunction()
Usage with CTest
include(cmake/Sanitizers.cmake)
add_executable(myapp src/main.cpp)
enable_sanitizers(myapp)
# CTest integration: ASan reports non-zero exit on error
enable_testing()
add_test(NAME myapp_test COMMAND myapp)
# Set ASan runtime options via environment
set_tests_properties(myapp_test PROPERTIES
ENVIRONMENT "ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:print_stats=1"
)
# Configure with ASan enabled
cmake -B build -DENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
# Build and run tests
cmake --build build
cd build && ctest --output-on-failure
flowchart TD
A[Bug Type?] --> B{Memory Error?}
B -->|Yes| C[AddressSanitizer]
B -->|No| D{Data Race?}
D -->|Yes| E[ThreadSanitizer]
D -->|No| F{Undefined Behavior?}
F -->|Yes| G[UBSan]
F -->|No| H{Memory Leak Only?}
H -->|Yes| I[LeakSanitizer]
H -->|No| J[Valgrind / Other]
C --> K[Cannot combine with TSan]
E --> K
G --> L[Can combine with ASan or TSan]
ThreadSanitizer (TSan)
ThreadSanitizer detects data races in multi-threaded programs. It cannot be used simultaneously with AddressSanitizer since both tools instrument memory access differently.
# Enable TSan for a specific test target
add_executable(thread_test tests/thread_test.cpp)
target_compile_options(thread_test PRIVATE -fsanitize=thread)
target_link_options(thread_test PRIVATE -fsanitize=thread)
add_test(NAME thread_test COMMAND thread_test)
set_tests_properties(thread_test PROPERTIES
ENVIRONMENT "TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1"
)
TSan Suppression File
# tsan-suppressions.txt
# Suppress known benign races in third-party code
race:third_party::Logger::getInstance
race:boost::detail::spinlock::lock
# Suppress all races in a specific file
file:src/legacy_module.cpp
# Point TSan to suppression file
set_tests_properties(thread_test PROPERTIES
ENVIRONMENT "TSAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/tsan-suppressions.txt"
)
UndefinedBehaviorSanitizer (UBSan)
UBSan catches undefined behavior including signed integer overflow, null pointer dereference, misaligned access, and type confusion. It's lightweight and can often be combined with ASan or TSan.
# UBSan with trap mode (abort on UB, no runtime library needed)
target_compile_options(myapp PRIVATE
-fsanitize=undefined
-fsanitize-trap=all # Trap instead of printing diagnostics
)
target_link_options(myapp PRIVATE -fsanitize=undefined)
# Or with recover mode (print diagnostics, continue execution)
target_compile_options(myapp PRIVATE
-fsanitize=undefined
-fno-sanitize-recover=all # Still abort, but print diagnostics first
)
Try It: Sanitizer CI Matrix
Create a GitHub Actions workflow that runs your test suite three times: once with ASan, once with TSan, and once with UBSan. Each job should use the -DENABLE_*=ON option and fail the build if any sanitizer reports an error. Track which bugs each sanitizer catches uniquely.
Code Coverage
gcov and lcov
Code coverage measures which lines of code your tests actually execute. GCC's gcov collects raw data, and lcov produces HTML reports. The CMake setup requires specific compile flags:
option(ENABLE_COVERAGE "Enable code coverage reporting" OFF)
if(ENABLE_COVERAGE)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
message(WARNING "Coverage results are best with Debug builds")
endif()
# GCC/Clang coverage flags
add_compile_options(--coverage -fprofile-arcs -ftest-coverage)
add_link_options(--coverage)
# Create a coverage target
find_program(LCOV_EXE lcov REQUIRED)
find_program(GENHTML_EXE genhtml REQUIRED)
add_custom_target(coverage
COMMAND ${LCOV_EXE} --directory . --zerocounters
COMMAND ctest --test-dir ${CMAKE_BINARY_DIR} --output-on-failure
COMMAND ${LCOV_EXE} --directory . --capture
--output-file coverage.info
COMMAND ${LCOV_EXE} --remove coverage.info
'/usr/*' '*/test/*' '*/third_party/*'
--output-file coverage.info.cleaned
COMMAND ${GENHTML_EXE} coverage.info.cleaned
--output-directory coverage-report
--title "${PROJECT_NAME} Coverage"
--legend --show-details
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating code coverage report..."
)
endif()
# Build with coverage, run tests, generate report
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
cmake --build build
cmake --build build --target coverage
# Open the report
open build/coverage-report/index.html
gcovr for CI Integration
gcovr is a Python tool that generates coverage reports in multiple formats (XML/Cobertura, JSON, HTML) — ideal for CI systems that consume Cobertura XML:
find_program(GCOVR_EXE gcovr)
if(ENABLE_COVERAGE AND GCOVR_EXE)
add_custom_target(coverage-xml
COMMAND ctest --test-dir ${CMAKE_BINARY_DIR} --output-on-failure
COMMAND ${GCOVR_EXE}
--root ${CMAKE_SOURCE_DIR}
--filter ${CMAKE_SOURCE_DIR}/src
--exclude ${CMAKE_SOURCE_DIR}/test
--xml-pretty
--output coverage.xml
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating Cobertura XML coverage report..."
)
endif()
clang-format Integration
Custom Formatting Targets
While clang-format isn't a compile-time tool, CMake can create custom targets that format all source files in the project:
find_program(CLANG_FORMAT_EXE NAMES "clang-format")
if(CLANG_FORMAT_EXE)
# Collect all source files
file(GLOB_RECURSE ALL_SOURCE_FILES
${CMAKE_SOURCE_DIR}/src/*.cpp
${CMAKE_SOURCE_DIR}/src/*.h
${CMAKE_SOURCE_DIR}/include/*.h
${CMAKE_SOURCE_DIR}/test/*.cpp
)
# Format target: applies formatting in-place
add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE} -i ${ALL_SOURCE_FILES}
COMMENT "Formatting source files with clang-format..."
)
# Format check target: fails if files need formatting (for CI)
add_custom_target(format-check
COMMAND ${CLANG_FORMAT_EXE} --dry-run --Werror ${ALL_SOURCE_FILES}
COMMENT "Checking source file formatting..."
)
endif()
.clang-format Configuration
# .clang-format
---
Language: Cpp
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
PointerAlignment: Left
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(myproject)/'
Priority: 1
- Regex: '^<'
Priority: 2
- Regex: '.*'
Priority: 3
Pre-Commit Hook Integration
#!/bin/bash
# .git/hooks/pre-commit
# Run clang-format on staged C++ files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|h|hpp)$')
if [ -n "$STAGED_FILES" ]; then
clang-format --dry-run --Werror $STAGED_FILES
if [ $? -ne 0 ]; then
echo "Error: Code formatting issues detected."
echo "Run 'cmake --build build --target format' to fix."
exit 1
fi
fi
Combining All Analysis Tools
Complete Project Template
Here's a production-ready cmake/Analysis.cmake module that combines all tools into a single include:
# cmake/Analysis.cmake — Include once from top-level CMakeLists.txt
include_guard()
# ─── Static Analysis ───────────────────────────────────────────
option(ENABLE_CLANG_TIDY "Enable clang-tidy" OFF)
option(ENABLE_CPPCHECK "Enable cppcheck" OFF)
option(ENABLE_IWYU "Enable include-what-you-use" OFF)
if(ENABLE_CLANG_TIDY)
find_program(CLANG_TIDY_EXE clang-tidy REQUIRED)
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
endif()
if(ENABLE_CPPCHECK)
find_program(CPPCHECK_EXE cppcheck REQUIRED)
set(CMAKE_CXX_CPPCHECK "${CPPCHECK_EXE}"
"--enable=warning,performance" "--inline-suppr")
endif()
if(ENABLE_IWYU)
find_program(IWYU_EXE include-what-you-use REQUIRED)
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "${IWYU_EXE}")
endif()
# ─── Sanitizers ────────────────────────────────────────────────
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
if(ENABLE_ASAN AND ENABLE_TSAN)
message(FATAL_ERROR "ASan and TSan are mutually exclusive")
endif()
set(_SAN_FLAGS "")
if(ENABLE_ASAN)
list(APPEND _SAN_FLAGS -fsanitize=address -fno-omit-frame-pointer)
endif()
if(ENABLE_TSAN)
list(APPEND _SAN_FLAGS -fsanitize=thread)
endif()
if(ENABLE_UBSAN)
list(APPEND _SAN_FLAGS -fsanitize=undefined -fno-sanitize-recover=all)
endif()
if(_SAN_FLAGS)
add_compile_options(${_SAN_FLAGS})
add_link_options(${_SAN_FLAGS})
endif()
# ─── Coverage ─────────────────────────────────────────────────
option(ENABLE_COVERAGE "Enable code coverage" OFF)
if(ENABLE_COVERAGE)
add_compile_options(--coverage)
add_link_options(--coverage)
endif()
# ─── Formatting ───────────────────────────────────────────────
find_program(CLANG_FORMAT_EXE clang-format)
if(CLANG_FORMAT_EXE)
file(GLOB_RECURSE _FMT_SOURCES src/*.cpp src/*.h include/*.h test/*.cpp)
add_custom_target(format COMMAND ${CLANG_FORMAT_EXE} -i ${_FMT_SOURCES})
add_custom_target(format-check
COMMAND ${CLANG_FORMAT_EXE} --dry-run --Werror ${_FMT_SOURCES})
endif()
CI Pipeline Integration
# .github/workflows/analysis.yml (excerpt)
# Each job runs independently for clear failure diagnostics
jobs:
clang-tidy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
cmake -B build -DENABLE_CLANG_TIDY=ON
cmake --build build 2>&1 | tee tidy-output.txt
grep -q "warning:" tidy-output.txt && exit 1 || exit 0
sanitizers:
runs-on: ubuntu-latest
strategy:
matrix:
sanitizer: [ASAN, TSAN, UBSAN]
steps:
- uses: actions/checkout@v4
- run: |
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_${{ matrix.sanitizer }}=ON
cmake --build build
ctest --test-dir build --output-on-failure
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
cmake --build build
cmake --build build --target coverage-xml
- uses: codecov/codecov-action@v4
with:
files: build/coverage.xml
Try It: Full Analysis Pipeline
Create a CMake project that includes the cmake/Analysis.cmake module above. Write a small library with intentional bugs (buffer overflow, data race, unused include). Verify that each tool catches its respective bug by running separate builds with each option enabled.
Conclusion & Next Steps
Program analysis tools transform CMake projects from "compiles successfully" to "verified correct." By integrating static analyzers, sanitizers, coverage, and formatting into your build system, defects are caught early and consistently. The key principles:
- Static analysis (clang-tidy, cppcheck) catches bugs at compile time with zero runtime cost
- Sanitizers (ASan, TSan, UBSan) detect runtime errors that static analysis misses
- Coverage ensures your tests actually exercise the code paths you care about
- Formatting eliminates style debates and keeps diffs focused on logic changes
- CI integration makes these checks mandatory for every commit
<LANG>_CLANG_TIDY, <LANG>_CPPCHECK, and <LANG>_INCLUDE_WHAT_YOU_USE target properties.