Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# clang-tidy configuration.
#
# WHY THIS SITS IN THE REPO ROOT and not under docs/ with the other tooling state: clangd finds
# it ONLY by walking up the directory tree from each source file, and has no flag to point
# elsewhere (clang-tidy's --config-file would work, clangd's equivalent does not exist). Moving
# it would silently kill editor diagnostics while CI stayed green — the worst possible split.
# It must live at or above src/.
#
# Shape: `*` minus an explicit disable list — the archetype ESPHome and ClickHouse use. The
# alternative (a short opt-in list, as LLVM and Chromium do) suits codebases too large to
# ever get clean; at ~50k LOC we can reach zero findings, and `*`-minus catches new check
# families as LLVM adds them instead of silently ignoring them.
#
# The list is derived from ESPHome's (a large ESP32-targeted C++ codebase, our closest peer —
# theirs is `*` minus 175 checks with WarningsAsErrors: ''), then tuned against this tree.
# Tuning mattered: `*` with only their FAMILY disables gave 6,073 findings here; adding the
# style and cert disables brought it to 131 real ones.
#
# WHY THE REASONS LIVE HERE AND NOT INLINE: a `#` inside the `>-` folded block below is NOT a
# YAML comment — it is folded into the check string as literal text and silently corrupts
# every disable after it. (Learned the hard way: a commented version of this file left
# `abseil-*` enabled and produced 12,181 findings.) So the block stays pure, and every
# disable is justified in the table below. A check removed without a stated reason will be
# re-litigated by the next reader; recording rejected checks is the Chromium pattern.
#
# OTHER PLATFORMS' RULE SETS — not our target, pure noise:
# abseil, altera, android, boost, fuchsia, hicpp, llvmlibc, mpi, objc, zircon
#
# CPPCOREGUIDELINES (whole family) — mostly aliases of checks we already run, plus bounds
# and cast rules that hardware register access and DMA buffer work must violate by nature.
# ClickHouse disables it as "impractical... also slow"; ESPHome disables ~25 individually.
#
# CERT — almost entirely aliases of bugprone-*, so enabling both double-reports.
# cert-err33-c alone produced 3,678 findings here: it fires on every snprintf whose return
# value we ignore deliberately.
#
# LOUD WITH NO ACTIONABLE FIX ON THIS CODEBASE:
# bugprone-easily-swappable-parameters — flags every (int x, int y); in a geometry/LED
# codebase that is most of them, and the "fix" is strong types for every coordinate pair,
# an architecture decision rather than a lint fix. Disabled by SerenityOS, ClickHouse,
# ESPHome, Godot and the Zephyr template.
# bugprone-narrowing-conversions — fires on every uint8_t pixel-math expression.
# performance-enum-size — 3 bytes per enum is real on a 180 KB-heap device, but the fix
# touches every enum for a gain the linker mostly recovers. ESPHome disables it despite
# shipping on the same class of hardware.
# bugprone-implicit-widening-of-multiplication-result — MEASURED 47 findings, 0 reachable.
# 27 are compile-time constants (`256 * 1024`) the compiler folds. The other 20 are
# buffer index arithmetic on nrOfLightsType, which is already uint32_t where it matters
# (light_types.h). Overflow needs 32 bits of product; the largest real installation, the
# 48x256 wall, needs 17 — a margin of ~87,000x. "Fixing" it means casting every index in
# the hot path for a hazard that cannot occur.
# bugprone-signed-char-misuse — MEASURED 12/12 FALSE, and its suggested fix is a BUG. Every hit
# is an int8_t GPIO pin widening to int/int16_t/uint16_t, where -1 is the "unset pin" sentinel
# (platform.h EthPinConfig, the addPin controls). Sign extension is precisely what we want;
# "cast to unsigned char first" would turn -1 into 255 and silently claim GPIO 255. The check
# is aimed at char-typed *text* (where a negative byte indexes out of a table), not at a
# signed-integer-with-sentinel convention.
# performance-no-int-to-ptr — MEASURED 9/9 the SAME deliberate idiom, none a hardware address:
# ControlDescriptor::aux (Control.h) is a `uintptr_t` tagged field holding either a count or an
# options-array pointer. Storing a pointer in an integer field is the point; the check's premise
# (an int that happens to be cast to a pointer defeats alias analysis) does not apply to a
# round-tripped pointer. Splitting aux into a union to satisfy it grows the descriptor on a
# 180 KB-heap device for no correctness gain.
# bugprone-throwing-static-initialization — MEASURED 7/8 provably cannot throw: the peripheral
# registrations and the MoonLive builtin tables write into fixed-size static arrays with no heap
# and no std::string/vector, and Palette::fromBuiltin is plain array math. The 8th is a desktop
# std::filesystem::path built from a literal — theoretically bad_alloc, never reachable, and
# desktop-only. A check that is wrong 7 times out of 8 costs more attention than it earns.
# bugprone-infinite-loop — MEASURED 28/28 FALSE on this tree. It cannot track uint8_t loop
# counters, and `for (uint8_t i = ...; i > 0; i--)` is a house convention
# (coding-standards § Prefer integers); it also misfires on `while (b)` in a textbook
# Euclid gcd. The upstream FPs are a known LLVM issue with fixes still landing, so this
# is worth re-testing on a future LLVM — but a check that is wrong every single time
# teaches us to ignore its whole family, which costs more than it catches.
#
# CONFLICTS WITH DELIBERATE ARCHITECTURE:
# misc-no-recursion — the JSON reader is deliberately recursive (Minimalism means elegance).
# misc-non-private-member-variables-in-classes — the module tree uses plain member data by
# design, so the UI can render any module generically.
# misc-multiple-inheritance — both hits are `MoonModule + ListSource`, where ListSource is a
# pure interface (no data, virtual dtor) and MoonModule is the one stateful base. That is the
# textbook data-source/adapter shape CLAUDE.md sanctions by name (UITableView's data source,
# Qt's QAbstractItemModel); the check cannot tell it from real multiple state inheritance
# because ListSource gives one convenience override a default body.
# misc-use-anonymous-namespace / misc-use-internal-linkage /
# llvm-prefer-static-over-anonymous-namespace — fight header-only light modules.
# portability-avoid-pragma-once / llvm-header-guard — `#pragma once` is our convention.
#
# TOO SLOW, OR FILTERED BY CLANGD ANYWAY:
# misc-const-correctness (>10x AST cost), misc-include-cleaner ("very very noisy"),
# misc-header-include-cycle.
#
# OWNED BY ANOTHER TOOL (one rule, one owner):
# readability-function-cognitive-complexity / *-function-size — complexity is lizard's job;
# it produces the per-commit number repo-health.json trends.
# bugprone-branch-clone — fires on deliberate parallel switch arms.
# clang-analyzer-security.ArrayBound — analyses the macOS SDK's headers, not our code.
#
# Everything else disabled below is a style opinion we do not hold; enabling any of them
# would gate a rewrite rather than a fix.
#
# NOT GATED YET. `WarningsAsErrors` stays empty until the remaining 47 clang-analyzer findings
# are triaged (backlog: "clang-tidy: triage the 47 clang-analyzer findings"). Switching it to
# `'*'` is the ratchet that stops a zero decaying, and it is one line — but it fails the gate
# today, so it lands with that triage rather than before it.
---
Checks: >-
*,
-abseil-*,
-altera-*,
-android-*,
-boost-*,
-fuchsia-*,
-hicpp-*,
-llvmlibc-*,
-mpi-*,
-objc-*,
-zircon-*,
-cppcoreguidelines-*,
-cert-err33-c,
-cert-err58-cpp,
-cert-dcl50-cpp,
-cert-str34-c,
-cert-int09-c,
-cert-oop57-cpp,
-bugprone-easily-swappable-parameters,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,
-bugprone-infinite-loop,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-signed-char-misuse,
-bugprone-throwing-static-initialization,
-performance-no-int-to-ptr,
-performance-enum-size,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-multiple-inheritance,
-misc-use-anonymous-namespace,
-misc-use-internal-linkage,
-misc-const-correctness,
-misc-include-cleaner,
-misc-header-include-cycle,
-llvm-prefer-static-over-anonymous-namespace,
-llvm-header-guard,
-llvm-include-order,
-llvm-namespace-comment,
-llvm-else-after-return,
-llvm-qualified-auto,
-llvm-use-ranges,
-portability-avoid-pragma-once,
-modernize-use-trailing-return-type,
-modernize-avoid-c-arrays,
-modernize-avoid-c-style-cast,
-modernize-avoid-variadic-functions,
-modernize-use-nodiscard,
-modernize-use-auto,
-modernize-use-nullptr,
-modernize-use-designated-initializers,
-modernize-use-integer-sign-comparison,
-modernize-use-equals-default,
-modernize-use-default-member-init,
-modernize-type-traits,
-modernize-raw-string-literal,
-modernize-return-braced-init-list,
-modernize-loop-convert,
-modernize-use-ranges,
-readability-magic-numbers,
-readability-identifier-length,
-readability-implicit-bool-conversion,
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-inconsistent-ifelse-braces,
-readability-else-after-return,
-readability-isolate-declaration,
-readability-math-missing-parentheses,
-readability-avoid-nested-conditional-operator,
-readability-named-parameter,
-readability-qualified-auto,
-readability-redundant-inline-specifier,
-readability-redundant-parentheses,
-readability-redundant-casting,
-readability-enum-initial-value,
-readability-convert-member-functions-to-static,
-readability-make-member-function-const,
-readability-static-accessed-through-instance,
-readability-use-std-min-max,
-readability-use-concise-preprocessor-directives,
-readability-container-contains,
-readability-simplify-boolean-expr,
-readability-function-cognitive-complexity,
-readability-function-size,
-google-explicit-constructor,
-google-readability-casting,
-google-readability-namespace-comments,
-google-readability-todo,
-google-readability-braces-around-statements,
-google-readability-function-size,
-google-build-using-namespace,
-google-runtime-int,
-clang-analyzer-security.ArrayBound
WarningsAsErrors: ''
HeaderFilterRegex: 'src/(core|light)/'
FormatStyle: none
68 changes: 68 additions & 0 deletions .clangd
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# clangd configuration — live diagnostics in the editor.
#
# Repo root is not a choice: clangd reads project config from a `.clangd` file in the project
# directory, with no override flag. Same constraint as `.clang-tidy` next to it — both are
# discovered by convention, so neither can move under docs/ with the rest of the tooling state.
#
# clangd is the language server behind VS Code's clangd extension (and most other editors).
# It runs the SAME .clang-tidy config CI will use, so a finding appears while you type rather
# than in a pipeline ten minutes later. Nothing here gates anything: this file only affects
# what your editor shows you.
#
# Setup (once): install the `llvm-vs-code-extensions.vscode-clangd` extension and DISABLE
# Microsoft's C/C++ IntelliSense — running both produces duplicated and contradictory
# diagnostics. Everything else is automatic; `cmake --build` refreshes the database.
#
# NOTE clangd deliberately skips clang-tidy checks it considers slow (>10% AST-build cost,
# e.g. misc-const-correctness), so the editor runs a subset of the CI set. That is why the
# same config file is safe to share between the two: CI stays the authority.

