Fix C/C++ debug hover on intermediate members of a dereferenced expression#14540
Fix C/C++ debug hover on intermediate members of a dereferenced expression#14540tieo wants to merge 2 commits into
Conversation
…enced members VS Code's default debug data-tip keeps a leading `*`/`&` and clips on the right, so hovering an intermediate member of e.g. `*a.b.c` evaluates `*a.b` (a dereference of the struct `a.b`) and shows no value. Register an EvaluatableExpressionProvider that, only for that case, returns the expression without the leading operator. Every other expression returns undefined so the default behavior is unchanged.
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Registers a C/C++ EvaluatableExpressionProvider so debug hover evaluates the correct sub-expression when the hovered identifier is an intermediate member inside a */&-prefixed access chain (e.g. avoiding *a.b when hovering b in *a.b.c).
Changes:
- Added
EvaluatableExpressionProviderimplementation that conditionally strips leading*/&for intermediate member hovers. - Registered the provider during language server client initialization for the C/C++ document selector.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts | Adds a custom evaluatable-expression computation to override VS Code’s default behavior in a narrow case. |
| Extension/src/LanguageServer/client.ts | Registers the new evaluatable expression provider for C/C++ documents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Addressed the review feedback in the latest commit:
On the Copilot suggestion to gate with What it does now is keep the leading |
| const defaultExpression = (): vscode.EvaluatableExpression => | ||
| new vscode.EvaluatableExpression(new vscode.Range(position.line, tokenStart, position.line, clipEnd), line.substring(tokenStart, clipEnd)); | ||
|
|
||
| if (leading === null) { | ||
| return defaultExpression(); |
There was a problem hiding this comment.
Seems like this isn't 100% resolved yet:
But the actual concern the comment raised — "can diverge from VS Code's built-in detection" — is still live. There's no parity test, and the divergence is real: tokens can begin with a connector, so foo().bar / (*this).member / obj->fn()->field return broken fragments (.bar, .member, ->field). So the intent is addressed and documented, but the divergence it warned about is not eliminated.
This is the same as the two new issues raised by the GitHub Copilot.
Register the EvaluatableExpressionProvider during debugger activation instead of through the language client, so it also works when IntelliSense is disabled, and move it next to the other debugger code. The expression computation lives in a vscode-free module (evaluatableExpression.ts) so it can be unit tested directly. Registering a provider replaces VS Code's built-in data-tip expression detection, so it reproduces that for ordinary tokens and additionally handles access chains the built-in detection gets wrong for C/C++: - A leading */& binds to the whole access chain (the postfix ., ->, [] operators bind tighter), so it is dropped for an interior member of a dot chain, where *a.b would dereference the struct a.b, and kept on the final member and before ->. - Array subscripts and :: are kept in the token, so members after a subscript (a.b[i].c) and scoped names (ns::var) resolve instead of producing a broken fragment.
5433ec8 to
b080bf6
Compare
|
Thanks — addressed the feedback and rewrote the description, since the old one no longer matched the code. On the "returns On On On tests: moved the computation into a |
| // In each input the `|` marks the cursor; it is removed before evaluating. | ||
| function evaluate(marked: string): string | undefined { | ||
| const character: number = marked.indexOf('|'); | ||
| const line: string = marked.replace('|', ''); |
There was a problem hiding this comment.
CodeQL js/incomplete-sanitization (test helper)
It's a benign warning on test code, but trivially silenced and made more robust by slicing at the known marker index instead of String.replace, which also documents the single-marker assumption:
function evaluate(marked: string): string | undefined {
const character: number = marked.indexOf('|');
const line: string = marked.slice(0, character) + marked.slice(character + 1);
return computeEvaluatableExpression(line, character)?.expression;
}| const tokenStart: number = token.index; | ||
| const tokenEnd: number = token.index + token[0].length; | ||
| const leading: RegExpMatchArray | null = token[0].match(/^[*&]+/); | ||
| const exprStart: number = tokenStart + (leading !== null ? leading[0].length : 0); |
| it('keeps :: scoped names together', () => { | ||
| strictEqual(evaluate('ns::|var'), 'ns::var'); | ||
| strictEqual(evaluate('ns::var::|z'), 'ns::var::z'); | ||
| }); | ||
| }); |
sean-mcmanus
left a comment
There was a problem hiding this comment.
My local Copilot says the following (the 1st issue is the same as the 2 comments made by the GitHub Copilot).
Significant: tokens can begin with a connector, producing broken expressions
The token grammar lets a match start with ., ->, or ::, because the repeated group's alternatives are tried independently:
/(?:[*&]+)?(?:[\p{L}\p{N}_]+|->|::|\.|\[[^\][]*\])+/gu
For any expression where a primary isn't a bare identifier — parenthesized or call results — the regex skips the un-matchable (/) and starts a fresh token at the connector. Tracing foo().bar:
foomatches, stops at((and)are unmatchable, scanned past- match resumes at
.→ token.bar
Hovering bar returns .bar, which gets sent to the debugger and errors. Same for:
(*this).member→ hoveringmember→.memberobj->method()->field→ hoveringfield→->field
These are common patterns, and built-in detection likely produced bar/member/field for them, so this is a regression risk, not just an edge case. Fix: anchor the chain so it must start with an identifier (or leading op + identifier), e.g.
/(?:[*&]+)?[\p{L}\p{N}_]+(?:[\p{L}\p{N}_]+|->|::|\.|\[[^\][]*\])*/guand add tests for foo().bar, (*this).member, and obj->fn()->field so a leading-connector expression can never be returned. (Optional, while you're there: tightening the identifier start to [\p{L}_][\p{L}\p{N}_]* would also stop 1.5/0x1F numeric literals from being tokenized as member chains — a smaller parity divergence.)
Confirm intent: *ptr->member hovering ptr → *ptr
The new test locks in evaluate('*|ptr->member') === '*ptr'. Worth flagging because in an earlier note you argued the opposite — "when you hover ptr you want to see ptr, not *ptr." The current behavior is actually defensible: ptr-> proves ptr is a pointer, so *ptr is a valid dereference (it won't error the way *a.b does), and it matches built-in's keep-leading-and-clip-right behavior. But it does show the pointee rather than the pointer the user pointed at. The same family of cases is also now locked in by the .-only drop:
*a.b[i]hoveringb→*a.b(valid — array decays — but shows the first element)&a[i]hoveringa→&a(address of the array)
None of these error, so they don't reintroduce the "shows nothing" bug, but they're semantically surprising. If *ptr/*a.b/&a are the intended answers, fine — just make it a deliberate, documented choice rather than a side effect of the charAt(clipEnd) !== '.' gate. A one-line comment on why . is special (it's the only connector whose left operand isn't provably a pointer, so it's the only one where keeping * would error) would make this self-explanatory.
Summary
- Must fix: anchor the token so it can't start with
./->/::(avoids returning.bar/->fieldforfoo().bar,(*this).member, etc.); add tests for those. - Confirm + comment: the
*ptr/*a.b/&akeep-leading outcomes are intentional (they don't error, but they're surprising) — add a one-line rationale for the.-only drop.
Problem
While debugging C/C++, hovering an intermediate member of a
*/&-prefixed access chain (e.g.aorbinif (*a.b.c == X)wherea.bis a struct) shows no value, and hovering a member that comes after an array subscript (e.g.cina.b[i].c) shows no value either.Cause
No
EvaluatableExpressionProvideris registered for C/C++, so VS Code's built-in data-tip expression detection is used. It keeps the leading*/&and clips on the right, so hoveringbin*a.b.cevaluates*a.b— a dereference of the non-pointer membera.b— which errors. It also stops at[and], so hoveringcina.b[i].cevaluates a broken fragment after the].Fix
Register an
EvaluatableExpressionProviderfor C/C++. Once a provider is registered, VS Code uses it instead of its built-in detection (it does not fall back to the default when the provider returns nothing), so the provider reproduces the built-in behavior for ordinary tokens and additionally fixes the access-chain cases:*/&applies to the whole access chain (the postfix.,->,[]operators bind tighter). It is dropped for an interior member of a.chain, where*a.bwould dereference the structa.b, and kept on the final member and before->(so*ptr->memberis unchanged).::are kept in the token, so members after a subscript (a.b[i].c) and scoped names (ns::var) resolve. Hovering a subscript bracket evaluates the indexed element; hovering inside[...]evaluates the index on its own.The provider is registered during debugger activation (so it also works when cpptools IntelliSense is disabled), and the expression computation lives in a
vscode-free module (evaluatableExpression.ts) with unit tests.Note: I created the code in this PR with the help of an LLM, and tested it manually in VS Code plus with the added unit tests.