Skip to content

feat: support DebuggerDisplayAttribute display templates#231

Closed
GustavEikaas wants to merge 3 commits into
Samsung:masterfrom
GustavEikaas:feat/debugger-display
Closed

feat: support DebuggerDisplayAttribute display templates#231
GustavEikaas wants to merge 3 commits into
Samsung:masterfrom
GustavEikaas:feat/debugger-display

Conversation

@GustavEikaas

Copy link
Copy Markdown

Adds DebuggerDisplayAttribute.Value support for class and struct variable display, part of the roadmap for #226 (previously requested in #142).

Stacked PR: this branch is based on #227 (ToString override display) and #230 (attribute blob refactor); the first two commits belong to those PRs. Marked draft until they land — only the last commit is new here.

What it does

A type annotated with [DebuggerDisplay("Id = {Id}, Name = {GetName()}")] displays as Id = 101, Name = "abc" in locals, watch, child variables and evaluate responses, for both MI and DAP. Lookup uses the exact runtime type with base-type fallback, and the attribute takes precedence over an overridden ToString() (matching Visual Studio).

Supported placeholder subset (v1, by design):

  • instance fields, properties and zero-argument methods: {Field}, {Prop}, {GetText()}
  • dotted member chains {A.B}, this / this.Member, and the ,nq modifier
  • escaped braces {{ }}, any literal text

Anything else (method arguments, operators, indexers, format specifiers) conservatively falls back to the existing {TypeName} display — a bad template never breaks variable retrieval. Explicit non-goals for now: Name/Type attribute properties, assembly-level Target, DebuggerTypeProxy.

How it works

  • attributes.cpp: ECMA-335 II.23.3 SerString decoder + GetAttributeFixedStringArgument() on top of ForEachAttribute() from refactor: centralize custom attribute blob handling #230.
  • variables.cpp: template parser plus receiver-bound placeholder evaluation — members resolve via Evaluator::ResolveIdentifiers and methods via Evaluator::WalkMethods + EvalHelpers::EvalFunction against the displayed value itself, not by re-evaluating the variable's evaluateName text, so watch expressions with side effects are never re-executed and children without a usable evaluateName still get display.
  • Placeholder results print with plain PrintValue (no nested DebuggerDisplay/ToString), so display evaluation cannot recurse.
  • Under EVAL_NOFUNCEVAL, field-only templates still resolve; templates needing a getter/method call fall back to {TypeName}.

Testing

Extended MITestVariables and VSCodeTestVariables: field/property/method/mixed templates, struct, base-class attribute inheritance, custom generic type, BCL List<int>/Dictionary<string,int> (Count = {Count}), literal-only, escaped braces, nested {A.B}, ,nq, missing-member and malformed-template fallback, precedence over ToString(), child expansion, and EVAL_NOFUNCEVAL behavior.

Full test suite run locally on this branch (Linux x64, .NET 10): 65/65 passed.

🤖 Generated with Claude Code

GustavEikaas and others added 3 commits June 10, 2026 08:53
When a class or struct declares a ToString() override (true 'override',
not 'new' shadowing; base types walked, stopping at System.Object,
System.ValueType and System.Exception to keep exception display intact),
use its result instead of the default {TypeName} display.

The override is located via metadata during the base-type walk and called
directly on the ICorDebugValue the debugger already holds (receiver-bound
EvalFunction), not by re-evaluating the variable's evaluateName as text.
This avoids re-executing watch expressions with side effects and covers
children that have no usable evaluateName.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote ForEachAttribute to a public helper that exposes the raw
attribute value blob (ECMA-335 II.23.3) to its callback, and move the
inline DebuggerBrowsableAttribute blob check from evaluator.cpp into
attributes.cpp as HasDebuggerBrowsableNeverAttribute. No behavior
change; this gives custom attribute argument decoding a single home.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use DebuggerDisplayAttribute.Value as the variable display for classes
and structs: literal text with {expression} placeholders, where v1
supports instance fields, properties, zero-argument methods, dotted
member chains, 'this' and the ',nq' modifier. Escaped '{{'/'}}' braces
are kept literal. The attribute is looked up on the exact type with
base type fallback, and takes precedence over an overridden ToString().

Placeholders are evaluated against the displayed value itself
(receiver-bound ResolveIdentifiers/EvalFunction), not the current
frame, and their results are printed with plain PrintValue, so display
evaluation can not recurse. Any unsupported or failed placeholder
conservatively keeps the {TypeName} display and never breaks variable
retrieval. Under EVAL_NOFUNCEVAL field-only templates still resolve,
while getter/method templates fall back.

The metadata side adds an ECMA-335 II.23.3 SerString decoder and
GetAttributeFixedStringArgument() on top of ForEachAttribute().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@GustavEikaas GustavEikaas deleted the feat/debugger-display branch June 10, 2026 07:44
@GustavEikaas

Copy link
Copy Markdown
Author

ignore, I was just testing the infamous fable 5

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.

1 participant