redefine_classes re-plants breakpoints purely by line number, discarding the addressing mode the breakpoint was created with. A hot-swap that shifts line numbers therefore rebinds content-anchored breakpoints to whatever now happens to live at the old line — silently, and reporting verified: true.
The general principle
Any marker with a richer identity than "line N" should have that identity honored whenever line numbers are invalidated. restart_debugging already does this; redefine_classes does not.
| Marker |
Identity |
On redefine today |
Should be |
| Line breakpoint |
line |
remapped by line |
✅ correct as-is — that's what was asked for |
statement:-anchored breakpoint |
content |
remapped by line ❌ |
re-resolve content against the new source |
| Function breakpoint |
symbol name |
rebound by name |
✅ already correct (verified: rebound to the new line after a swap) |
condition |
— |
preserved through replant |
✅ (but should travel with a moved anchor) |
suspendPolicy |
— |
preserved through replant |
✅ |
Logpoint (logMessage) |
line + message |
n/a — Java adapter has no logpoint support |
flag for adapters that do, if hot-reload ever extends beyond Java |
expectedContent |
set-time assertion |
not persisted |
✅ intentionally not an anchor — noting so it isn't conflated with statement: |
So the concrete gap is one row: statement anchors are ignored on redefine. The rest is either already right or not applicable — but the rule should be stated once so future marker kinds inherit it.
Repro
examples/java/RedefineTargetV2.java has a 3-line-longer header than RedefineTarget.java, so every line shifts by +3 after the swap:
|
RedefineTarget.java |
RedefineTargetV2.java |
return 42/99; |
11 |
14 |
println("val1 = " …) |
19 |
23 |
println("val2 = " …) |
23 |
26 |
- breakpoint at line 19, launch, pause in
main
- recompile V2 over the same class file,
redefine_classes → replantedBreakpoints: 1
- breakpoint at line 23, continue
Actual: never stops; program runs to completion. Line 23 in the new line table is println("val1 …") — a real location, already executed. locationsOfLine(23) succeeds, so verified: true is technically honest and practically misleading.
Control: the same breakpoint with no redefine stops correctly at RedefineTarget.main:23. Using the correct post-swap line (26) also stops correctly, with val1 = 42, val2 = 99. A function breakpoint on getValue set after the redefine fires at the new line 15. So the replant machinery is sound — only the addressing mode is lost.
Where it lives
JdiDapServer.replantBreakpointsAfterRedefine() deletes the stale BreakpointRequests (JDI cancels them without notification) and replays the deferred-breakpoint path, which bottoms out at:
List<Location> locs = refType.locationsOfLine(line); // the entire "mapping"
BreakpointRequest bpr = erm.createBreakpointRequest(locs.get(0));
The bridge never learns the mode: statement: → line resolution happens server-side (src/server.ts), and DAP setBreakpoints only carries line numbers on the wire. The anchor is retained on the session (bp.anchor, surfaced by list_breakpoints) — it's just never consulted here.
Proposed fix
SessionManagerOperations.reresolveAnchors() already does exactly this job, including the awkward parts: fresh file read (bypassing the LineReader cache), current line as the nearLine hint so duplicate statements re-anchor to the nearest occurrence, multi-candidate matches flagged as moved[].candidates (#379), and unmatched anchors keeping their stale line with a warning rather than being dropped (#271).
It has exactly one caller: restart_debugging (session-manager-operations.ts:1076). redefineClasses (same file, ~2952) forwards straight to the proxy.
Wire it into the redefine path and return the same anchorResolution: { moved, stale } shape restart_debugging already returns, so a line-shifting hot-swap reports which breakpoints moved and which went stale.
Not a drop-in. On restart the relaunch re-sends every breakpoint, so mutating bp.line suffices. A redefine has no relaunch — it needs reresolveAnchors plus an explicit setBreakpoints push for the affected files, ordered after vm.redefineClasses so the JDI replant resolves against the new line table.
Caveat worth documenting either way: content re-resolution only helps when the source on disk is the new source. In the real edit → recompile → hot-swap loop that holds by construction.
Drive-by
replantBreakpointsAfterRedefine discards the function-breakpoint return value:
int replanted = handleClassPrepared(refType);
handleClassPreparedForFunctionBreakpoints(refType); // return value dropped
return replanted;
so replantedBreakpoints under-reports whenever function breakpoints were re-planted.
Test
Redefine a class whose header grew by N lines; assert a statement:-anchored breakpoint follows the statement (and that a plain line: breakpoint does not — the distinction is the feature). The existing fixtures need a fix first: RedefineTargetV2.java can't be compiled directly (javac rejects it — the public class inside is RedefineTarget), so it must be staged under the matching filename. Padding its header to line-align with RedefineTarget.java would make the drift-free case the default and keep this fixture usable for the non-shifting scenario.
Found during a full /testdebugger sweep (9 servers × 3 backends); hot-swap itself verified working on all 9.
redefine_classesre-plants breakpoints purely by line number, discarding the addressing mode the breakpoint was created with. A hot-swap that shifts line numbers therefore rebinds content-anchored breakpoints to whatever now happens to live at the old line — silently, and reportingverified: true.The general principle
Any marker with a richer identity than "line N" should have that identity honored whenever line numbers are invalidated.
restart_debuggingalready does this;redefine_classesdoes not.statement:-anchored breakpointconditionsuspendPolicylogMessage)expectedContentstatement:So the concrete gap is one row: statement anchors are ignored on redefine. The rest is either already right or not applicable — but the rule should be stated once so future marker kinds inherit it.
Repro
examples/java/RedefineTargetV2.javahas a 3-line-longer header thanRedefineTarget.java, so every line shifts by +3 after the swap:RedefineTarget.javaRedefineTargetV2.javareturn 42/99;println("val1 = " …)println("val2 = " …)mainredefine_classes→replantedBreakpoints: 1Actual: never stops; program runs to completion. Line 23 in the new line table is
println("val1 …")— a real location, already executed.locationsOfLine(23)succeeds, soverified: trueis technically honest and practically misleading.Control: the same breakpoint with no redefine stops correctly at
RedefineTarget.main:23. Using the correct post-swap line (26) also stops correctly, withval1 = 42, val2 = 99. A function breakpoint ongetValueset after the redefine fires at the new line 15. So the replant machinery is sound — only the addressing mode is lost.Where it lives
JdiDapServer.replantBreakpointsAfterRedefine()deletes the staleBreakpointRequests (JDI cancels them without notification) and replays the deferred-breakpoint path, which bottoms out at:The bridge never learns the mode:
statement:→ line resolution happens server-side (src/server.ts), and DAPsetBreakpointsonly carries line numbers on the wire. The anchor is retained on the session (bp.anchor, surfaced bylist_breakpoints) — it's just never consulted here.Proposed fix
SessionManagerOperations.reresolveAnchors()already does exactly this job, including the awkward parts: fresh file read (bypassing the LineReader cache), current line as thenearLinehint so duplicate statements re-anchor to the nearest occurrence, multi-candidate matches flagged asmoved[].candidates(#379), and unmatched anchors keeping their stale line with a warning rather than being dropped (#271).It has exactly one caller:
restart_debugging(session-manager-operations.ts:1076).redefineClasses(same file, ~2952) forwards straight to the proxy.Wire it into the redefine path and return the same
anchorResolution: { moved, stale }shaperestart_debuggingalready returns, so a line-shifting hot-swap reports which breakpoints moved and which went stale.Not a drop-in. On restart the relaunch re-sends every breakpoint, so mutating
bp.linesuffices. A redefine has no relaunch — it needsreresolveAnchorsplus an explicitsetBreakpointspush for the affected files, ordered aftervm.redefineClassesso the JDI replant resolves against the new line table.Caveat worth documenting either way: content re-resolution only helps when the source on disk is the new source. In the real edit → recompile → hot-swap loop that holds by construction.
Drive-by
replantBreakpointsAfterRedefinediscards the function-breakpoint return value:so
replantedBreakpointsunder-reports whenever function breakpoints were re-planted.Test
Redefine a class whose header grew by N lines; assert a
statement:-anchored breakpoint follows the statement (and that a plainline:breakpoint does not — the distinction is the feature). The existing fixtures need a fix first:RedefineTargetV2.javacan't be compiled directly (javacrejects it — the public class inside isRedefineTarget), so it must be staged under the matching filename. Padding its header to line-align withRedefineTarget.javawould make the drift-free case the default and keep this fixture usable for the non-shifting scenario.Found during a full
/testdebuggersweep (9 servers × 3 backends); hot-swap itself verified working on all 9.