From 583dedf9ade1ce5864d91e411b14b6e85cff2600 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 15 Oct 2026 00:07:24 +0200 Subject: [PATCH] debugger: list and clear breakpoints set on functions `sb('fn()')` used the `debug()` command line API, which gives no breakpoint id, so these breakpoints never showed up in `breakpoints` and could not be removed with `cb`. Use `Debugger.setBreakpointOnFunctionCall` instead and keep the breakpoint in the list, so it is listed as `fn()` and can be cleared with `cb('fn()')`. Fixes: https://github.com/nodejs/node/issues/41790 Co-authored-by: Dan Rose Signed-off-by: dualfroz --- doc/api/debugger.md | 1 + lib/internal/debugger/inspect_repl.js | 37 ++++++++++------- test/parallel/test-debugger-break.js | 5 ++- .../test-debugger-function-breakpoints.js | 41 +++++++++++++++++++ 4 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 test/parallel/test-debugger-function-breakpoints.js diff --git a/doc/api/debugger.md b/doc/api/debugger.md index c7490af82924..ba526d867b9f 100644 --- a/doc/api/debugger.md +++ b/doc/api/debugger.md @@ -132,6 +132,7 @@ To begin watching an expression, type `watch('my_expression')`. The command evaluates to `true` * `clearBreakpoint('script.js', 1)`, `cb(...)`: Clear breakpoint in `script.js` on line 1 +* `clearBreakpoint('fn()')`, `cb(...)`: Clear breakpoint set on `fn` It is also possible to set a breakpoint in a file (module) that is not loaded yet: diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index 69ca174dd241..0476d1938222 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -732,7 +732,8 @@ function createRepl(inspector) { return; } - function formatLocation(location) { + function formatBreakpoint({ location, functionName }) { + if (functionName !== undefined) return `${functionName}()`; if (!location) return ''; const script = knownScripts[location.scriptId]; const scriptUrl = script ? script.url : location.scriptUrl; @@ -741,7 +742,7 @@ function createRepl(inspector) { const breaklist = ArrayPrototypeJoin( ArrayPrototypeMap( knownBreakpoints, - (bp, idx) => `#${idx} ${formatLocation(bp.location)}`), + (bp, idx) => `#${idx} ${formatBreakpoint(bp)}`), '\n'); print(breaklist); } @@ -779,20 +780,23 @@ function createRepl(inspector) { // setBreakpoint('fn()'): Break when a function is called if (StringPrototypeEndsWith(script, '()')) { - const debugExpr = `debug(${script.slice(0, -2)})`; - const debugCall = selectedFrame ? + const functionName = StringPrototypeSlice(script, 0, -2); + const evaluation = selectedFrame ? Debugger.evaluateOnCallFrame({ callFrameId: selectedFrame.callFrameId, - expression: debugExpr, - includeCommandLineAPI: true, + expression: functionName, }) : - Runtime.evaluate({ - expression: debugExpr, - includeCommandLineAPI: true, - }); - return PromisePrototypeThen(debugCall, ({ result, wasThrown }) => { + Runtime.evaluate({ expression: functionName }); + return PromisePrototypeThen(evaluation, ({ result, wasThrown }) => { if (wasThrown) return convertResultToError(result); - return undefined; // This breakpoint can't be removed the same way + if (result.type !== 'function') { + throw new ERR_DEBUGGER_ERROR(`${functionName} is not a function`); + } + return PromisePrototypeThen( + Debugger.setBreakpointOnFunctionCall({ objectId: result.objectId, condition }), + ({ breakpointId }) => { + ArrayPrototypePush(knownBreakpoints, { breakpointId, functionName }); + }); }); } @@ -854,7 +858,10 @@ function createRepl(inspector) { } function clearBreakpoint(url, line) { - const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location }) => { + const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location, functionName }) => { + if (line === undefined) { + return functionName !== undefined && `${functionName}()` === url; + } if (!location) return false; const script = knownScripts[location.scriptId]; if (!script) return false; @@ -864,7 +871,7 @@ function createRepl(inspector) { ); }); if (!breakpoint) { - print(`Could not find breakpoint at ${url}:${line}`); + print(`Could not find breakpoint at ${line === undefined ? url : `${url}:${line}`}`); return PromiseResolve(); } return PromisePrototypeThen( @@ -879,7 +886,7 @@ function createRepl(inspector) { const lastBreakpoints = ArrayPrototypeSplice(knownBreakpoints, 0); const newBreakpoints = ArrayPrototypeMap( ArrayPrototypeFilter(lastBreakpoints, - ({ location }) => !!location.scriptUrl), + ({ location }) => !!location?.scriptUrl), ({ location }) => setBreakpoint(location.scriptUrl, location.lineNumber + 1)); if (!newBreakpoints.length) return PromiseResolve(); diff --git a/test/parallel/test-debugger-break.js b/test/parallel/test-debugger-break.js index 09481480c35e..be814488467f 100644 --- a/test/parallel/test-debugger-break.js +++ b/test/parallel/test-debugger-break.js @@ -62,7 +62,8 @@ const cli = startCLI([script]); await cli.command('breakpoints'); assert.ok(cli.output.includes(`#0 ${script}:6`)); - assert.ok(cli.output.includes(`#1 ${script}:16`)); + assert.ok(cli.output.includes('#1 otherFunction()')); + assert.ok(cli.output.includes(`#2 ${script}:16`)); await cli.command('list()'); assert.match( @@ -105,7 +106,7 @@ const cli = startCLI([script]); await cli.stepCommand(''); assert.ok( - cli.output.includes(`debugCommand in ${script}:14`), + cli.output.includes(`break in ${script}:14`), 'found function breakpoint we set above'); })().finally(() => cli.quit()) .then(common.mustCall()); diff --git a/test/parallel/test-debugger-function-breakpoints.js b/test/parallel/test-debugger-function-breakpoints.js new file mode 100644 index 000000000000..576b96fdbfe9 --- /dev/null +++ b/test/parallel/test-debugger-function-breakpoints.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const fixtures = require('../common/fixtures'); +const startCLI = require('../common/debugger'); + +const assert = require('assert'); +const path = require('path'); + +const scriptFullPath = fixtures.path('debugger', 'break.js'); +const script = path.relative(process.cwd(), scriptFullPath); +const cli = startCLI([script]); + +(async () => { + await cli.waitForInitialBreak(); + await cli.waitForPrompt(); + + await cli.command('sb("sayHello()")'); + await cli.command('sb("otherFunction()")'); + await cli.command('breakpoints'); + assert.ok(cli.output.includes('#0 sayHello()')); + assert.ok(cli.output.includes('#1 otherFunction()')); + + await cli.command('sb("x()")'); + assert.match(cli.output, /x is not a function/); + + await cli.command('cb("otherFunction()")'); + await cli.command('cb("missing()")'); + assert.match(cli.output, /Could not find breakpoint at missing\(\)/); + await cli.command('breakpoints'); + assert.ok(cli.output.includes('#0 sayHello()')); + assert.ok(!cli.output.includes('otherFunction()')); + + await cli.stepCommand('cont'); + assert.ok( + cli.output.includes(`break in ${script}:5`), + 'pauses on the first statement of sayHello'); +})().finally(() => cli.quit()) + .then(common.mustCall());