Skip to content

Static Analysis involves examining source code without executing it. Unlike the compiler, which Focuses on grammar and binary generation, static analyzers focus on correctness, readability.

In a robust architecture, static analysis is not a manual task performed occasionally; it is a Continuous Pipeline integrated directly into the build system. Following covers the dominant Tools in the ecosystem: clang-tidy, Cppcheck, and enterprise-grade alternatives.

clang-tidy is part of the LLVM project. Because it uses the Clang compiler frontend, it parses The code into a full Abstract Syntax Tree (AST). It understands templates, preprocessor macros, and Type deductions with the exact same precision as the compiler.

  1. Linter: Checks for style violations and legacy patterns (e.g., modernize-use-std-print).
  2. Static Analyzer: Performs path-sensitive analysis to find null pointer dereferences or use-after-free (via the clang-analyzer-* module).
  3. Refactoring Tool: Can automatically apply fixes to the source code.

Configuration is managed via a .clang-tidy file in the project root. This ensures every developer Applies the same rules.

Best Practice C++23 Configuration:

---
# Enable specific checks.
# syntax: +enable, -disable
Checks: >
-*, bugprone-*, cert-*, clang-analyzer-*, concurrency-*, cppcoreguidelines-*, misc-*, modernize-*,
performance-*, readability-*, -modernize-use-trailing-return-type,
-cppcoreguidelines-avoid-magic-numbers
# Treat all warnings as errors in CI
WarningsAsErrors: "*''
# header-filter ensures we analyze headers in our project,
# but ignore headers in third_party/ or /usr/include
HeaderFilterRegex: "src/.*'
FormatStyle: file

False positives are inevitable. Suppressions must be explicit and granular.

// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
auto* raw_data = reinterpret_cast<const byte*>(ptr);
void func() {
int x = 0; // NOLINT(misc-const-correctness) - modified in legacy macro
}

For legacy files that trigger many warnings, block suppression is available since clang-tidy 14:

// NOLINTBEGIN(cert-err58-cpp, cppcoreguidelines-pro-bounds-pointer-arithmetic)
legacy_c_api_function(buffer, size);
process_raw_pointer(ptr);
// NOLINTEND(cert-err58-cpp, cppcoreguidelines-pro-bounds-pointer-arithmetic)

Static analysis tools employ several distinct analysis techniques, each with different precision and Cost characteristics:

The simplest technique. The analyzer walks the AST and matches patterns against known bug or style Violations. This is fast (O(n) in AST size) but can only detect syntactic patterns, not semantic Bugs. Examples: modernize-use-override (detects virtual function overrides without override), bugprone-sizeof-expression (detects sizeof(ptr) instead of sizeof(*ptr)).

Tracks how values flow through the program. At each program point, the analyzer maintains a set of Facts (e.g., “variable x is null,” “buffer buf has been freed”). These facts propagate through Assignments, function calls, and control flow merges. Example: Detecting use of an uninitialized variable:

int x;
if (condition) {
x = 42;
}
// At this point, data flow analysis knows x may be uninitialized
// (fact: x is {uninitialized, 42} on the two paths)
return x; // bugprone: possible use of uninitialized variable

A refinement of data flow analysis that tracks facts per execution path rather than merging them At join points. This is more precise but exponentially more expensive. Example: Distinguishing two paths where a pointer is null vs non-null:

int* p = get_pointer();
if (p == nullptr) {
// Path 1: p is definitely null
log_error("null pointer");
return -1;
}
// Path 2: p is definitely non-null (the null path returned above)
// Path-sensitive analysis knows p != nullptr here
return *p; // no warning

The Clang Static Analyzer (accessed via clang-analyzer-* checks in clang-tidy) uses path-sensitive Analysis based on an exploration engine (symbolic execution with a configurable exploration budget).

Abstract interpretation is a theoretical framework for static analysis. It computes an over-approximation of all possible program behaviors using abstract domains (intervals, signs, Congruences). The analysis is sound: if the analysis does not report a bug, the bug does not exist (within the precision of the abstract domain). Example: Interval analysis for integer overflow:

int f(int x) {
// Abstract domain: x ∈ [-2^31, 2^31 - 1]
// After the check: x ∈ [1, 2^31 - 1]
if (x > 0) {
// x + 1 ∈ [2, 2^31] — may overflow!
return x + 1;
}
return 0;
}

Taint analysis tracks how untrusted data (user input, network data, file content) flows through the Program. If tainted data reaches a security-sensitive operation (SQL query, shell command, memory Allocation size), the analyzer reports a vulnerability. Example: SQL injection detection:

void handle_request(const std::string& user_input) {
// user_input is a taint source
std::string query = "SELECT * FROM users WHERE name = '" + user_input + "'";
// query is now tainted
db.execute(query); // taint sink — potential SQL injection
}

Clang-tidy does not have built-in taint analysis. PVS-Studio and SonarQube provide taint analysis Capabilities for C++.

TechniquePrecisionCostTools Using It
AST Pattern MatchingLowVery Lowclang-tidy (most checks)
Data Flow AnalysisMediumMediumcppcheck, clang-analyzer
Path-SensitiveHighHighclang-analyzer, Coverity
Abstract InterpretationHighVery HighPolyspace, Astrée
Taint AnalysisHighHighPVS-Studio, SonarQube

Cppcheck uses a custom parser, not Clang. While it may struggle with highly complex template Metaprogramming, it excels at Data Flow Analysis. It is often faster than clang-tidy and finds Different classes of bugs, particularly related to buffer overruns, uninitialized variables, and Resource leaks in execution paths that the compiler optimization phase might mask.

Cppcheck uses inline comments for suppression.

// cppcheck-suppress uninitvar
int x;

For project-wide suppression, use a .cppcheck file:

.cppcheck
--enable=all
--suppress=missingIncludeSystem
--suppress=unusedFunction
--std=c++20
-I include/

The architectural standard is to run analysis during compilation. CMake provides native Properties to inject these tools into the build graph.

Add this logic to your root CMakeLists.txt or a cmake/StaticAnalysis.cmake module.

option(ENABLE_STATIC_ANALYSIS "Enable Clang-Tidy and Cppcheck during build" OFF)
if(ENABLE_STATIC_ANALYSIS)
# 1. Find the tools
find_program(CLANG_TIDY_PATH clang-tidy)
find_program(CPPCHECK_PATH cppcheck)
# 2. Configure Clang-Tidy
if(CLANG_TIDY_PATH)
# set CXX_CLANG_TIDY property on all targets created after this line
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_PATH}")
else()
message(WARNING "clang-tidy not found")
endif()
# 3. Configure Cppcheck
if(CPPCHECK_PATH)
# --enable=all: Enable all checks
# --suppress=missingIncludeSystem: Don't fail if system headers are missing
set(CMAKE_CXX_CPPCHECK
"${CPPCHECK_PATH};--enable=all;--suppress=missingIncludeSystem;--error-exitcode=1")
else()
message(WARNING "cppcheck not found")
endif()
endif()
  1. Configure: cmake -S . -B build -DENABLE_STATIC_ANALYSIS=ON
  2. Build: cmake --build build Behavior: CMake will execute clang-tidy and cppcheck on every source file as it is Compiled. If the analyzer reports an error (and WarningsAsErrors is set), the build fails Immediately.

Static analysis is computationally expensive.

  • Debug Builds: Disable analysis to keep iteration times fast.
  • CI/Release Builds: Enable analysis to enforce quality gates.

Sometimes running analysis as part of the build is too slow. You can run clang-tidy independently Using the compile_commands.json database generated in Module 4.1. This allows for parallel execution decoupled from the compiler.

Terminal window
# Analyze the entire project using parallel execution
# -p points to the build directory containing compile_commands.json
run-clang-tidy -p build/

This script (run-clang-tidy) wraps the binary and schedules analysis jobs across all available Cores.

