Skip to content

Commit 86a37f5

Browse files
committed
Add README with build/usage instructions and benchmark numbers
Benchmark: one real run against spdlog (v1.17.0, commit 989d28d), ~27.5k LOC across include/+src/, 7 TUs. Average of 3 runs on this machine: 1.76s wall clock, ~15,650 LOC/s, ~189 MiB peak RSS. Also turned up a real pre-existing redundant-move-return case in spdlog's own pattern_formatter-inl.h.
1 parent 8c30889 commit 86a37f5

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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

Comments
 (0)