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

Start free on Cariara · Coding problems · System design studio