A small C firmware project with deliberately seeded defects, for evaluating Qodana C/C++ against RTOS-style embedded code. It is a demonstration project rather than real firmware — every defect is intentional, and all of them are documented in SEEDED-DEFECTS.md.
It exists to answer three questions that come up in any real evaluation:
- What does it report on embedded C? The seeded defect inventory, and what to look at first in the report, in SEEDED-DEFECTS.md.
- Can we configure inspections per directory rather than per project?
Yes, at four levels of granularity. See CONFIGURATION.md and the
commented example in
qodana.yaml. - Can we suppress a single line in the code, the way
NOLINTdoes? Yes, in three forms, and one popular guess does not work. Every case is a live test insrc/suppressions.cwith a matching control.
No cross-toolchain needed. zephyr_shim/ and vendor_shim/ provide the shape of the
Zephyr, nRF52 and STM32 HAL APIs so the project builds host-native with clang:
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build buildIt builds and links cleanly. -Wall -Wextra is on but -Werror is not, because the point
is that these defects survive an ordinary build.
C/C++ analysis is driven by a compilation database, not by source globbing. If
compile_commands.json is missing or its paths do not resolve, you get an empty report
rather than an error. This is the single most common way a first C/C++ run goes wrong.
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
qodana scan --linter qodana-clang --compile-commands ./build/compile_commands.jsonRuns clang-tidy, reports each check under its own name (bugprone-signed-bitwise).
On this project: 32 findings.
Running on macOS or any host where the paths differ inside the container: CMake records absolute host paths and host-specific flags (
-arch,-isysroot) intocompile_commands.json, but Qodana mounts the project at/data/project. The result isLLVM ERROR: Cannot chdir into ....tools/normalize-compile-commands.pyrewrites the database for the container. The same class of problem applies to any cross-compiled build with a bespoke SDK path, which is worth knowing before you point this at real firmware.
qodana scan --linter qodana-cppConfigures CMake itself, so it needs no --compile-commands. Adds the CLion data-flow
engine (CppDFA*), which is what finds the array-bounds and lifetime defects clang-tidy
misses here. On this project: 54 findings.
Known issue in 2026.2: the
qodana-cppimage can die at startup withIllegalStateException: Lifetime 'Anonymous' ... Terminated, caused by a missing ICU library in the .NET backend. Workaround until it ships fixed:qodana scan --linter qodana-cpp -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
qodana-clang is the free Community linter and runs clang-tidy. qodana-cpp also runs
clang-tidy, and adds the CLion data-flow engine (CppDFA*) on top: the array-bounds
proofs, the escaping stack address and the unreachable-code findings on this project all
come from there. On this code that is 32 findings against 54.
Because the free linter needs no licence, a common arrangement is qodana-clang on every
pull request and qodana-cpp where the deeper analysis is wanted.
.github/workflows/qodana.yml is a working GitHub Actions
setup. Two things in it matter more than the rest:
- The compilation-database step is where a C/C++ pipeline actually succeeds or fails, and the normalize step after it is not optional. CMake records absolute host paths; the analyser runs in a container that mounts the project elsewhere. Skip the normalize step and every database entry points outside the project, so you get zero translation units — an empty report with no error and a green-looking pipeline. That is why the normalize step is in the workflow rather than left to the reader.
- Findings do not fail the job by default. With no baseline and no threshold they
are published as commit/PR annotations and as GitHub code scanning alerts. On this
project the pipeline is green and all 32 findings appear in the Security tab, which is
the closest analogue to SonarQube Cloud's PR decoration. Add
--fail-threshold Nto make it a hard gate, or a baseline to gate only on new findings — see CONFIGURATION.md.
| Path | What it is |
|---|---|
src/ |
The firmware code: HID report assembly, BLE HID-over-GATT, key matrix scan, battery fuel gauge, sensor state machine, DFU chunk handling |
src/vendor/ |
Vendor-layer code: nRF52 register access (nrf52_keyscan.c) and STM32 HAL (stm32_spi_sensor.c). Different defect classes live at this layer |
src/suppressions.c |
Live test bench for line-level suppression. Every function is tagged EXPECT: reported / EXPECT: suppressed |
src/vendor/nested_config.c |
Proof that a nested .clang-tidy is honoured — one half subtraction, one half inheritance |
zephyr_shim/, vendor_shim/ |
API-shape shims so this builds without a cross-toolchain |
qodana.yaml |
Tuned profile and the commented scoping example |
.clang-tidy, src/vendor/.clang-tidy |
Root config and per-directory override |
tools/normalize-compile-commands.py |
Makes a host compilation database usable inside the container |
.github/workflows/qodana.yml |
Working GitHub Actions workflow, including the compilation-database step |
| File | Contents |
|---|---|
| CONFIGURATION.md | Per-directory and per-line inspection control, with the behaviour and limits of each mechanism |
| SEEDED-DEFECTS.md | Every seeded defect in the project, and what to look at first in the report |