Formatting, Linting, and Static Analysis
See the clang-format docs and clang-tidy docs for the full option reference — here's the validated configuration and the reasoning behind the choices.
Why automated tooling beats code review for style
Formatting debates and style nitpicks waste code review time on things that don't matter. Worse, manual review misses subtle bugs that a compiler-level tool would catch instantly. The two tools here divide responsibilities cleanly:
- clang-format eliminates all formatting discussion. The config is checked in; every file looks the same on every machine. Reviewers spend time on logic, not whitespace.
- clang-tidy runs the compiler's full AST on your code and catches real bugs — not just style issues. Unlike a text-based linter, it understands semantics: it knows that
vinstd::move(v); v.push_back(x);is the same variable, and that the move leaves it in an unspecified state.
Concrete examples of what clang-tidy catches:
bugprone-use-after-move:std::move(v); v.push_back(x);— compiles, passes most sanitizers, fails intermittently in production.bugprone-unchecked-optional-access:auto val = maybe_value.value();without checkinghas_value()— throwsstd::bad_optional_accessat runtime.cppcoreguidelines-pro-type-cstyle-cast:(int*)ptrsilently discardsconst, bypasses virtual dispatch, and succeeds even whenstatic_castwould fail at compile time.performance-unnecessary-copy-initialization:auto x = heavy_obj;that should beconst auto& x = heavy_obj;.
.clang-format
This config uses Google style as a base with practical overrides. The goal is deterministic output — not the prettiest formatting, the same formatting everywhere.
# .clang-format
BasedOnStyle: Google
Language: Cpp
Standard: c++17
ColumnLimit: 100 # Google uses 80; 100 is more practical for modern screens
IndentWidth: 4
TabWidth: 4
UseTab: Never
AccessModifierOffset: -3 # `public:` indented 1 level less than the class body
NamespaceIndentation: None # don't indent namespace contents
PointerAlignment: Left # int* p — pointer is part of the type, not the variable
DerivePointerAlignment: false
ReferenceAlignment: Left
AllowShortFunctionsOnASingleLine: Empty # only empty bodies on one line
AllowShortIfStatementsOnASingleLine: Never # always brace ifs
AllowShortLoopsOnASingleLine: false
BreakBeforeBraces: Attach # opening brace on same line (K&R style)
BreakConstructorInitializers: BeforeComma
BreakInheritanceList: BeforeComma
# Group and sort #includes. Priority determines group order:
# 1. Standard library (<vector>) 2. System/third-party (<grpc/grpc.h>)
# 3. Project headers 4. Same-directory headers
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<[^/.]+>$' # <vector>, <string>
Priority: 1
- Regex: '^<.*\.h(pp)?>$' # <grpc/grpc.h>, <openssl/ssl.h>
Priority: 2
- Regex: '^"[^/]+"$' # "widget.h" — same-directory header
Priority: 4
- Regex: '.*' # everything else (project-relative paths)
Priority: 3
SortIncludes: CaseSensitive
SortUsingDeclarations: true
AlignAfterOpenBracket: Align
AlignTrailingComments: true
SpacesBeforeTrailingComments: 2
EmptyLineBeforeAccessModifier: LogicalBlock
EmptyLineAfterAccessModifier: Never
FixNamespaceComments: true # add `// namespace acme_core` after closing `}`
ShortNamespaceLines: 0Run:
# Format all C++ files in-place:
clang-format -i $(git ls-files '*.h' '*.hpp' '*.cc' '*.cpp')
# Check-only mode for CI (exits non-zero if any file would change):
clang-format --dry-run --Werror $(git ls-files '*.h' '*.hpp' '*.cc' '*.cpp').clang-tidy
# .clang-tidy
Checks: >
bugprone-*,
cert-*,
clang-analyzer-*,
cppcoreguidelines-*,
modernize-*,
performance-*,
portability-*,
readability-*,
-bugprone-easily-swappable-parameters,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-modernize-use-trailing-return-type,
-readability-magic-numbers,
-readability-identifier-length,
-readability-function-cognitive-complexity
WarningsAsErrors: >
bugprone-use-after-move,
bugprone-unchecked-optional-access,
cert-err58-cpp,
clang-analyzer-*
HeaderFilterRegex: '^(include|src)/.*'
CheckOptions:
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
- { key: readability-identifier-naming.StructCase, value: CamelCase }
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
- { key: readability-identifier-naming.EnumConstantCase, value: CamelCase }
- { key: readability-identifier-naming.EnumConstantPrefix, value: k }
- { key: readability-identifier-naming.FunctionCase, value: lower_case }
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
- { key: readability-identifier-naming.MemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
- { key: readability-identifier-naming.NamespaceCase, value: lower_case }
- { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase }
- { key: readability-identifier-naming.ConstexprVariablePrefix, value: k }
- { key: readability-identifier-naming.MacroDefinitionCase, value: UPPER_CASE }What each check group catches
bugprone-* — patterns that are frequently bugs even when they compile cleanly:
bugprone-use-after-move: using a moved-from objectbugprone-unchecked-optional-access: dereferencing an optional without checkinghas_value()bugprone-integer-division:float x = 3 / 2;silently gives1.0, not1.5
cert-* — CERT C++ Secure Coding Standard:
cert-err58-cpp: static-storage objects that throw during construction — the exception is uncatchable
clang-analyzer-* — inter-procedural analysis, promoted to errors:
- Null pointer dereferences across function boundaries
- Memory and resource leaks
- Dead code indicating logic errors
modernize-* — C++11/14/17 idioms replacing error-prone C patterns:
modernize-use-nullptr: replacesNULLand0for pointersmodernize-use-override: addsoverrideto virtual overrides, catching silent shadowing bugsmodernize-use-emplace: replacespush_back(T(...))withemplace_back(...)
performance-* — common performance mistakes:
performance-unnecessary-copy-initialization:auto x = heavy_obj;→ should beconst auto& xperformance-move-const-arg:std::move(const_var)compiles but does nothing
Why some checks are disabled:
cppcoreguidelines-avoid-magic-numbers: too noisy in practice — bitmasks, HTTP status codes, math constantsreadability-function-cognitive-complexity: useful signal but flags any function with nested loopsmodernize-use-trailing-return-type: style preference, not correctness —int compute()is clearer thanauto compute() -> int
HeaderFilterRegex limits diagnostics to your own headers. Without it, clang-tidy runs on every transitively included header — including Conan packages — generating thousands of irrelevant warnings.
Run:
# Single file (fastest for iteration):
clang-tidy -p build src/widget.cpp
# Whole project, parallel:
run-clang-tidy -p build -j $(nproc) -header-filter='^(include|src)/.*'-p build points to the compile_commands.json directory. CMake generates this when CMAKE_EXPORT_COMPILE_COMMANDS=ON — set in the CMake structure preset.
CMake targets
Wire format checking and tidy into CMake so developers and CI use identical commands:
find_program(CLANG_FORMAT_EXE clang-format)
find_program(CLANG_TIDY_EXE clang-tidy)
find_program(RUN_CLANG_TIDY run-clang-tidy)
file(GLOB_RECURSE ALL_CXX_SOURCES CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/src/*.cpp"
"${CMAKE_SOURCE_DIR}/src/*.h"
"${CMAKE_SOURCE_DIR}/include/*.hpp")
if(CLANG_FORMAT_EXE)
add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE} -i ${ALL_CXX_SOURCES}
COMMENT "Running clang-format on all sources")
add_custom_target(format-check
COMMAND ${CLANG_FORMAT_EXE} --dry-run --Werror ${ALL_CXX_SOURCES}
COMMENT "Checking clang-format compliance")
endif()
if(RUN_CLANG_TIDY)
add_custom_target(tidy
COMMAND ${RUN_CLANG_TIDY} -p ${CMAKE_BINARY_DIR}
-header-filter=^${CMAKE_SOURCE_DIR}/(include|src)/.*
COMMENT "Running clang-tidy across the project")
endif()cmake --build build --target format # in-place format (developer)
cmake --build build --target format-check # check only, exits 1 if diff (CI)
cmake --build build --target tidy # full tidy passInline tidy during compilation (optional)
option(ENABLE_TIDY "Run clang-tidy on every build" OFF)
if(ENABLE_TIDY)
set(CMAKE_CXX_CLANG_TIDY clang-tidy)
endif()Gate it behind an option — inline tidy significantly slows builds. Enable only in CI:
cmake --preset release -DENABLE_TIDY=ON
cmake --build --preset releasePre-commit hook
.pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8
hooks:
- id: clang-format
types_or: [c++, c]pre-commit install # one-time per checkoutAfter this, git commit automatically runs clang-format on staged files and aborts if any file would change. The developer re-stages the formatted files and commits again.
CI gate
# .github/workflows/lint.yml
name: lint
on: [pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y clang-format-18 clang-tidy-18
- run: cmake --preset release && cmake --build build/release --target format-check
- run: cmake --build build/release --target tidyFormat check runs first — it's fast (seconds). Tidy runs second — it's slow (minutes) but catches deeper issues. Both must pass before merge.