|
| 1 | +# cpp-sentinel |
| 2 | + |
| 3 | +A command-line static analyzer for C++17/20, built on Clang LibTooling, AST |
| 4 | +Matchers, and a hand-rolled lock-order graph analysis. Loads a |
| 5 | +`compile_commands.json`, analyzes real translation units, and reports |
| 6 | +defects with precise source locations. |
| 7 | + |
| 8 | +This is the MVP scoped in [`docs/spec.md`](docs/spec.md): three rules, |
| 9 | +text/JSON output, YAML config with suppression, and one flagship |
| 10 | +cross-file rule with real graph-algorithm teeth. See |
| 11 | +[`docs/architecture.md`](docs/architecture.md) for how it's built |
| 12 | +internally. |
| 13 | + |
| 14 | +## Rules |
| 15 | + |
| 16 | +| Rule | Kind | What it catches | |
| 17 | +|---|---|---| |
| 18 | +| `missing-override` | AST | A virtual method that overrides a base method without the `override` keyword. | |
| 19 | +| `redundant-move-return` | AST | `return std::move(local);`, which suppresses NRVO for no benefit. | |
| 20 | +| `lock-order-inversion` | CFG + cross-file graph | A cycle in lock acquisition order across the whole project (a potential deadlock), found via Tarjan's SCC over a project-wide lock graph. | |
| 21 | + |
| 22 | +`lock-order-inversion` is the flagship: it tracks which locks are held |
| 23 | +through each function (explicit `lock()`/`unlock()` and |
| 24 | +`lock_guard`/`unique_lock` RAII scopes), accumulates that as facts across |
| 25 | +every translation unit in the run, and only after all of them are |
| 26 | +processed builds the merged graph and looks for cycles. Two files can each |
| 27 | +be individually lock-order-safe and still form a cycle once merged -- |
| 28 | +that's the case the flagship is built to catch (see |
| 29 | +`tests/projects/lock-cycle/`). |
| 30 | + |
| 31 | +## Building |
| 32 | + |
| 33 | +Requires CMake 3.20+, a C++20 compiler, and LLVM/Clang dev packages |
| 34 | +(headers + the CMake config files, i.e. |
| 35 | +`LLVMConfig.cmake`/`ClangConfig.cmake`). Developed and CI-tested against |
| 36 | +LLVM/Clang 22; other reasonably recent versions likely work but aren't |
| 37 | +verified. |
| 38 | + |
| 39 | +```bash |
| 40 | +# macOS (Homebrew) |
| 41 | +brew install llvm cmake |
| 42 | + |
| 43 | +# Ubuntu/Debian |
| 44 | +wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 22 all |
| 45 | +``` |
| 46 | + |
| 47 | +```bash |
| 48 | +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release |
| 49 | +cmake --build build -j"$(nproc)" |
| 50 | +ctest --test-dir build --output-on-failure |
| 51 | +``` |
| 52 | + |
| 53 | +GoogleTest and yaml-cpp are pulled in automatically via `FetchContent` -- |
| 54 | +no need to install them separately. On macOS, CMake is pointed at |
| 55 | +`/opt/homebrew/opt/llvm` by default; override with |
| 56 | +`-DCMAKE_PREFIX_PATH=<your LLVM prefix>` if yours lives elsewhere. |
| 57 | + |
| 58 | +## Usage |
| 59 | + |
| 60 | +```bash |
| 61 | +cpp-sentinel analyze --compile-commands build/compile_commands.json |
| 62 | + |
| 63 | +cpp-sentinel analyze --compile-commands build/compile_commands.json \ |
| 64 | + --format json --output report.json |
| 65 | + |
| 66 | +cpp-sentinel list-rules |
| 67 | +cpp-sentinel explain lock-order-inversion |
| 68 | +``` |
| 69 | + |
| 70 | +Key `analyze` options: `--format text|json`, `--output <file>`, |
| 71 | +`--enable`/`--disable <rule-id>` (repeatable), `--config <file.yaml>`, |
| 72 | +`--include`/`--exclude <glob>` (repeatable), `--fail-on warning|error`. |
| 73 | + |
| 74 | +Exit codes: `0` clean, `1` findings at/above `--fail-on`, `2` usage/config |
| 75 | +error, `4` a translation unit failed to compile. |
| 76 | + |
| 77 | +### Config file |
| 78 | + |
| 79 | +```yaml |
| 80 | +rules: |
| 81 | + enable: [missing-override, redundant-move-return] |
| 82 | + disable: [lock-order-inversion] |
| 83 | +``` |
| 84 | +
|
| 85 | +### Suppression |
| 86 | +
|
| 87 | +```cpp |
| 88 | +int x = risky(); // cpp-sentinel-ignore some-rule |
| 89 | + |
| 90 | +// cpp-sentinel-disable some-rule |
| 91 | +... code some-rule won't be applied to ... |
| 92 | +// cpp-sentinel-enable some-rule |
| 93 | +``` |
| 94 | + |
| 95 | +Either form works with no rule id too (`// cpp-sentinel-ignore` suppresses |
| 96 | +every rule on that line). |
| 97 | + |
| 98 | +## Demonstration |
| 99 | + |
| 100 | +1. Build a small multithreaded sample with an intentional lock-order bug |
| 101 | + spanning two files (`tests/projects/lock-cycle/` is exactly this). |
| 102 | +2. `cpp-sentinel analyze --compile-commands build/compile_commands.json` |
| 103 | + -- shows the missing-`override` and redundant-move-return catches. |
| 104 | +3. The same run reports the cross-file lock cycle, with both acquisition |
| 105 | + sites' source locations. |
| 106 | +4. Re-run with `--format json` to show the structured output. |
| 107 | + |
| 108 | +## Benchmark |
| 109 | + |
| 110 | +One real run against [spdlog](https://github.com/gabime/spdlog) at commit |
| 111 | +[`989d28d`](https://github.com/gabime/spdlog/commit/989d28dd768adef702538634857942db24e1058a) |
| 112 | +(v1.17.0) -- a real-world, mutex-heavy C++ logging library, ~27.5k LOC |
| 113 | +across `include/` + `src/`, 7 translation units in its compiled-library |
| 114 | +configuration (`-DSPDLOG_BUILD_EXAMPLE=OFF -DSPDLOG_BUILD_TESTS=OFF`). |
| 115 | +Measured on an Apple M-series machine, Release build, single-threaded |
| 116 | +(no parallel scheduler in the MVP), average of 3 runs: |
| 117 | + |
| 118 | +| Metric | Value | |
| 119 | +|---|---| |
| 120 | +| Wall clock | 1.76 s | |
| 121 | +| Throughput | ~15,650 LOC/s | |
| 122 | +| Peak RSS | ~189 MiB (198 MB) | |
| 123 | + |
| 124 | +It also found a real, pre-existing `redundant-move-return` case in |
| 125 | +spdlog's own `pattern_formatter-inl.h`. |
| 126 | + |
| 127 | +This is one honest number, not a benchmark suite -- see |
| 128 | +[`docs/spec.md`](docs/spec.md) §10/§14 for what's deliberately out of |
| 129 | +scope for the MVP (parallel/incremental analysis, a benchmark corpus, |
| 130 | +scaling studies). |
| 131 | + |
| 132 | +## Known limitations |
| 133 | + |
| 134 | +- Single-threaded, no incremental caching (analyzes every TU from scratch |
| 135 | + every run). |
| 136 | +- `lock-order-inversion`: no alias analysis (two pointers to the same |
| 137 | + mutex are different lock ids unless they resolve to the same |
| 138 | + `VarDecl`/`FieldDecl`), loop bodies walked once rather than to a |
| 139 | + dataflow fixed point, only `lock_guard`/`unique_lock` RAII wrappers |
| 140 | + recognized (not `scoped_lock`), `unique_lock`'s `defer_lock` form is |
| 141 | + treated as an immediate acquisition. |
| 142 | +- `redundant-move-return` doesn't verify all of a function's return paths |
| 143 | + agree on the same NRVO candidate. |
| 144 | +- Text/JSON output only (no SARIF yet -- the JSON schema is deliberately |
| 145 | + close enough that adding one later is a serialization change, not a |
| 146 | + redesign). |
| 147 | + |
| 148 | +Full roadmap for what comes after the MVP is in |
| 149 | +[`docs/spec.md`](docs/spec.md) §14. |
| 150 | + |
| 151 | +## License |
| 152 | + |
| 153 | +MIT -- see [LICENSE](LICENSE). |
0 commit comments