In a Continuous Integration pipeline, the static analysis step serves as a Quality Gate.

  1. Fast Feedback: Build & Test (No Analysis).
  2. Deep Analysis: Static Analysis (Parallel Job).

Since clang-tidy does not natively support caching, running it on the entire codebase for every Commit is wasteful. Integration with CCache: Modern versions of clang-tidy effectively ignore ccache. However, Specialized wrappers or advanced build systems (like Bazel) can cache analysis results. In CMake/Ninja workflows, the standard mitigation is Incremental Analysis:

  • On developer machines: CMake integration only analyzes files that are recompiled.
  • On CI agents: Use git diff to identify changed files and run clang-tidy only on the changeset.
Terminal window
# CI Script Example: Analyze only changed files
git diff --name-only origin/main | grep '\.cpp$' | xargs clang-tidy -p build/
name: Static Analysis
on: [push, pull_request]
jobs:
clang-tidy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install clang-tidy
run: sudo apt install clang-tidy
- name: Configure
run: cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release
- name: Run clang-tidy on changed files
run: |
git diff --name-only origin/main | grep -E '\.(cpp|hpp|h)$' | \
xargs -I{} clang-tidy -p build --warnings-as-errors='*' {}

Clang-Tidy organizes its checks into named modules. Understanding these modules helps you adopt Checks incrementally without overwhelming the build with warnings.

Modernizes code to use contemporary C++ idioms. These are the safest checks to enable first.

CheckWhat it doesExample
modernize-use-std-printReplaces std::cout / printf with std::printstd::cout &lt;&lt; xstd::print("{}", x)
modernize-use-overrideAdds override to virtual function overridesvoid f()void f() override
modernize-use-autoReplaces explicit type with auto where appropriateint x = foo()auto x = foo()
modernize-use-nullptrReplaces NULL / 0 with nullptrNULLnullptr
modernize-use-usingReplaces typedef with usingtypedef int Xusing X = int
modernize-use-enum-classSuggests enum class over unscoped enumenum Colorenum class Color
modernize-loop-convertConverts for loops to range-for where applicableIndex-based → range-based
modernize-use-starts-ends-withReplaces hand-rolled prefix/suffix checkss.find(x) == 0s.starts_with(x)

Catches common mistakes that compilers often miss. These are higher-priority checks.

CheckWhat it detects
bugprone-use-after-moveUsing an object after it has been moved from
bugprone-string-integer-assignmentAssigning integer to std::string (implicit char)
bugprone-narrowing-conversionsImplicit narrowing (e.g., double to int)
bugprone-redundant-branch-conditionSame condition tested twice in an if/else if chain
bugprone-assert-side-effectassert() with side effects (removed in release)
bugprone-sizeof-expressionsizeof(ptr) instead of sizeof(*ptr)
bugprone-integer-divisionInteger division where floating-point was intended

Enforces consistent style. Some are opinionated — disable the ones that conflict with your project’s Conventions.

CheckWhat it does
readability-identifier-namingEnforces naming conventions (camelCase, snake_case)
readability-const-return-typeWarns about unnecessary const on return types
readability-braces-around-statementsRequires braces on single-line if/else bodies
readability-else-after-returnFlags else after return / break / continue
readability-implicit-bool-conversionFlags implicit conversions to/from bool
readability-simplify-boolean-exprSimplifies redundant boolean expressions

Identifies patterns that prevent the compiler from generating optimal code.

CheckWhat it detects
performance-unnecessary-value-paramFunction parameters passed by value but only read
performance-unnecessary-copy-initializationUnnecessary copies during initialization
performance-for-range-copyRange-for loop copies elements instead of using references
performance-no-int-to-ptrInteger-to-pointer conversion without reinterpret_cast
performance-move-const-argMoving from a const value (copy, not move, occurs)

Enabling all checks at once on a large codebase generates thousands of warnings. Use an incremental Strategy to adopt clang-tidy without blocking development.

Run clang-tidy in read-only mode to assess the current state. Do not fail the build.

# .clang-tidy — Phase 1: Audit only
Checks: >
-*, bugprone-*, modernize-use-override, modernize-use-nullptr, modernize-use-auto
WarningsAsErrors: "'' # Empty = warnings are not errors
HeaderFilterRegex: "src/.*'

Use WarningsAsErrors only on new or modified files. On CI, run clang-tidy only on the diff:

Terminal window
# Only analyze files that changed in this commit
git diff --name-only HEAD~1 -- '*.cpp' '*.h' '*.hpp' | \
xargs clang-tidy -p build/ --warnings-as-errors='*'

Once the codebase is clean, enable WarningsAsErrors: "*" for the full project.

9. Integrating with clangd (IDE Experience)

Section titled “9. Integrating with clangd (IDE Experience)”

clangd is the language server that powers IDE features (autocomplete, go-to-definition, Diagnostics) for C++ in VS Code, Neovim, CLion, and other editors. Clangd reads the .clang-tidy File and surfaces clang-tidy warnings as in-editor diagnostics in real time.

Clangd finds .clang-tidy automatically if it exists in a parent directory of the source file. Ensure your compile_commands.json is generated and discoverable:

Terminal window
# CMake generates compile_commands.json
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# clangd auto-discovers compile_commands.json in build/
# If not, create a symlink at the project root:
ln -sf build/compile_commands.json .

Create a .clangd file at the project root for clangd-specific settings:

.clangd
CompileFlags:
CompilationDatabase: build
Diagnostics:
UnusedIncludes: Strict
Suppress:
- modernize-use-trailing-return-type
ClangTidy:
Add: [bugprone-*, modernize-*]
Remove: [modernize-use-trailing-return-type]
  • clangd runs clang-tidy checks on the fly as you type. Enabling too many checks can cause latency.
  • For large projects, start with bugprone-* and modernize-* in clangd, and run the full check suite via run-clang-tidy in CI.
  • The --header-filter option in .clang-tidy reduces analysis scope and speeds up clangd.
Terminal window
# Basic scan of the entire project
cppcheck --enable=all --suppress=missingIncludeSystem src/
# With project configuration (CMake auto-generated)
cmake -S . -B build -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;--error-exitcode=1"
# HTML report output
cppcheck --enable=all --htmlstep=2 --report-minimum-severity=warning src/ --output-file=report.html
FeatureClang-TidyCppcheckPVS-StudioSonarQube
ParserClang AST (full C++ support)Custom parser (limited templates)Clang-based + customClang-based
Data flow analysisVia clang-analyzer-* moduleBuilt-in, strongPath-sensitivePath-sensitive
Taint analysisNoNoYesYes
Template supportFullLimitedFullFull
SpeedSlower (full AST parse)FasterSlow (commercial)Slow (commercial)
AutofixYes (--fix)NoNo (suggests fixes)No
CI/CD integrationVia CMake or run-clang-tidyVia CMake or direct invocationNative CI pluginNative CI pipeline
IDE integrationclangd (real-time)Limited (plugin-based)Plugin for VS/JetBrainsPlugin for VS/JetBrains
False positive rateModerate (template edge cases)Moderate (data flow edge cases)LowLow-Medium
LicensingOpen source (Apache 2.0)Open source (GPL)CommercialCommercial
Best atStyle, modernization, refactoringBuffer overruns, leaks, uninitializedComplex logic bugs, securityCode quality metrics, security

In practice, using both tools catches the widest range of bugs. Clang-tidy excels at Modernization and style enforcement; cppcheck excels at data-flow bugs that require path-sensitive Analysis (e.g., using an uninitialized variable on a specific code path).

11. Static Analysis for Security (CERT C++ Coding Standards)

Section titled “11. Static Analysis for Security (CERT C++ Coding Standards)”

Beyond correctness and style, static analysis tools can enforce security rules from the CERT C++ Coding Standard. Clang-tidy includes the cert-* check group, which maps directly to CERT Recommendations:

