diff --git a/src/MIDebugEngine/Engine.Impl/Variables.cs b/src/MIDebugEngine/Engine.Impl/Variables.cs index b6dd867bb..9524d677c 100644 --- a/src/MIDebugEngine/Engine.Impl/Variables.cs +++ b/src/MIDebugEngine/Engine.Impl/Variables.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.Debugger.Interop; using Microsoft.VisualStudio.Debugger.Interop.DAP; using System; +using Microsoft.DebugEngineHost; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -92,6 +93,18 @@ internal sealed class VariableInformation : IVariableInformation static readonly Lazy s_addressPattern = new Lazy(() => new Regex(@"^(0x[0-9a-fA-F]+)\b")); + static readonly Regex s_naPattern = new Regex(@"^0x[0-9a-fA-F]+\s+(.)"); + + internal static string StripLeadingAddress(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return s_naPattern.Replace(value, "$1"); + } + public string Address() { // ask GDB to evaluate "&expression" @@ -238,7 +251,7 @@ internal VariableInformation(string displayName, string expr, ThreadContext ctx, : this(ctx, engine, thread) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = displayName; IsParameter = isParameter; _parent = null; @@ -250,7 +263,7 @@ internal VariableInformation(string expr, IVariableInformation parent, AD7Engine : this(parent.ThreadContext, engine, parent.Client) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = displayName ?? expr; _parent = parent; VariableNodeType = NodeType.Synthetic; @@ -261,7 +274,7 @@ internal VariableInformation(string expr, VariableInformation parent) : this(parent._ctx, parent._engine, parent.Client) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = expr; VariableNodeType = NodeType.Root; } @@ -272,6 +285,12 @@ private VariableInformation(TupleValue results, VariableInformation parent, stri { TypeName = results.TryFindString("type"); Value = results.TryFindString("value"); + _formatHasNa = parent._formatHasNa; + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } + Name = name ?? results.FindString("exp"); if (results.Contains("dynamic")) { @@ -375,6 +394,9 @@ public VariableInformation FindChildByName(string name) private DeferedFormatExpression _deferedFormatExpression; private IVariableInformation _parent; private string _format; + // Indicates the original format specifier included the natvis "na" modifier + // (used to decide whether to strip MI's leading address prefix from string values) + private bool _formatHasNa = false; private string _strippedName; // "Name" stripped of format specifiers private string _fullname; @@ -401,8 +423,9 @@ public enum NodeType private static Regex s_isFunction = new Regex(@".+\(.*\).*"); - private string ProcessFormatSpecifiers(string exp, out string formatSpecifier) + private string ProcessFormatSpecifiers(string exp, out string formatSpecifier, out bool formatNa) { + formatNa = false; formatSpecifier = null; // will be used with -var-set-format if (EngineUtils.IsConsoleExecCmd(exp, out string _, out string _)) @@ -416,6 +439,9 @@ private string ProcessFormatSpecifiers(string exp, out string formatSpecifier) // Find the format specifier expression string expFS = exp.Substring(lastComma + 1).Trim(); + // Detect whether the natvis 'na' modifier is present in the original format specifier. + // We must detect this before we strip modifiers below. + formatNa = expFS.IndexOf("na", StringComparison.Ordinal) >= 0; // Strip off modifiers that may be included together with another format specifier, e.g. 'nvoXb' is a valid format specifier, but we only care about the 'Xb' part // This is not quite the right fix -- really the below switch statement should be a series of if statements. But since none of the supported format specifiers @@ -698,6 +724,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw _attribsFetched = true; } Value = results.TryFindString("value"); + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } if ((string.IsNullOrEmpty(Value) || _format != null) && !string.IsNullOrEmpty(_internalName)) { if (_format != null) @@ -711,6 +741,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw if (results.ResultClass == ResultClass.done) { Value = results.FindString("value"); + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } } else if (results.ResultClass == ResultClass.error) { diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 5e7e30d05..86f33bb14 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -1502,13 +1502,31 @@ internal static string ExtractFormatSpecifier(string expression) .Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", ""); } + /// + /// Returns the format specifier from a NatVis expression (the part after the last + /// top-level comma), normalized the same way as + /// : modifiers "nvo", "na", + /// "nr", "nd" are stripped before returning. Returns null when no specifier is present. + /// also returns whether modifier 'na' was there via the out parameter + /// + internal static string ExtractFormatSpecifier(string expression, out bool hasNa) + { + hasNa = false; + int commaPos = FindLastTopLevelComma(expression); + if (commaPos < 0) return null; + + string tail = expression.Substring(commaPos + 1).Trim(); + hasNa = tail.IndexOf("na", StringComparison.Ordinal) >= 0; + + return tail + .Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", ""); + } + /// /// Cleans up the raw value that GDB/LLDB returns for a const char16_t* /// expression (i.e. one evaluated with the ,sub / ,su format specifier). /// GDB and LLDB both prefix the string with the pointer address, e.g. /// 0x00007fff5fbff6c0 u"Hello" - /// This method strips the address and the surrounding u"…" quotes so that - /// the NatVis DisplayString shows just the string content. /// internal static string CleanUtf16StringValue(string value) { @@ -1531,9 +1549,6 @@ internal static string CleanUtf16StringValue(string value) /// (i.e. one evaluated with the ,sb format specifier). /// GDB and LLDB prefix the string with the pointer address, e.g. /// 0x00007fff5fbff6c0 "Hello" - /// This method strips the address and the surrounding "…" quotes so that - /// the NatVis DisplayString shows just the string content (matching VS behaviour, - /// where {ptr,sb} evaluates to bare text without quotes). /// internal static string CleanAsciiStringValue(string value) { diff --git a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs index 287be0072..02e9a94b0 100644 --- a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs +++ b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs @@ -15,33 +15,34 @@ public class NatvisFormatSpecifierTest [Fact] public void ExtractFormatSpecifier_Sub_Extracted() { - Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub")); + Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub", out _)); } [Fact] public void ExtractFormatSpecifier_Decimal_Extracted() { - Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d")); + Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d", out _)); } [Fact] public void ExtractFormatSpecifier_NoSpecifier_ReturnsNull() { - Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1")); + Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1", out _)); } [Fact] public void ExtractFormatSpecifier_NvoModifierStripped() { // "nvoXb": strip "nvo" modifier, result is "Xb" - Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb")); + Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb", out _)); } [Fact] public void ExtractFormatSpecifier_NaModifierStripped() { // "view(RecZone)na": strip "na", result is "view(RecZone)" - Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na")); + Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na", out bool hasNa)); + Assert.True(hasNa); } // -- CleanUtf16StringValue -------------------------------------------- diff --git a/src/MIDebugEngineUnitTests/NatvisFormatStringTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatStringTest.cs new file mode 100644 index 000000000..e5caffd9e --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisFormatStringTest.cs @@ -0,0 +1,85 @@ +using Xunit; +using Microsoft.MIDebugEngine.Natvis; +using Microsoft.MIDebugEngine; + +namespace MIDebugEngineUnitTests +{ + /// + /// unit tests for natvis format string processing with format specifiers + /// tests the handling of format specifiers like, 'na', sub (UTF-16), sb (ASCII). + /// + public class NatvisFormatStringTest + { + // -- format specifier with template parameters ($T1, $T2,) -- + [Fact] + public void ExtractFormatSpecifier_HasTemplateParameter_TemplateSubstituted() + { + string spec = Natvis.ExtractFormatSpecifier("myVector,sub"); + Assert.Equal("sub", spec); + } + + // -- Format specifier na -- + + [Fact] + public void ExtractFormatSpecifier_WithNaModifier_HasNaIsTrue() + { + string spec = Natvis.ExtractFormatSpecifier("someExpr,na", out bool hasNa); + Assert.Equal("", spec); + Assert.True(hasNa); + } + + [Fact] + public void ExtractFormatSpecifier_WithNaAndFormat_HasNaIsTrueAndSpecReturned() + { + string spec = Natvis.ExtractFormatSpecifier("myString,subna", out bool hasNa); + Assert.Equal("sub", spec); + Assert.True(hasNa); + } + + [Fact] + public void ExtractFormatSpecifier_WithoutNaModifier_HasNaIsFalse() + { + string spec = Natvis.ExtractFormatSpecifier("myString,sub", out bool hasNa); + Assert.Equal("sub", spec); + Assert.False(hasNa); + } + + // -- UTF-16 and ASCII string cleanup -- + + [Fact] + public void CleanUtf16StringValue_WithAddressAndQuotes_AddressAndQuotesStripped() + { + string result = Natvis.CleanUtf16StringValue("0x00007fff5fbff6c0 u\"Hello\""); + Assert.Equal("Hello", result); + } + + [Fact] + public void CleanUtf16StringValue_WithCapitalU_QuotesStripped() + { + string result = Natvis.CleanUtf16StringValue("U\"Hello\""); + Assert.Equal("Hello", result); + } + + [Fact] + public void CleanUtf16StringValue_JustAddress_AddressStripped() + { + string result = Natvis.CleanUtf16StringValue("0x00007fff u"); + Assert.Equal("u", result); + } + + [Fact] + public void CleanAsciiStringValue_WithAddressAndQuotes_AddressAndQuotesStripped() + { + string result = Natvis.CleanAsciiStringValue("0x00007fff5fbff6c0 \"Hello\""); + Assert.Equal("Hello", result); + } + + [Fact] + public void CleanAsciiStringValue_TruncatedNoClosingQuote_OpeningQuoteStripped() + { + string result = Natvis.CleanAsciiStringValue("0x00007fff \"Hello..."); + Assert.Equal("Hello...", result); + } + } +} +