Skip to main content

ADR-0006: Code Style and Conventions for C Components

Accepted

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.

RuleSetting
Brace styleK&R — opening brace on the same line
Indentation2 spaces, no tabs
Line length120 columns
Pointer alignmentRight (int *p)
Parameter packingOne per line when wrapping
Binary operatorsBreak before all
Include orderingRegroup and sort — standard headers first, then project headers
Preprocessor indentationBefore hash ( #include inside blocks)
Line endingsLF

clang-format is invoked:

  • Locally by the contributor's editor or manually before committing.
  • In CI with clang-format --dry-run -Werror to 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.

ElementCaseExample
Functionslower_casebuffer_init
Variableslower_casebyte_count
Parameterslower_casemax_size
Global constantsUPPER_CASEDEFAULT_CAPACITY
MacrosUPPER_CASEARRAY_LEN
Struct typeslower_casestruct hash_map
Enum typeslower_caseenum log_level
Enum constantsUPPER_CASELOG_DEBUG
Typedefslower_case with _t suffixhash_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 — flags memcpy, memmove, etc., which are fundamental to C. The project relies on bounds checking and sanitizers instead.

Complexity and size limits:

MetricThreshold
Cognitive complexity50
Function lines200
Function statements100
Function parameters8

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.json is exported for clang-tidy and language server integration.
  • The make tidy target runs clang-tidy with auto-fix.
  • The make check target 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 blame more useful.
  • Onboarding. New contributors do not need to learn the style — their editor applies it automatically via .clang-format and .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.