ADR-0006: Code Style and Conventions for C Components
Context
With C23 as the core language (ADR-0002) and a build system that already exports
compile_commands.json (ADR-0005), the project needs a single coding style that
every C component follows and a mechanism to enforce it automatically.
Without automated enforcement, style consistency depends on contributor discipline and reviewer vigilance — both
unreliable at scale. Formatting disagreements waste review time, inconsistent naming makes cross-component
contributions harder, and cosmetic-only diffs obscure real changes in git blame.
The style must be:
- Machine-enforceable — formatting and naming rules must be checked automatically so that humans only review semantics.
- Familiar — conventions should not surprise experienced C programmers.
- Defined in committed files — editors and CI must derive the style from the repository, not from external documentation.
Decision
All C components adopt K&R brace style with 2-space indentation and a 120-column line limit, enforced by
clang-format and clang-tidy. An .editorconfig file complements these tools with basic editor settings.
Formatting (clang-format)
Each component commits a .clang-format file that encodes the formatting rules. This file is the single source
of truth for formatting — the table below is a summary, not a replacement.
| Rule | Setting |
|---|---|
| Brace style | K&R — opening brace on the same line |
| Indentation | 2 spaces, no tabs |
| Line length | 120 columns |
| Pointer alignment | Right (int *p) |
| Parameter packing | One per line when wrapping |
| Binary operators | Break before all |
| Include ordering | Regroup and sort — standard headers first, then project headers |
| Preprocessor indentation | Before hash ( #include inside blocks) |
| Line endings | LF |
clang-format is invoked:
- Locally by the contributor's editor or manually before committing.
- In CI with
clang-format --dry-run -Werrorto reject non-conforming code.
Naming conventions (clang-tidy)
Each component commits a .clang-tidy file that enforces naming conventions through the
readability-identifier-naming check family.
| Element | Case | Example |
|---|---|---|
| Functions | lower_case | buffer_init |
| Variables | lower_case | byte_count |
| Parameters | lower_case | max_size |
| Global constants | UPPER_CASE | DEFAULT_CAPACITY |
| Macros | UPPER_CASE | ARRAY_LEN |
| Struct types | lower_case | struct hash_map |
| Enum types | lower_case | enum log_level |
| Enum constants | UPPER_CASE | LOG_DEBUG |
| Typedefs | lower_case with _t suffix | hash_map_t |
Static analysis (clang-tidy)
The same .clang-tidy file enables broad static analysis beyond naming. The following check families are
activated: bugprone-*, cert-*, clang-analyzer-*, concurrency-*, misc-*, performance-*,
portability-*, readability-*.
A small set of checks is explicitly disabled where they produce more noise than value:
bugprone-easily-swappable-parameters— triggers on idiomatic C APIs (e.g.,memcpy(dst, src, n)).misc-include-cleaner— unreliable with generated headers and conditional compilation.misc-unused-parameters— C callback signatures are fixed by the caller; unused parameters are normal.readability-identifier-length— single-letter loop variables (i,n) are idiomatic C.readability-magic-numbers— too noisy in low-level code with well-known constants (sizes, offsets, masks).portability-avoid-pragma-once— the project uses#pragma once; the portability concern is not relevant for our target compilers.clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling— flagsmemcpy,memmove, etc., which are fundamental to C. The project relies on bounds checking and sanitizers instead.
Complexity and size limits:
| Metric | Threshold |
|---|---|
| Cognitive complexity | 50 |
| Function lines | 200 |
| Function statements | 100 |
| Function parameters | 8 |
These thresholds account for the fact that clang-tidy analyzes code after macro expansion. Functions that use control-flow macros (error handling, iteration, cleanup patterns) can have significantly higher complexity and statement counts in expanded form than their source suggests. The limits are set high enough to avoid false positives from macro-heavy code while still flagging genuinely oversized functions. clang-tidy warns but does not fail the build on size violations, and authors may suppress individual warnings with a comment when the structure genuinely requires it.
Editor configuration (.editorconfig)
Each component commits an .editorconfig file with:
- UTF-8 encoding, LF line endings.
- 2-space indentation for all files except Makefiles (tabs).
- 120-character line length.
- Trailing whitespace trimmed, final newline inserted.
Build system integration
The build system (ADR-0005) already provides the infrastructure for these tools:
compile_commands.jsonis exported for clang-tidy and language server integration.- The
make tidytarget runs clang-tidy with auto-fix. - The
make checktarget runs tidy followed by tests. - CI presets run clang-tidy as part of the build; clang-format checks are added by this decision.
Alternatives Considered
GNU coding standards. The traditional style for GNU projects: braces on their own line, 2-space body indent
but 6-space continuation, /* */ comments only. clang-format provides a GNU preset, but it only approximates
the full standard — certain conventions around continuation indentation and comment formatting require additional
overrides and still produce deviations. The brace-on-its-own-line convention also inflates vertical space
significantly, and the style is unusual outside the GNU ecosystem.
Linux kernel style. Tabs for indentation, 80-column limit, opening braces on the same line except for function definitions. Well documented and supported by clang-format. However, the 80-column limit is unnecessarily restrictive on modern displays, tab indentation creates alignment ambiguity in mixed tab/space scenarios, and the function-brace exception adds a rule for marginal benefit. The kernel style is optimized for the kernel's unique codebase and review culture.
Written style guide without tooling. Many projects maintain a STYLE.md or CONTRIBUTING.md describing the preferred style in prose. This relies on contributors reading the guide and reviewers catching violations. In practice, written-only guides are inconsistently applied, generate review friction, and drift over time. Machine enforcement eliminates these problems entirely.
No shared style — let each component decide. Maximum autonomy for component maintainers, but cross-component contributions become painful when each repository uses a different formatting convention. Consistent tooling configuration across the workspace is a low-cost, high-value convention.
Consequences
- Zero formatting debates. clang-format is the single source of truth. Disagreements are settled by the configuration file, not by reviewers.
- Clean diffs. Because every contributor formats identically, diffs contain only semantic changes. This
makes code review faster and
git blamemore useful. - Onboarding. New contributors do not need to learn the style — their editor applies it automatically via
.clang-formatand.editorconfig. Naming violations are caught by clang-tidy at build time rather than in review. - Toolchain dependency. Contributors need clang-format and clang-tidy installed locally for the best experience. Both are bundled with any LLVM/Clang distribution (18+), already required by ADR-0002 for C23 support.
- Configuration maintenance. The three configuration files must be kept in sync across components.