CERT Ruleclang-tidy CheckVulnerability
EXP50-CPPcert-err52-cppThrowing in destructor
EXP63-CPPcert-dcl03-cImplicit conversions
DCL50-CPPcert-dcl58-cppUninitialized const
INT30-Ccert-int30-cUnsigned integer wrap
OOP57-CPPcert-oop57-cppCopy constructor missing
OOP58-CPPcert-oop58-cppMove constructor missing
Enabling cert-* in CI is a low-cost way to catch common vulnerability patterns. Many of these
Checks are also covered by bugprone-* and cppcoreguidelines-*So the overlap is intentional —
CERT rules provide a formal security justification for checks you might already have enabled.

In large codebases (100k+ lines), false positives are inevitable. A disciplined approach to managing Them prevents the suppression mechanism from becoming a liability:

  1. Fix immediately: Warnings with a clear, safe fix. No suppression needed.
  2. Suppress with justification: Warnings that are false positives but require a documented reason: // NOLINT(bugprone-narrowing-conversion): intentional truncation for wire format.
  3. Disable at project level: Warnings that are overwhelmingly false positives for the entire codebase (e.g., cppcoreguidelines-avoid-magic-numbers in a numerical library). Disable in .clang-tidyNot per-line.
  4. File-level suppression: For legacy files that cannot be fixed without a major refactor, use // NOLINTBEGIN / // NOLINTEND blocks.
// NOLINTBEGIN(cert-err58-cpp): legacy C API, exceptions not propagated across boundary
extern "C" int legacy_callback(void* ctx, int status) {
// ...
return status;
}
// NOLINTEND(cert-err58-cpp)

Maintain a suppression inventory. In CI, count the number of NOLINT comments and fail the build if The count increases:

Terminal window
# CI gate: ensure NOLINT count does not grow
NOLINT_COUNT=$(grep -r "NOLINT" src/ | wc -l)
echo "NOLINT suppressions: $NOLINT_COUNT"
if [ "$NOLINT_COUNT" -gt "$ALLOWED_SUPPRESSIONS" ]; then
echo "ERROR: NOLINT count exceeds threshold"
exit 1
fi

For large projects, clang-tidy can become the bottleneck in the CI pipeline. Understanding where Time is spent helps optimize the analysis configuration:

  • Per-file overhead: Each file requires a full AST parse (~0.5-2 seconds for a typical source file). This is unavoidable.
  • Check overhead: Data flow analysis checks (clang-analyzer-*) are 3-10x slower than simple AST pattern matching checks (modernize-*). Enable clang-analyzer-* only in a dedicated CI job.
  • Header traversal: If HeaderFilterRegex allows analysis of deeply included system headers, analysis time can explode. Restrict the filter to project headers.
Terminal window
# Profile clang-tidy on a single file
time clang-tidy -p build/ src/main.cpp --checks='bugprone-*,modernize-*' 2>&1
# Compare with data flow analysis enabled
time clang-tidy -p build/ src/main.cpp --checks='bugprone-*,modernize-*,clang-analyzer-*' 2>&1

In practice, splitting analysis into a fast path (bugprone-*``modernize-*``readability-*) that Runs on every commit and a slow path (clang-analyzer-*``cppcoreguidelines-pro-bounds-*) that Runs nightly provides the best developer experience.

Clang-tidy’s --fix and --fix-errors flags automatically apply suggested fixes to source code. This is the most powerful feature of clang-tidy: it can refactor an entire codebase in a single Pass.

Terminal window
# Step 1: Dry run — show what would be changed
clang-tidy -p build/ --checks='modernize-*' --fix-errors src/ --dry-run
# Step 2: Apply fixes
clang-tidy -p build/ --checks='modernize-*' --fix-errors src/
# Step 3: Format the result (clang-tidy does not reformat)
clang-format -i src/**/*.cpp src/**/*.hpp src/**/*.h

:::caution Always run clang-tidy fixes in a separate commit. Review the diff carefully before Merging. Some fixes (e.g., modernize-use-auto) can change semantics if the deduced type is not What you expected.

