Static Analysis for C/C++ in CI
Compiler warnings, clang-tidy, the clang static analyzer, cppcheck and the commercial tools — what each finds, how to roll analysis onto a legacy codebase without blocking everyone, and how SARIF and diff-scoped runs make it survivable.
Overview
Complementary to sanitizers, not a substitute
Static analysis finds defects without running the program. Sanitizers see only the paths tests execute; static analysis reads every path but cannot know which are reachable in practice.
> [!IMPORTANT] That asymmetry is why both exist, and it is the first thing an interviewer wants you to articulate.
A ladder of cost and depth
- Compiler warnings — Free because the compiler already parsed the code; the highest-value rung by far, yet nearly every codebase leaves warnings on the table because -Wall -Wextra is wrongly believed to mean everything
- clang-tidy — Several hundred AST-matching checks by prefix, configured in a checked-in .clang-tidy, needing compile_commands.json because it genuinely parses your code
- Clang static analyzer — Symbolic path execution: finds the null dereference that happens only when two branches both go a particular way
- cppcheck — An independent engine with a different false-positive profile
- Coverity, PVS-Studio, Klocwork — Whole-program interprocedural analysis, certified MISRA and AUTOSAR packs with audit traceability, a triage database that remembers decisions across releases
The hard part is the rollout
Turn on a broad clang-tidy set across two million lines: tens of thousands of findings, mostly stylistic, in code untouched for years. Gate on that and every PR fails for reasons unrelated to its change, so the team disables the tool for the next five years.
- Fail only on new findings — clang-tidy-diff.py; GitHub code scanning natively surfaces alerts only on diff lines
- Burn down the baseline — Ordinary maintenance, not a blocker
The codebase improves monotonically and nobody is blocked by debt they did not create.
False-positive discipline
- Right about 95 percent of the time — Keep, with narrow inline suppressions carrying a reason
- Right about 60 percent of the time — Disable at configuration level, with a comment
> [!WARNING] What kills a program is the middle path: keeping a noisy check on and scattering bare NOLINT comments, which suppress real hits with the noise and leave no record of why.
The -Werror question
- CI — Correct — The only thing that stops warning count growing without bound
- Local development — Wrong — A compiler upgrade turns every working tree red at once, and exploratory code with an unused variable should still build
CMake supports the split through the COMPILE_WARNING_AS_ERROR property and the --compile-no-warning-as-error escape hatch.
Key concepts
- Compilation database
- compile_commands.json, a JSON array recording the exact compiler invocation for every translation unit. Generated by CMake with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, or by bear for make-based builds. Required by clang-tidy, IWYU and clang-based tooling generally, and strongly recommended for cppcheck via --project.
- clang-tidy check prefixes
- Checks are namespaced by group: bugprone- for error-prone patterns, cert- for CERT secure coding, clang-analyzer- wrapping the path-sensitive static analyzer, concurrency-, cppcoreguidelines-, google-, hicpp-, misc-, modernize-, performance-, portability-, readability-. Selected with globs where a leading dash disables, as in -checks=-*,bugprone-*.
- .clang-tidy configuration file
- Checked-in YAML with Checks, WarningsAsErrors, HeaderFilterRegex and CheckOptions keys, discovered by walking up the directory tree so subdirectories can inherit and override. Keeping configuration in the repo rather than in CI flags is what makes local and CI runs agree.
- HeaderFilterRegex
- Controls which headers clang-tidy reports diagnostics from. Left unset, findings in system and third-party headers are suppressed but findings in your own headers are too, so real bugs in your header-only code are invisible. Set it to match your source root and nothing else.
- Path-sensitive vs AST-matching analysis
- clang-tidy matches structural patterns in the syntax tree: fast, local, high recall on known shapes. The clang static analyzer symbolically executes program paths and tracks constraints, finding defects that depend on a specific combination of branches. Slower, and intraprocedural with limited inlining, which is the boundary commercial whole-program tools sell against.
- Baseline and ratchet
- The rollout technique for a legacy codebase. Record existing findings as an accepted baseline, fail CI only on new findings, and reduce the baseline on a schedule. Implemented as diff-scoped analysis with clang-tidy-diff.py, as a stored result database in CodeChecker or a commercial tool, or natively by code scanning alerting only on lines in the diff.
- SARIF 2.1.0
- The Static Analysis Results Interchange Format that lets heterogeneous tools feed one alert surface. GitHub code scanning accepts only 2.1.0, via the upload-sarif action or the code-scanning API, deduplicating across runs using partialFingerprints. Limits: 20 runs per file, 25,000 results per run with the top 5,000 retained, 10 MB gzip-compressed.
- NOLINT and its scope
- Inline suppression: NOLINT for the current line, NOLINTNEXTLINE for the following one, NOLINTBEGIN and NOLINTEND for a range, each optionally narrowed to a check glob such as NOLINT(bugprone-*). A bare NOLINT disables every check on that line, which is why suppressions should always name the check and carry a reason.
When to use it
- Every build, everywhere: a strong warning set beyond -Wall -Wextra, with warnings promoted to errors in the CI configuration only
- Every pull request: clang-tidy scoped to the changed lines via clang-tidy-diff.py, with a narrow high-signal check set rather than a broad one
- Nightly, non-blocking: full-repo clang-tidy and cppcheck producing a trend line and a SARIF upload, so the baseline is visible and shrinking
- Weekly or per-release: the clang static analyzer through scan-build, or a commercial whole-program scan, since path-sensitive and interprocedural analysis is too slow for the pull request path
- Regulated or safety-critical work: a commercial tool with a certified MISRA, AUTOSAR or CERT rule pack, because the audit needs traceability the open source tools do not produce
- Header hygiene on a codebase with runaway build times: include-what-you-use, run manually and reviewed by hand, never applied automatically
Interview questions
- You are asked to turn on clang-tidy for a two-million-line legacy C++ codebase. Walk me through the rollout.
- Which warnings do -Wall and -Wextra not give you, and which ones do people wrongly disable?
- Why -Werror in CI but not in local developer builds?
- Describe a false-positive triage workflow. What do you do with a check that is 40 percent false positives?
- clang-tidy, the clang static analyzer, cppcheck, and Coverity — what does each actually do differently?
Sign up free to read the full topic
More DevOps topics
- Sigstore & Software Supply Chain Security
- SLO / SLI / Error Budgets
- SLSA — Software Supply Chain Security
- Software Supply Chain Security
- SonarQube — Quality Gates, Web API, Administration, Errors and Mistakes
- Sources — Git, OCI, Helm, Bucket
- Storage Errors — FailedMount, Pending PVCs, Multi-Attach and Full Volumes
- Strangler Fig
- Team Topologies
- Tekton
- Terraform — Production Errors, Mistakes and Recovery
- Terraform & OpenTofu
Start free on Cariara · Coding problems · System design studio