CompileFlags:
# Where to find compile_commands.json — the real include paths and flags for each file.
# Without it clangd guesses, and reports phantom "file not found" errors on every header.
# build/macos is this host's dir (the build script writes build/<macos|linux|windows>);
# clangd falls back to searching parent dirs if this one is absent, so a Linux or Windows
# checkout still works after its own build.
CompilationDatabase: build/macos

# THE ONE SETTING THAT MAKES OR BREAKS THIS. The database records whichever compiler CMake
# picked (/usr/bin/c++, Apple Clang, on a stock Mac), but clangd is usually a DIFFERENT
# clang — Homebrew LLVM, or the one bundled with the extension. A different clang does not
# know Apple Clang's standard-library paths, so EVERY file reports `'cstdint' file not
# found` and all real diagnostics vanish behind the noise.
#
# --query-driver tells clangd to ask the recorded compiler where its headers live. The
# globs cover the system compilers and any Homebrew/Xcode toolchain; harmless if a path
# does not exist. (Same class of bug as the CI trap recorded in the plan: analysing with a
# toolchain that did not produce the database.)
# NOTE --query-driver is a clangd COMMAND-LINE flag, not a compile flag — putting it in
# `Add:` passes it to the compiler, which rejects it ("unknown argument") and breaks every
# file. Set it in the editor instead: VS Code → clangd extension → "Clangd: Arguments" →
# add `--query-driver=/usr/bin/c++`. Left here as a comment so the next reader does not
# re-add it to Add:.
Add:
# macOS: clangd is usually Homebrew LLVM while CMake recorded Apple Clang, and a different
# clang cannot find Apple's SDK headers — every file then reports `'cstdint' file not
# found` and real diagnostics vanish.
#
# MACHINE-SPECIFIC, and this file cannot avoid that: `.clangd` has no conditionals and no
# command substitution, so the path is literal and applies on EVERY platform, not only
# macOS. It is correct for Command Line Tools. Change it if `xcrun --show-sdk-path`
# disagrees (full Xcode puts the SDK under /Applications/Xcode.app/...), and delete the
# line on Linux/Windows, where the system clang already knows its own include paths.
# (check_clang_tidy.py has no such limit — it shells out to `xcrun` and gets this right
# automatically. Only the editor needs a hardcoded value.)
- -isysroot/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

Diagnostics:
# The .clang-tidy file at the repo root is picked up automatically; nothing to repeat here.
# This block is for editor-only adjustments that should NOT apply to CI.
ClangTidy:
# Editor-only removals go here. (Anything in .clang-tidy is already applied; repeating a
# name clangd does not know produces a "check was not found" warning on this file.)
Remove: []

# Suppress diagnostics from headers we do not own, so opening one of our files does not
# fill the Problems pane with ESP-IDF or standard-library noise.
Suppress:
- unused-includes
2 changes: 1 addition & 1 deletion .claude/workflows/write-behaviour-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Repo: the current workspace root (the projectMM checkout you're running in) —
- First line MUST be: \`// @module ${cls}\` (this is what the doc generator + MoonDeck read — the whole point is that this module becomes a documented, tested module). Add \`// @also X, Y\` only if the test genuinely also exercises another module.
- Each TEST_CASE gets a single \`//\` comment line ABOVE it describing the behaviour it pins (the generator turns that into the doc description). Write real, present-tense descriptions.
- Assert REAL BEHAVIOUR, not just "renders non-zero". Examples of the bar:
- SolidEffect → the whole buffer is ONE uniform colour (every light equals the configured colour).
- SolidEffect → the whole buffer is ONE uniform color (every light equals the configured color).
- FixedRectangleEffect → only lights inside the configured rect are lit; outside is black; defaults (0,0,0)+(15,15,15) light the origin corner.
- MirrorModifier → a coord and its mirror map to the same logical position; modifyLogicalSize halves the mirrored axis (study the .h for which axis/percentage).
- TransposeModifier → swaps axes (x↔y etc. per the .h); modifyLogicalSize swaps the corresponding size fields.
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: CodeQL

# POC (docs/history/plans/Plan-20260727): does CodeQL's stock C/C++ suite find anything
# real in this firmware? The question that motivated it: we parse six network packet
# formats (ArtNet, DDP, E1.31, WLED audio sync, MQTT, WLED) plus HTTP, doing ~22 memcpy
# operations on data arriving from the LAN, on a device with no MMU and no process
# isolation — and we run NO security analysis at all today. Nothing else in our toolchain
# looks for that class of bug: the compiler checks effects, lizard counts branches, our
# Python checks read text.
#
# `build-mode: none` (GA Oct 2025) scans C/C++ WITHOUT building it, inferring compile flags
# per file. That matters here because our real firmware build is an ESP-IDF cross-compile
# for Xtensa/RISC-V that a runner would have to reproduce; build-free skips that entirely.
# The trade is some extraction noise on macro/template-dense headers — acceptable for a POC
# whose question is "is there anything here", not "prove there is nothing".
#
# Free on public repos, so the cost is wall-clock only. Scheduled weekly + on demand rather
# than per-push: this is a sweep, not a gate, until the POC says otherwise.
#
# If this finds nothing actionable after a few runs, that is a RESULT — record it in the
# scorecard and delete this file.

on:
# Push-triggered on the POC branch, deliberately.
#
# `workflow_dispatch` alone would NOT work here: GitHub only offers the "Run workflow"
# button for workflows that exist on the DEFAULT branch, and this one lives on
# next-iteration until the POC is judged. A push trigger is the only way to get a first
# result without merging an unevaluated workflow into main.
#
# This is still not a gate: it does not run on pull_request, so it cannot block a merge.
# It runs, we read the findings, and we decide.
push:
branches:
- next-iteration
paths:
# Only when C/C++ or the workflow itself changes — a docs commit has nothing new to
# analyse and would just burn a runner.
- 'src/**'
- '.github/workflows/codeql.yml'
workflow_dispatch:
schedule:
# Monday 04:00 UTC. Weekly is enough for a codebase this size, and keeps the signal
# from becoming background noise nobody reads.
- cron: '0 4 * * 1'

permissions:
contents: read
# Required to upload results to the Security tab (this is what buys the alert lifecycle:
# open/fixed/dismissed tracked across runs, i.e. baselining we would otherwise build).
security-events: write

jobs:
analyze:
name: Analyze C/C++
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: c-cpp
build-mode: none
# security-and-quality is the widest stock set: the security queries answer the
# question above, and the quality ones tell us whether CodeQL sees anything our
# existing checks miss. Narrow it later if the noise is not worth it.
queries: security-and-quality

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:c-cpp"
Loading
Loading