A common use case is migrating a codebase to C++23 idioms:

Terminal window
# Replace printf with std::print
clang-tidy -p build/ --checks='modernize-use-std-print' --fix src/
# Replace NULL with nullptr
clang-tidy -p build/ --checks='modernize-use-nullptr' --fix src/
# Add override to virtual function overrides
clang-tidy -p build/ --checks='modernize-use-override' --fix src/
# Replace typedef with using
clang-tidy -p build/ --checks='modernize-use-using' --fix src/

For organization-specific rules that are not covered by existing checks, you can write custom Clang-tidy checks in C++. This requires linking against the clang-tidy library:

MyCustomCheck.cpp
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang-tidy/ClangTidyCheck.h"
using namespace clang::ast_matchers;
class MyCustomCheck : public clang::tidy::ClangTidyCheck {
public:
MyCustomCheck(llvm::StringRef Name, clang::tidy::ClangTidyContext* Context)
: ClangTidyCheck(Name, Context) {}
void registerMatchers(clang::ast_matchers::MatchFinder* Finder) override {
// Example: find all functions named 'process' that take more than 5 parameters
Finder->addMatcher(
functionDecl(hasName("process"), parameterCount(isGreaterThan(5)))
.bind("process_func"),
this);
}
void check(const clang::ast_matchers::MatchFinder::MatchResult& Result) override {
const auto* Func = Result.Nodes.getNodeAs<clang::FunctionDecl>("process_func");
diag(Func->getLocation(), "function 'process' has too many parameters; consider a struct")
<< FixItHint::CreateInsertion(Func->getLocation(), "// TODO: refactor\n");
}
};

Custom checks are compiled as shared libraries and loaded via the --checks flag or by adding them To the .clang-tidy configuration. This is an advanced topic and requires familiarity with the Clang AST.

Static analysis tools are most effective when integrated into the code review process:

  1. Pre-commit hooks: Run clang-tidy --fix and clang-format as a pre-commit hook to catch issues before they reach the review.
  2. CI gate: Fail the build if clang-tidy reports errors on the changed files.
  3. Review comments: clang-tidy warnings appear as review comments on GitHub/GitLab via Codecov or similar tools.
  4. Metrics tracking: Track the number of clang-tidy warnings over time. A decreasing trend indicates improving code quality.
.git/hooks/pre-commit
#!/bin/bash
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp|h)$')
if [ -n "$CHANGED_FILES" ]; then
echo "$CHANGED_FILES" | xargs clang-tidy -p build/ --warnings-as-errors='*'
if [ $? -ne 0 ]; then
echo "clang-tidy found errors. Commit aborted."
exit 1
fi
fi
  • Enabling -* without understanding what it disables. This turns off ALL checks, then you selectively re-enable. Forgetting to re-enable important checks means they are silently skipped.
  • Suppressing warnings instead of fixing them. Every NOLINT comment should have a justification. Use // NOLINT(check-name): reason to document why the suppression exists.
  • Not pinning clang-tidy version. Different clang-tidy versions have different check sets. Pin the version in CI to avoid surprising new warnings.
  • Ignoring header files. If HeaderFilterRegex is too restrictive, bugs in headers go undetected.
  • Running analysis without a compilation database. Without compile_commands.jsonClang-tidy cannot resolve includes, macros, or platform-specific code correctly.
  • Enabling clang-analyzer-* in clangd. The path-sensitive analysis is too expensive for real-time feedback. Enable it only in CI or via explicit run-clang-tidy invocation.
  • Not using HeaderFilterRegex. Without this, clang-tidy may attempt to analyze system headers, which can produce false positives and slow down analysis significantly.

This topic covers the geographical processes and issues related to static analysis, including key theories, case studies, and management strategies. Key concepts include:

  • geographical concepts and theories
  • case studies and examples
  • data analysis and fieldwork techniques
  • sustainability and management strategies
  • synthesis and evaluation Using specific case studies and data to support arguments is essential for achieving the highest marks in geography assessments.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above. :::