doc/claude/code-style.md
scripts/code-verify.py enforces this — read its --check output, don't re-derive the rules.
The compressed essentials live inline in CLAUDE.md; this doc is the full specification.
int* p, const Foo& r).RemoveBracesLLVM strips braces from single-statement bodies. BinPackArguments /
BinPackParameters = false (one per line when wrapping).clang-format.| Kind | Convention | Example |
|---|---|---|
| Classes / Enums | CamelCase | FrameReader, BusType |
| Functions | camelCase | hotpathRxFrame |
| Locals / params | lower_case | frame_data |
| Static vars | s_lower_case | s_devices |
| Private members | m_camelCase | m_deviceIndex |
| Public/protected members | lower_case | sourceId |
| Constants / constexpr | kCamelCase | kMaxBufferSize |
| Macros | UPPER_CASE | BUILD_COMMERCIAL |
if (!frame.isValid())
return;
for (const auto& g : frame.groups())
{
if (!g.isEnabled())
continue;
processGroup(g);
}
Reference: app/src/IO/Drivers/BluetoothLE.h. Order: Q_OBJECT → Q_PROPERTY block
(clang-format off, one attribute per line) → signals: → private ctor + deleted copy/move
(singletons) → public: (instance() first, then [[nodiscard]] getters) → public slots:
→ private slots: → private: helpers → private: members.
[[nodiscard]] on every non-void return.Q_INVOKABLE void — public slots:. Q_INVOKABLE is for non-void returns only.noexcept on trivial const getters that only read members.int m_foo = 0; is forbidden). Use the ctor init list.Q_EMIT, never bare emit.signals: / public slots: / private slots: — never Q_SIGNALS: / public Q_SLOTS:.connect() on one line; long form one arg per line. Never SIGNAL() / SLOT().disconnect(nullptr) as the slot — capture the QMetaObject::Connection and
disconnect that.parseFunction.call() directly on a QJSEngine parser — always
IScriptEngine::guardedCall().Code is the spec. Comments label sections; they don't narrate.
Headers (.h) — only two kinds of comments allowed:
/** @brief ... */ directly above every type-level definition: classes, structs, enum /
enum class, top-level typedef, top-level using-aliases. One @brief per definition
— helper structs and payload typedefs need their own, not just the primary class.No function doxygen above member declarations, no trailing /**< ... */, no multi-tag
verbose blocks, no inline //. Names + types are the documentation. Exempt from @brief:
forward declarations, nested types inside a class body, using Base::Base; imports, type
aliases declared inside a function body.
Source (.cpp) — every function definition gets a one-line /** @brief ... */ directly
above it. Ctors, dtors, slots, helpers, every one. No @param/@return/@note. Use 98-dash
//--- banners for concern groups between functions. No comments inside a function body.
Functions are capped at 100 lines, so the @brief above the function plus self-explanatory code
carry it: a comment that restates or narrates the next line gets deleted, and a load-bearing
why is folded up into the @brief (lengthen the brief if needed — the brief is the right home
for the why). A genuinely-needed in-body note (a literal lookup table, a derivation, a citation)
stays behind a reviewed // code-verify off / on fence. code-verify.py flags every in-body
comment as cxx-inbody-comment (advisory; tree-sitter-located, so the @brief above the
function is never caught, and tooling pragmas like // clang-format/// NOLINT/// fallthrough
are skipped). Forbidden: inline EOL comments, multi-line // prose, /* ... */ inside
function bodies, restating the code, AI narration ("we", "Note that", tutorial voice, "this used
to...", hedging, bare TODO).
Don't fake the em-dash. Source and user-facing Markdown are ASCII-only, so the em-dash
glyph (U+2014) is out — but the fix is to rewrite the sentence, not to swap in a spaced
double-hyphen --. -- as a sentence dash is a mechanical glyph trade that reads like a
robot did the edit; recast with a comma, period, or parentheses instead. The point of
the rule is human, considered prose, not one dash glyph for another. code-verify.py flags it
in comments (comment-dash-substitute, advisory) and documentation-verify.py in docs
(style-dash-substitute); i--, --i, and //--- banners don't match (the rule needs a
space on both sides). The whole codebase carries baseline -- debt, so both ship as advisory
— new prose should still clear them.
id first, blank line after.font: Cpp_Misc_CommonFonts.uiFont etc. Individual font.* sub-properties
only in dashboard widgets that compute dynamic pixel sizes (zoom-dependent).Q_PROPERTY + NOTIFY. No comma-expression hacks.SerialStudio.BusType, ProjectModel.SomeEnum. Never hardcoded integers.// comments mid-statement; section headers on their own line only.Font helpers: uiFont, boldUiFont, monoFont, customUiFont(fraction, bold),
customMonoFont(fraction, bold), widgetFont(fraction, bold). Scales: kScaleSmall=0.85,
kScaleNormal=1.0, kScaleLarge=1.25, kScaleExtraLarge=1.50.
[[likely]]/[[unlikely]], static-cached singletons.CircularBuffer::findFirstOfPatterns() for multi (single-pass,
stack array ≤8, no heap).constexpr CRC tables. Profile first.SPDX headers required: GPL-3.0-only, LicenseRef-SerialStudio-Commercial, or both.
Validate at system boundaries only (API input, file I/O, network). Trust internal data.
Mission-critical telemetry. Hotpath violations are blockers.
goto/setjmp/longjmp. No unbounded recursion — every recursive function has a
hard depth cap (FrameParser::parseMultiFrame ≤2, JsonValidator ≤128,
Taskbar::findItemByWindowId ≤3, ConversionUtils ≤64).kMaxIterations.
while(true) only with a provable termination invariant — document it.new/make_shared/.append() on the
dashboard path. FrameBuilder::acquireFrame() draws each TimestampedFramePtr from a
fixed-size slot pool (kFramePoolSize = 1024); the slot is recycled when the last
consumer drops the shared_ptr (custom deleter flips inUse to false). Don't bypass the
pool with a direct std::make_shared<TimestampedFrame>(...) on the hotpath — that
re-introduces a per-frame heap alloc. Pool exhaustion logs once and falls back to
make_shared so the producer never blocks. The perf-* advisories (code-verify.py)
catch accidental hot-path allocation, regex construction, locking, logging, throwing,
large by-value params, shared_ptr by-value, runtime divide/modulo, pow(),
dynamic_cast, virtual calls, large stack buffers, false-sharing, recursion in hot loops.Q_ASSERT for
debug; if (!cond) return; for release safety. No assert(true).[[nodiscard]]
everywhere. try_enqueue() failures must be logged. JS calls go through
IScriptEngine::guardedCall(), never direct.#include, #pragma once, #ifdef BUILD_COMMERCIAL /
ENABLE_GRPC, platform guards. No token pasting, no variadic macros.reinterpret_cast except byte-level access (const uint8_t*). Prefer
std::bit_cast. No raw function pointers. No dynamic_cast on the hotpath — refactor
to a tag or invariant-checked static_cast.-Wall -Wextra -Wpedantic, ENABLE_HARDENING for production. Fix
root cause; never suppress without justification.