Skip to content

Repository files navigation

Qodana C/C++ on embedded firmware

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:

  1. What does it report on embedded C? The seeded defect inventory, and what to look at first in the report, in SEEDED-DEFECTS.md.
  2. 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.
  3. Can we suppress a single line in the code, the way NOLINT does? Yes, in three forms, and one popular guess does not work. Every case is a live test in src/suppressions.c with a matching control.

Build it

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 build

It builds and links cleanly. -Wall -Wextra is on but -Werror is not, because the point is that these defects survive an ordinary build.

Scan it

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.

qodana-clang — Community, no licence required

cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
qodana scan --linter qodana-clang --compile-commands ./build/compile_commands.json

Runs 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) into compile_commands.json, but Qodana mounts the project at /data/project. The result is LLVM ERROR: Cannot chdir into .... tools/normalize-compile-commands.py rewrites 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-cpp — the CLion engine

qodana scan --linter qodana-cpp

Configures 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-cpp image can die at startup with IllegalStateException: 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

How the two relate

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.

Continuous integration

.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 N to make it a hard gate, or a baseline to gate only on new findings — see CONFIGURATION.md.

What is in here

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

Documentation

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

About

Worked example: Qodana C/C++ on embedded firmware — per-directory inspection scoping, nested .clang-tidy composition, and line-level suppression, all measured

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages