Undo-block review fixes: exception-safe depth, corrected docs, real test teeth

Fixes ctor exception safety, a ValidatePtr2 guard, false SDK-citation and
enforcement claims, the span-depth invariant, and vacuous fx_reclaim tests.
This commit is contained in:
2026-08-07 17:07:56 -04:00
parent b7b93a7ac3
commit 3d95eb715c
8 changed files with 126 additions and 38 deletions
+28 -7
View File
@@ -11,13 +11,19 @@ nests over it.
## Invariants
- **`Undo_BeginBlock2` is not callable outside this directory, and that is enforced
by the compiler, not by review.** Only `undo_block.cpp` and `shell/view`'s
`fx_reclaim.cpp` (which owns the span's own begin/end) define
`REAPERAPI_WANT_Undo_BeginBlock2` / `..._Undo_EndBlock2`. Every other TU compiles
under `REAPERAPI_MINIMAL` with no header in its include graph pulling the SDK in
unguarded, so a raw `Undo_BeginBlock2` there is an undeclared identifier. Adding
the WANT define back to a third TU is the one way to defeat this — do not.
- **`Undo_BeginBlock2` is not callable outside this directory in every TU but one, and
that is enforced by the compiler, not by review.** Only `undo_block.cpp` and
`shell/view`'s `fx_reclaim.cpp` (which owns the span's own begin/end) define
`REAPERAPI_WANT_Undo_BeginBlock2` / `..._Undo_EndBlock2`; every other TU compiles
under `REAPERAPI_MINIMAL`, so a raw `Undo_BeginBlock2` there is an undeclared
identifier. **`app/main.cpp` is the one exception** — as the sole
`REAPERAPI_IMPLEMENT` TU it never sets `REAPERAPI_MINIMAL` (it must allocate storage
for whatever ANY TU wants), so the whole API, `Undo_*` included, compiles clean
there too. That TU stays out of the undo business by discipline
(`src/app/CLAUDE.md`: pointers + entry + dispatch only), not because the compiler
stops it — do not add business logic there that would make this matter. Adding the
WANT define back to a third TU is a second, easier way to defeat the enforcement
that DOES exist — do not.
- **Ending the reclaim span is part of opening, and ONLY the outermost block does
it.** At depth 0 the span is the only block that can be open, so ending it mints
the pending mode switch's point whole before this block exists. At depth > 0 an
@@ -26,6 +32,12 @@ nests over it.
is the same criterion `applyMode`'s reapply is gated on; it lives in the
constructor so no call site has to make the judgment. Full reasoning for the rule
itself: `src/shell/view/CLAUDE.md`.
- **The constructor's body is one `try`.** `fxReclaimEndUndoSpan` copies a
`std::string` and can throw; a throwing constructor runs no destructor, so an
uncompensated depth increment would stick forever — `undoBlockOpen()` reads true
for the rest of the session, `fxReclaimTick` refuses every tick, and no later
`UndoBlock` is ever outermost. Any throw between the increment and the end of the
constructor rolls the depth back before propagating.
- **Closing is a destructor.** No throw between the open and the label decision can
leave a block open in the user's project, which is why several call sites can
allocate freely between the two.
@@ -46,6 +58,15 @@ nests over it.
## Gotchas
- **`Undo_EndBlock2` is guarded by `ValidatePtr2` only when `project_` is a real,
non-null pointer.** `nullptr` means "the active project" — the same convention
`Undo_BeginBlock2` was handed — and `ValidatePtr2` does not accept `nullptr` as a
valid object of any type, so guarding it unconditionally would silently stop every
`UndoBlock undo(nullptr)` site (most of them) from ever closing. A real pointer, by
contrast, can go stale between open and close (several call sites allocate freely
in between), so only that case is validated — the same asymmetry `fx_reclaim.cpp`'s
`closeSpan` already has, for the same reason: its span always holds a real pinned
pointer, never `nullptr` for "active".
- The depth counter deliberately does NOT count the reclaim span. The span is the
thing this stands off, not a peer.
- The counter drops AFTER `Undo_EndBlock2`, not before: the close that reaches depth