Skip to content

AST-based breakpoint validation#317

Open
chrisdp wants to merge 8 commits intomasterfrom
feature/smarter-breakpoint-location-setting
Open

AST-based breakpoint validation#317
chrisdp wants to merge 8 commits intomasterfrom
feature/smarter-breakpoint-location-setting

Conversation

@chrisdp
Copy link
Copy Markdown
Collaborator

@chrisdp chrisdp commented Mar 28, 2026

Summary

  • Adds AST-based breakpoint validation that marks breakpoints on non-executable lines (function headers, end statements, comments, imports, etc.) as failed with No executable code at this line
  • Validates breakpoints for all debugger types (telnet and debug protocol), not just telnet
  • Splits writeBreakpointsForProject into two clearly-scoped methods: resolveBreakpointsForProject (runs for all debuggers) and injectBreakpointsForProject (telnet only — writes STOP statements and registers permanent breakpoints)
  • Files not referenced by the project return no message so other debuggers can claim the breakpoint

Test plan

  • Breakpoints set on executable lines verify successfully in both telnet and debug protocol sessions
  • Breakpoints on sub/function headers, end sub, end function, comments, import, library statements are marked failed with No executable code at this line
  • JSON and other non-BrightScript files get no failure message (so other debuggers can claim them)
  • Existing telnet STOP injection behaviour is unchanged

🤖 Generated with Claude Code

chrisdp and others added 7 commits March 28, 2026 14:54
…d return type

Replaces the old whitelist walk (which had a dead-code short-circuit making it
always return true) with a correct blacklist implementation:

- Walks depth-first, finding the innermost statement that *starts* on the target
  line; separately tracks block-end lines (end if/for/while) via range check
- Rejects known non-executable statement types (comments, imports, declarations,
  labels, dim, const, enum/interface members, etc.) rather than requiring every
  executable type to be enumerated
- Function/sub header lines are valid breakpoints; end function/sub are not
- Returns LineValidationResult {isExecutable, correctedLine?} instead of boolean,
  laying the groundwork for future breakpoint-position correction
- Rewrites tests to use an executable(...lines: [string, boolean][]) helper so
  expected values sit inline next to each source line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- end if / end for / end while: now non-executable (removed hasBlockEnd logic entirely)
- dim / label / standalone end: now executable (removed from blacklist)
- Simplifies the walk: any line where no statement starts is unconditionally non-executable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now that function headers are valid breakpoint locations, sub main() on line 1
is staged as a permanent breakpoint and correctly comes back verified:true —
the original test expectations hold without the workaround.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nguage

- Rename method: isStagingLineExecutable → isValidBreakpointLine
- Rename helper: isNonExecutableLine → isInvalidBreakpointLine
- Rename result field: isExecutable → isValid on LineValidationResult
- Update all JSDoc, comments, and section headers to use "valid/invalid
  breakpoint location" language instead of "executable/non-executable"
- Rename test helper: executable() → checkLines() and update its JSDoc
- Update all test names to use "valid/invalid breakpoint location" phrasing
- Fix integration test that used sub header (now valid) as the invalid line;
  switch to end sub instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chrisdp chrisdp marked this pull request as ready for review March 31, 2026 13:08
Comment on lines +1204 to +1206
await this.breakpointManager.resolveBreakpointsForProject(this.projectManager.mainProject);
} else {
await this.breakpointManager.injectBreakpointsForProject(this.projectManager.mainProject);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this branching logic inside of a single call? Would prefer avoiding branchout out here if possible.


await this.breakpointManager.writeBreakpointsForProject(this.projectManager.mainProject);
if (this.enableDebugProtocol) {
await this.breakpointManager.resolveBreakpointsForProject(this.projectManager.mainProject);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's find different names for these.

"write" still feels good for when it actually is rewriting the code.

"resolve" is the ambiguous one, it feels like it's more like "scan the project and make sure that every breakpoint is allowed to be at this spot, or throw out the ones that aren't allowed or move them". So you know, a good function name for that. ;)

* Write "stop" lines into source code for each breakpoint of each file in the given project
* Validate breakpoints against the project's staging files and fail any that cannot be placed
* (non-executable lines, unsupported file types, files not in the project).
* Called for all debugger types. Returns the resolved breakpoints keyed by staging file path.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Called for all debugger types. Returns the resolved breakpoints keyed by staging file path.
* Returns the resolved breakpoints keyed by staging file path.

bp.verified = false;
bp.reason = 'failed';
//only set a message for file types we understand — for unknown file types
//(JSON, etc.) leave the message empty so other debuggers can claim the breakpoint
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment feels incorrect, "claim the breakpoint"

* Marks injected breakpoints as verified and registers them as permanent.
* Only called for telnet debuggers.
*/
public async injectBreakpointsForProject(project: Project) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, i liked the name "write" better than "inject".

* Clear the script-referenced files cache. Should be called when staging directory
* contents may have changed (e.g. at the start of a new debug session).
*/
public clearScriptReferencedFilesCache() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine these to a .reset() function instead of separate functions? Also, if we dispose the manager across reloads, we don't need to worry about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants