Linting Strategy and Caching¶
This document outlines the static analysis and formatting strategy for the suckless-ogl project and the implementation of its high-performance caching mechanism.
Coding Style Philosophy¶
This project follows a coding style heavily inspired by the Linux kernel (Documentation/process/coding-style.rst), adapted for a C11 OpenGL rendering engine.
Why the Linux Kernel Style?¶
The kernel style was designed for large-scale C codebases maintained by thousands of contributors over decades. Its rules optimize for readability at scale, grep-ability, and minimal cognitive load — the same qualities we value in this project.
Other notable C conventions we drew from:
| Convention | Origin | Key Takeaway |
|---|---|---|
| Linux kernel coding style | Torvalds, kernel community | Primary inspiration: tabs-8, braces, 80 cols, goto cleanup, short functions |
| SEI CERT C | Carnegie Mellon SEI | Security-focused rules (MEM, ERR, STR) — enforced via clang-tidy cert-* checks |
| FreeBSD style(9) | FreeBSD project | Close to kernel style; explicit about blank lines between functions, return without parens |
| SQLite | D. Richard Hipp | 100% branch coverage, goto cleanup, self-contained C — a model for quality |
| id Software (Quake/Doom) | John Carmack | Pragmatic C for real-time graphics; performance-aware naming and layout |
Core Principles (from the Kernel)¶
- Indentation = 8 spaces (tabs) — Forces short nesting; if you need 3+ levels, refactor (kernel §1)
- 80-column limit — Encourages splitting complex expressions and extracting helpers (kernel §2)
- Opening braces on same line (except functions) — Linux/K&R brace style (kernel §3)
- One blank line between function definitions — Visual separation; never stack definitions without breathing room
- Functions should be short — Do one thing; fit on one or two screenfuls (kernel §6)
gotofor centralized cleanup — Single exit path for error handling, not cascadingif/else(kernel §7). See goto cleanup Patternsnake_caseeverywhere — Functions, variables, struct members.UPPER_CASEfor macros only (kernel §4)- No typedefs on structs — Exception: opaque handles and function pointers (kernel §5)
Code Formatting (Clang-Format)¶
All C, header, and GLSL files are formatted with clang-format. The configuration lives in .clang-format at the repository root.
Rule Reference¶
Each rule is mapped to its kernel style rationale:
| Option | Value | Kernel Reference | Effect |
|---|---|---|---|
BasedOnStyle |
Google |
— | Base style (overridden below to match kernel conventions) |
IndentWidth |
8 |
§1: "Tabs are 8 characters" | 8-space logical indentation; discourages deep nesting |
UseTab |
ForIndentation |
§1: "use tabs" | Tabs for indentation, spaces for alignment |
BreakBeforeBraces |
Linux |
§3: "K&R brace placement" | Opening brace on same line; functions on next line |
PointerAlignment |
Left |
§4: "declare close to the type" | int* p style (C convention) |
ColumnLimit |
80 |
§2: "the preferred limit is 80 columns" | Hard wrap at 80 columns |
AllowShortFunctionsOnASingleLine |
None |
§6: functions should be readable | Never collapse function bodies to a single line |
SeparateDefinitionBlocks |
Always |
§1, FreeBSD style(9) | Enforce a blank line between every top-level definition (functions, structs, enums) |
SortIncludes |
true |
— | Alphabetical include ordering (GLAD first via IncludeCategories priority) |
SeparateDefinitionBlocks¶
This rule (available since clang-format 14) ensures consistent visual separation between function definitions, struct declarations, and enum definitions. Without it, clang-format leaves inter-definition spacing untouched — adjacent function bodies with no blank line in between pass silently.
With Always, clang-format automatically inserts a blank line wherever one is missing, and removes double blank lines where they are excessive.
The Linux kernel enforces this by convention during code review. FreeBSD's style(9) states it explicitly: "Use blank lines to separate logical sections of code, including between function definitions." We automate it via clang-format.
CI Enforcement¶
Formatting is enforced in CI by the lint-and-format job:
# .github/workflows/main.yml (excerpt)
make format CONTAINER_RUN=""
git diff --exit-code || (echo "❌ Format error" && exit 1)
Any file that would be reformatted by clang-format causes the job to fail. Run just format locally before committing.
Strategy: Clang-Tidy¶
We use clang-tidy for static analysis. The configuration is defined in .clang-tidy, focusing on:
- Security: Avoiding insecure buffer handling and deprecated APIs.
- Reliability: Detecting narrowing conversions and uninitialized variables.
- Readability: Enforcing consistent coding styles and removing "magic numbers".
- Portability: Ensuring compliance with C standards (CERT, HICPP).
Style Preferences¶
We prioritize "Suckless" philosophy:
- Minimize external dependencies.
NOLINTcomments are forbidden — fix the root cause instead (see No-Suppression Policy below).- Use
static constorenuminstead of magic numbers.
Incremental Caching (Sentinel Files)¶
Originally, we explored cltcache. However, due to its overhead and specific requirement for explicit compiler flags (--), we transitioned to a native Sentinel-based caching system implemented directly in the Makefile.
How it Works¶
Instead of linting every file on every run, we use "Sentinel files" (.linted) to track the status of each source file.
- Dependency Tracking: Each
.lintedfile in.lint_cache/depends on:- The corresponding
.csource file. - The project's
.clang-tidyconfiguration. - The
compile_commands.jsondatabase.
- The corresponding
- Date Comparison:
makenatively compares the timestamp of the source vs. the sentinel. If the source is older than the sentinel, the file is skipped. - Updating: If a file needs linting,
clang-tidyis executed. On success, the sentinel file is updated usingtouch. - Dependencies: Before linting, the system ensures that generated headers (like
glad/glad.h) are ready by building the necessary targets. - Parallelization: The process is parallelized using
make -j$(NPROCS), allowing simultaneous analysis of multiple files.
Why this approach?¶
- Speed: Subsequent runs are near-instantaneous (O(1) file stat check).
- Robustness: If an analysis is interrupted, the sentinel isn't updated, ensuring it runs again on the next try.
- Simplicity: No external Python dependencies or complex cache databases; it leverages the operating system's file system and standard build tools.
- Visibility: The
Makefileoutput clearly shows which file is being processed, providing immediate feedback.
Maintenance¶
To clear the cache and force a full re-lint:
Training a new rule in .clang-tidy will also automatically invalidate the entire cache, ensuring project-wide compliance.
Include Hygiene (misc-include-cleaner)¶
The misc-include-cleaner check is enabled in .clang-tidy to detect unused #include directives at lint time.
Configuration¶
# .clang-tidy (excerpt)
Checks: '...,misc-*,...'
CheckOptions:
- key: misc-include-cleaner.MissingIncludes
value: 'false'
- UnusedIncludes: Enabled — flags headers that are included but never directly used.
- MissingIncludes: Disabled — avoids false positives on symbols available through transitive includes (common with cglm, stb, GLFW).
This ensures just lint catches stale includes automatically, without requiring IDE-specific tooling.
GLSL Shader Validation¶
Shaders are validated at lint time using glslangValidator via scripts/lint_shaders.sh.
Standard Mode (integrated in just lint)¶
Validates all .vert, .frag, and .comp shaders in shaders/. The script resolves custom @header include directives before passing the resolved source to glslangValidator.
Strict Mode (optional, SPIR-V target)¶
Runs validation with --target-env opengl (SPIR-V rules). This surfaces issues like missing layout(location=N) qualifiers that cause RenderDoc's shader debugger to fail silently.
As of March 2026, all 33 shader files pass strict SPIR-V validation. The project enforces explicit layout(location=N) on all varyings and non-opaque uniforms, and layout(binding=N) on all samplers/images. See renderdoc_guide.md for the full rationale.
No-Suppression Policy¶
NOLINT, NOLINTNEXTLINE, and NOLINTBEGIN/NOLINTEND are forbidden. The correct approach is always to fix the root cause.
An automated guard (scripts/check_nolint.sh) enforces this at every stage of the development workflow:
| Stage | Trigger | Mechanism | Blocking |
|---|---|---|---|
| Local commit | git commit on *.c / *.h |
pre-commit hook (check-nolint in .pre-commit-config.yaml) |
Yes (bypass: --no-verify) |
| Local manual | just check-nolint or make check-nolint |
Direct script invocation | Yes (exit 1) |
| CI — push | Push to any branch | lint-and-format job step |
Yes — fails the job |
| CI — pull request | PR opened/updated targeting master | lint-and-format job step |
Yes — fails the job |
| CI — scheduled | Nightly cron (01:00 UTC) | lint-and-format job step |
Yes — fails the job |
How it Works¶
The script checks for new NOLINT additions in *.c and *.h files using a two-layer approach:
- Committed changes:
git diff <base_ref>...HEADdetects NOLINT in already-committed code. - Staged changes:
git diff --cached <base_ref>detects NOLINT in content about to be committed (index). This is critical duringpre-commit, whereHEADstill points to the previous commit and staged content would otherwise be invisible.
When staged files exist, their final content is compared against the base ref, correctly handling cases where staged changes add or remove NOLINT suppressions. If any net new NOLINT lines are found, the check fails with a listing of all violations.
# Local usage
just check-nolint # Compare vs origin/master (default)
just check-nolint origin/main # Custom base ref
make check-nolint NOLINT_BASE_REF=origin/main
Exception Policy¶
If suppression is the only viable path, it requires:
- An explicit comment explaining why the fix is not possible.
- A clear assessment confirming no alternative exists.
- A tracking issue to revisit and remove the suppression.
- Explicit user validation before committing (use
--no-verify).