diff --git a/src/MIDebugEngine/Engine.Impl/Variables.cs b/src/MIDebugEngine/Engine.Impl/Variables.cs index 031dd40c8..d5b593bfc 100644 --- a/src/MIDebugEngine/Engine.Impl/Variables.cs +++ b/src/MIDebugEngine/Engine.Impl/Variables.cs @@ -297,10 +297,17 @@ private VariableInformation(TupleValue results, VariableInformation parent, stri int index; - if (!results.Contains("value") && (Name == TypeName || (Name.Contains("::") && name == null))) + if ((!results.Contains("value") && (Name == TypeName || (Name.Contains("::") && name == null))) + || (Name == TypeName && Name.IndexOfAny(s_typeNameOnlyChars) >= 0)) { - // base classes show up with no value and exp==type + // base classes show up with no value and exp==type // (sometimes underlying debugger does not follow this convention, when using typedefs in templated types so look for "::" in the field name too) + // lldb reports base-class children WITH an aggregate value ("{...}"), so + // additionally treat exp==type as a base class whenever the name cannot + // be a field identifier (template arguments, scope operator, space). + // Known gap: a base whose name is a plain identifier, reported with a + // value, stays classified as a field — accepting it would risk + // misclassifying a member named after its type. Name = TypeName + " (base)"; Value = TypeName; VariableNodeType = NodeType.BaseClass; @@ -392,6 +399,10 @@ public enum NodeType public NodeType VariableNodeType { get; private set; } + // Characters that can appear in a type name but never in a field identifier — + // used to recognize base-class children whose expression equals their type name. + private static readonly char[] s_typeNameOnlyChars = new char[] { '<', ':', ' ' }; + private static readonly string[] s_stringTypes = new string[] { @"^char *\*$", @"^char *\[[0-9]*\]$", diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 5e7e30d05..66b275a8a 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -13,6 +13,7 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; @@ -249,10 +250,27 @@ public VisualizerInfo(VisualizerType viz, TypeName name) private static readonly Regex s_intrinsicCallPattern = new Regex(@"\b(\w+)\s*\("); // Matches the leading "0x " address that GDB/LLDB prepends when displaying a string pointer value. private static readonly Regex s_addressPrefix = new Regex(@"^0x[0-9a-fA-F]+\s+"); + // Restricts debugger type-name queries to plain C++ type names (identifiers, + // scope operators, template arguments, cv/ref/pointer decorations) so that no + // other console syntax can be smuggled into the query command. Literal spaces, + // not \s: a newline inside a name could terminate the MI console line early. + private static readonly Regex s_safeTypeNameForQuery = new Regex(@"^[A-Za-z_$][\w$]*(::[\w$]+)*( *<[\w :<>,\*&$]*>)?( *[\*&])*$", RegexOptions.Compiled); private List _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; private Dictionary _vizCache; + // Debugger type-name resolution results (typedef target / first base class), + // keyed by the reported type name. Null values cache negative results so each + // unmatched name costs at most one round of debugger queries per session. + private Dictionary _typeNameQueryCache = new Dictionary(); + // Single-token built-in type names, never worth a debugger query (natvis rules + // cannot target primitives). Multi-word forms ("unsigned long") are already + // rejected by s_safeTypeNameForQuery. + private static readonly HashSet s_primitiveTypeNames = new HashSet(StringComparer.Ordinal) + { + "void", "bool", "char", "short", "int", "long", "float", "double", + "signed", "unsigned", "wchar_t", "char8_t", "char16_t", "char32_t" + }; private uint _depth; public HostWaitDialog WaitDialog { get; private set; } @@ -1257,9 +1275,361 @@ private VisualizerInfo FindType(IVariableInformation variable) } parsedName = TypeName.Parse(var.TypeName, _process.Logger.NatvisLogger); } + + // Name-based fallback: the debugger reports a variable's type under its + // declared name. A typedef alias or a subclass of a visualized type + // therefore misses the wildcard rule of its underlying type, and the + // debugger may expose no BaseClass child for FindBaseClass to walk. + // Ask the debugger to resolve the name (typedef target or first base class) + // and retry the rule lookup with the resolved name. + if (_typeVisualizers.Count == 0) + { + return null; // no natvis loaded — a resolved name could not match anything + } + string currentName = StripTypeDecorations(variable.TypeName); + for (int chain = 0; chain < MAX_ALIAS_CHAIN && !String.IsNullOrEmpty(currentName); chain++) + { + string resolvedName = ResolveTypeNameViaDebugger(currentName); + if (String.IsNullOrEmpty(resolvedName) || resolvedName == currentName) + { + break; + } + _process.Logger.NatvisLogger?.WriteLine(LogLevel.Verbose, "FindType: '{0}' resolved to '{1}'", currentName, resolvedName); + TypeName resolvedParsed = TypeName.Parse(resolvedName, _process.Logger.NatvisLogger); + if (resolvedParsed == null) + { + break; + } + // On success Scan caches the visualizer under variable.TypeName, so the + // next variable reported under this alias hits _vizCache directly. + var visualizer = Scan(resolvedParsed, variable); + if (visualizer != null) + { + return visualizer; + } + currentName = resolvedName; // alias of an alias / base of a base + } + return null; + } + + /// + /// Strips cv-qualifiers and pointer/reference decorations from a reported type + /// name so it can be used in a debugger type query, e.g. + /// "const TextItemList &" -> "TextItemList". + /// + internal static string StripTypeDecorations(string typeName) + { + if (String.IsNullOrEmpty(typeName)) + { + return typeName; + } + string result = typeName.Trim(); + while (result.EndsWith("*", StringComparison.Ordinal) || result.EndsWith("&", StringComparison.Ordinal)) + { + result = result.Substring(0, result.Length - 1).TrimEnd(); + } + foreach (string qualifier in new[] { "const ", "volatile " }) + { + if (result.StartsWith(qualifier, StringComparison.Ordinal)) + { + result = result.Substring(qualifier.Length).TrimStart(); + } + } + return result; + } + + /// + /// Extracts the underlying type from a debugger type-description output: + /// the typedef target, the head type name (when the debugger already expanded + /// a typedef), or the first base class. Returns null when the output describes + /// only itself. + /// Handles GDB "whatis"/"ptype" ("type = ..." prefix) and LLDB "type lookup". + /// + internal static string ExtractResolvedTypeName(string output, string queriedName) + { + if (String.IsNullOrWhiteSpace(output)) + { + return null; + } + string line = null; + foreach (string candidate in output.Split('\n')) + { + string trimmed = candidate.Trim(); + if (trimmed.Length > 0) + { + line = trimmed; + break; + } + } + if (line == null) + { + return null; + } + + // Debugger error prose ("error: use of undeclared identifier ...") has a + // top-level colon, which would make "error" parse as a head name. + if (line.StartsWith("error:", StringComparison.OrdinalIgnoreCase) || + line.StartsWith("warning:", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (line.StartsWith("type = ", StringComparison.Ordinal)) + { + line = line.Substring("type = ".Length).Trim(); + } + + if (line.StartsWith("typedef ", StringComparison.Ordinal)) + { + // "typedef " (lldb) or "typedef " (C style). + // Strip the trailing alias only when it is a whole whitespace-separated + // token, so a target that merely ends with the alias name (ns::Alias) + // is kept intact. + string target = line.Substring("typedef ".Length).Trim().TrimEnd(';').Trim(); + if (target.Length > queriedName.Length && + target.EndsWith(queriedName, StringComparison.Ordinal) && + Char.IsWhiteSpace(target[target.Length - queriedName.Length - 1])) + { + target = target.Substring(0, target.Length - queriedName.Length).TrimEnd(); + } + return (target != queriedName) ? ValidatedTypeName(target) : null; + } + + foreach (string keyword in new[] { "class ", "struct " }) + { + if (line.StartsWith(keyword, StringComparison.Ordinal)) + { + line = line.Substring(keyword.Length).Trim(); + break; + } + } + + // Strip a "template<...> " prefix (lldb prints template specializations + // as "template<> class Foo : ..."). Only when '<' follows the keyword — + // a type genuinely named "template_something" must not be eaten. + if (line.StartsWith("template", StringComparison.Ordinal) && + line.Substring("template".Length).TrimStart().StartsWith("<", StringComparison.Ordinal)) + { + int d = 0; + int i = "template".Length; + for (; i < line.Length; i++) + { + if (line[i] == '<') { d++; } + else if (line[i] == '>') { d--; if (d == 0) { i++; break; } } + } + line = line.Substring(i).TrimStart(); + foreach (string keyword in new[] { "class ", "struct " }) + { + if (line.StartsWith(keyword, StringComparison.Ordinal)) + { + line = line.Substring(keyword.Length).Trim(); + break; + } + } + } + + // GDB prints template types with a substitution clause after the head name: + // "ItemStack [with T = int] : public ItemList {" + // Capture the parameter values and strip the clause; the base clause is + // printed with UNsubstituted parameters and needs them re-applied. + Dictionary templateParams = null; + int withPos = line.IndexOf("[with ", StringComparison.Ordinal); + if (withPos >= 0) + { + int closePos = line.IndexOf(']', withPos); + if (closePos > withPos) + { + string clause = line.Substring(withPos + "[with ".Length, closePos - withPos - "[with ".Length); + templateParams = new Dictionary(StringComparer.Ordinal); + int partStart = 0; + int d2 = 0; + for (int i = 0; i <= clause.Length; i++) + { + char c = i < clause.Length ? clause[i] : ','; + if (c == '<' || c == '(') { d2++; } + else if (c == '>' || c == ')') { d2--; } + else if (c == ',' && d2 == 0) + { + string part = clause.Substring(partStart, i - partStart); + int eq = part.IndexOf('='); + if (eq > 0) + { + string key = part.Substring(0, eq).Trim(); + string value = part.Substring(eq + 1).Trim(); + if (key.Length > 0 && value.Length > 0) + { + templateParams[key] = value; + } + } + partStart = i + 1; + } + } + line = (line.Substring(0, withPos) + line.Substring(closePos + 1)).Trim(); + } + } + + // line is now e.g. "TextItemList : public ItemList {" or + // "ItemList {" (typedef already expanded by the debugger). + // Find the head-name end: the base-clause ':' (template depth 0, + // skipping "::") or the opening '{'. + int colonPos = -1; + int headEnd = line.Length; + int depth = 0; + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (c == '<') { depth++; } + else if (c == '>') { depth--; } + else if (c == '{') { headEnd = i; break; } + else if (c == ':' && depth == 0) + { + if (i + 1 < line.Length && line[i + 1] == ':') + { + i++; // scope operator + continue; + } + colonPos = i; + headEnd = i; + break; + } + } + + // If the debugger already resolved the queried name to another type + // (alias expansion), the head name is the answer. Even when a base + // clause follows, that is the base of the RESOLVED type, not of the + // queried one — returning the base would skip past the type whose + // rule we are looking for. + string headName = line.Substring(0, headEnd).Trim(); + if (headName.Length > 0 && headName != queriedName) + { + return ValidatedTypeName(headName); + } + + if (colonPos >= 0) + { + // First base class: cut at '{' and at the first top-level ','. + string bases = line.Substring(colonPos + 1); + int end = bases.Length; + depth = 0; + for (int i = 0; i < bases.Length; i++) + { + char c = bases[i]; + if (c == '<') { depth++; } + else if (c == '>') { depth--; } + else if ((c == ',' || c == '{') && depth == 0) + { + end = i; + break; + } + } + string firstBase = bases.Substring(0, end).Trim(); + // Strip access/virtual keywords to fixpoint: gdb prints + // "public virtual Base", lldb prints "virtual public Base". + bool strippedKeyword = true; + while (strippedKeyword) + { + strippedKeyword = false; + foreach (string keyword in new[] { "virtual", "public", "protected", "private" }) + { + if (firstBase.StartsWith(keyword + " ", StringComparison.Ordinal)) + { + firstBase = firstBase.Substring(keyword.Length + 1).TrimStart(); + strippedKeyword = true; + } + } + } + // Re-apply gdb's "[with ...]" template-parameter values: the base + // clause is printed with the bare parameter names (ItemList). + if (templateParams != null) + { + foreach (KeyValuePair param in templateParams) + { + firstBase = Regex.Replace(firstBase, @"\b" + Regex.Escape(param.Key) + @"\b", param.Value); + } + } + return ValidatedTypeName(firstBase); + } + + // The output names only the queried type itself, with no base clause. return null; } + /// + /// Returns when it has the shape of a plain type name, + /// null otherwise. Keeps debugger error prose (e.g. "no type was found ...") + /// from being handed back as a resolution or entering the cache and the log. + /// + private static string ValidatedTypeName(string name) + { + return (!String.IsNullOrEmpty(name) && s_safeTypeNameForQuery.IsMatch(name)) ? name : null; + } + + /// + /// Asks the debugger what a reported type name is (typedef target or first base + /// class) and returns the resolved name, or null. Results, including negative + /// resolutions, are cached for the session; transient query errors are not. + /// + private string ResolveTypeNameViaDebugger(string typeName) + { + if (String.IsNullOrEmpty(typeName) || + s_primitiveTypeNames.Contains(typeName) || + !s_safeTypeNameForQuery.IsMatch(typeName)) + { + return null; + } + if (_typeNameQueryCache.TryGetValue(typeName, out string cached)) + { + return cached; + } + + string resolved = null; + try + { + switch (_process.MICommandFactory.Mode) + { + case MIMode.Gdb: + // "whatis" is one line and resolves one typedef level. For a + // class it echoes the name back, so fall through to "ptype", + // whose first line carries the base-class clause. + resolved = ExtractResolvedTypeName(ConsoleCommandSync("whatis " + typeName), typeName); + if (resolved == null) + { + resolved = ExtractResolvedTypeName(ConsoleCommandSync("ptype " + typeName), typeName); + } + break; + case MIMode.Lldb: + resolved = ExtractResolvedTypeName(ConsoleCommandSync("type lookup " + typeName), typeName); + break; + default: + break; + } + } + catch (Exception e) + { + // Do not cache: the failure may be transient (e.g. the process was not + // stopped) rather than a genuine "no resolution" for this name. + // Task.Wait() wraps the real failure in an AggregateException — unwrap + // it so the log carries the actual message. + string message = (e as AggregateException)?.Flatten().InnerException?.Message ?? e.Message; + _process.Logger.NatvisLogger?.WriteLine(LogLevel.Verbose, "FindType: type name query for '{0}' failed: {1}", typeName, message); + return null; + } + + _typeNameQueryCache[typeName] = resolved; + return resolved; + } + + private string ConsoleCommandSync(string cmd) + { + string output = null; + Task task = Task.Run(async () => + { + output = await _process.ConsoleCmdAsync(cmd, allowWhileRunning: false, ignoreFailures: true); + }); + task.Wait(); + return output; + } + private string FormatValue(string format, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { if (String.IsNullOrWhiteSpace(format)) diff --git a/src/MIDebugEngineUnitTests/NatvisTypeNameResolutionTest.cs b/src/MIDebugEngineUnitTests/NatvisTypeNameResolutionTest.cs new file mode 100644 index 000000000..6067ab492 --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisTypeNameResolutionTest.cs @@ -0,0 +1,244 @@ +using Xunit; +using Microsoft.MIDebugEngine.Natvis; + +namespace MIDebugEngineUnitTests +{ + /// + /// Unit tests for and + /// (typedef / base-class name + /// resolution used by the FindType fallback). + /// + public class NatvisTypeNameResolutionTest + { + // -- StripTypeDecorations ---------------------------------------------- + + [Fact] + public void StripTypeDecorations_ConstRef_Stripped() + { + Assert.Equal("TextItemList", Natvis.StripTypeDecorations("const TextItemList &")); + } + + [Fact] + public void StripTypeDecorations_Pointer_Stripped() + { + Assert.Equal("TextItemList", Natvis.StripTypeDecorations("TextItemList *")); + } + + [Fact] + public void StripTypeDecorations_PlainName_Unchanged() + { + Assert.Equal("RawItemList", Natvis.StripTypeDecorations("RawItemList")); + } + + [Fact] + public void StripTypeDecorations_TemplateName_Unchanged() + { + Assert.Equal("ItemList", Natvis.StripTypeDecorations("ItemList")); + } + + // -- ExtractResolvedTypeName: GDB whatis -------------------------------- + + [Fact] + public void Extract_GdbWhatisTypedef_ReturnsTarget() + { + Assert.Equal("ItemList", + Natvis.ExtractResolvedTypeName("type = ItemList", "ItemValueList")); + } + + [Fact] + public void Extract_GdbWhatisEchoesName_ReturnsNull() + { + // whatis of a class echoes the class name back — no new information. + Assert.Null(Natvis.ExtractResolvedTypeName("type = TextItemList", "TextItemList")); + } + + // -- ExtractResolvedTypeName: GDB ptype --------------------------------- + + [Fact] + public void Extract_GdbPtypeSubclass_ReturnsFirstBase() + { + string output = "type = class TextItemList : public ItemList {\n public:\n ...\n}"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "TextItemList")); + } + + [Fact] + public void Extract_GdbPtypeTypedefExpanded_ReturnsHeadName() + { + // ptype of a typedef expands to the underlying class definition. + string output = "type = class ItemList {\n public:\n ...\n}"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "ItemValueList")); + } + + [Fact] + public void Extract_GdbPtypeNoBase_ReturnsNull() + { + string output = "type = class MyPlainType {\n int x;\n}"; + Assert.Null(Natvis.ExtractResolvedTypeName(output, "MyPlainType")); + } + + // -- ExtractResolvedTypeName: LLDB type lookup -------------------------- + + [Fact] + public void Extract_LldbTypedef_ReturnsTarget() + { + Assert.Equal("ItemList", + Natvis.ExtractResolvedTypeName("typedef ItemList", "ItemValueList")); + } + + [Fact] + public void Extract_LldbClassWithBase_ReturnsFirstBase() + { + string output = "class TextItemList : public ItemList {\n ...\n}"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "TextItemList")); + } + + [Fact] + public void Extract_LldbAliasExpandedWithItsOwnBase_ReturnsHeadNotBase() + { + // lldb resolves an alias by printing the underlying class definition — + // including THAT class's own base and a "template<>" prefix. The head + // name is the resolution; the base clause belongs to the resolved type. + string output = "template<> class ItemList : public ListSpecial {\n typedef Something DataPointer;\n}"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "TextItemList")); + } + + [Fact] + public void Extract_CStyleTypedefWithAliasSuffix_ReturnsTarget() + { + // "typedef " — the trailing alias must be stripped. + Assert.Equal("ItemList", + Natvis.ExtractResolvedTypeName("typedef ItemList ItemValueList", "ItemValueList")); + } + + // -- ExtractResolvedTypeName: base-clause parsing edge cases ------------ + + [Fact] + public void Extract_MultipleBases_ReturnsFirstOnly() + { + string output = "class Derived : public BaseA, protected BaseB {"; + Assert.Equal("BaseA", Natvis.ExtractResolvedTypeName(output, "Derived")); + } + + [Fact] + public void Extract_VirtualPublicBase_KeywordsStripped() + { + string output = "class Derived : virtual public Base {"; + Assert.Equal("Base", Natvis.ExtractResolvedTypeName(output, "Derived")); + } + + [Fact] + public void Extract_ScopedBaseName_ScopeOperatorNotMistakenForBaseClause() + { + string output = "class Derived : public ns::Base {"; + Assert.Equal("ns::Base", Natvis.ExtractResolvedTypeName(output, "Derived")); + } + + [Fact] + public void Extract_TemplateArgsContainingScopes_Parsed() + { + string output = "class Wrapper : public std::vector {"; + Assert.Equal("std::vector", Natvis.ExtractResolvedTypeName(output, "Wrapper")); + } + + [Fact] + public void Extract_GdbTemplateSubclassWithClause_ParamsSubstitutedInBase() + { + // GDB prints template types with a "[with ...]" substitution clause and + // leaves the base clause UNsubstituted (verified against gdb 16.3). + string output = "type = class ItemStack [with T = int] : public ItemList {\n ...\n}"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "ItemStack")); + } + + [Fact] + public void Extract_GdbWithClauseMultipleParams_AllSubstituted() + { + string output = "type = class PairLike [with K = int, V = long] : public MapBase {"; + Assert.Equal("MapBase", Natvis.ExtractResolvedTypeName(output, "PairLike")); + } + + [Fact] + public void Extract_GdbVirtualBaseKeywordOrder_Stripped() + { + // gdb emits "public virtual", lldb emits "virtual public" — both must strip. + string output = "type = class Derived : public virtual Base {"; + Assert.Equal("Base", Natvis.ExtractResolvedTypeName(output, "Derived")); + } + + [Fact] + public void Extract_GdbWithClauseNonTypeParam_Substituted() + { + // Non-type template parameters appear in the with-clause as values too. + string output = "type = class FixedArr [with T = int, N = 4] : public ArrBase {"; + Assert.Equal("ArrBase", Natvis.ExtractResolvedTypeName(output, "FixedArr")); + } + + [Fact] + public void Extract_GdbVirtualBaseCombinedWithClause_Substituted() + { + // Keyword stripping and with-clause substitution must compose. + string output = "type = class ItemStack [with T = int] : public virtual ItemList {"; + Assert.Equal("ItemList", Natvis.ExtractResolvedTypeName(output, "ItemStack")); + } + + [Fact] + public void Extract_StructKeyword_Handled() + { + string output = "type = struct DerivedS : public BaseS {"; + Assert.Equal("BaseS", Natvis.ExtractResolvedTypeName(output, "DerivedS")); + } + + [Fact] + public void Extract_TypeNamedTemplateHelper_NotTreatedAsTemplatePrefix() + { + // A type genuinely named "template_helper" must not have its name eaten + // by the template<...> prefix stripping. + string output = "class template_helper : public Base {"; + Assert.Equal("Base", Natvis.ExtractResolvedTypeName(output, "template_helper")); + } + + [Fact] + public void Extract_ScopedTypedefTargetEndingWithAlias_TargetKept() + { + // The alias suffix is only stripped as a whole whitespace-separated token: + // a target that merely ends with the alias name must stay intact. + Assert.Equal("ns::ItemValueList", + Natvis.ExtractResolvedTypeName("typedef ns::ItemValueList ItemValueList", "ItemValueList")); + } + + // -- ExtractResolvedTypeName: degenerate inputs ------------------------- + + [Fact] + public void Extract_EmptyOrWhitespace_ReturnsNull() + { + Assert.Null(Natvis.ExtractResolvedTypeName(null, "X")); + Assert.Null(Natvis.ExtractResolvedTypeName("", "X")); + Assert.Null(Natvis.ExtractResolvedTypeName(" \n ", "X")); + } + + [Fact] + public void Extract_ErrorText_ReturnsNullOrHarmless() + { + // A "no type found" style message must not be mistaken for a type name + // that could loop; it differs from the queried name, so the caller's + // TypeName.Parse / Scan will simply find no rule for it. + Assert.Null(Natvis.ExtractResolvedTypeName("type = MyPlainType", "MyPlainType")); + } + + [Fact] + public void Extract_ErrorPrefixedOutput_ReturnsNull() + { + // "error: ..." has a top-level colon; without the guard, "error" would + // parse as a head name and waste a follow-up query. + Assert.Null(Natvis.ExtractResolvedTypeName("error: use of undeclared identifier 'x'", "Foo")); + Assert.Null(Natvis.ExtractResolvedTypeName("warning: something", "Foo")); + } + + [Fact] + public void Extract_LldbNoTypeFoundMessage_ReturnsNull() + { + // Debugger error prose must not come back as a "resolved type name". + string output = "no type was found in the current language c++14 matching 'Foo'; performing a global search across all languages"; + Assert.Null(Natvis.ExtractResolvedTypeName(output, "Foo")); + } + } +} diff --git a/test/CppTests/Tests/NatvisTests.cs b/test/CppTests/Tests/NatvisTests.cs index 46f31299c..5a30d8b50 100644 --- a/test/CppTests/Tests/NatvisTests.cs +++ b/test/CppTests/Tests/NatvisTests.cs @@ -39,7 +39,7 @@ public NatvisTests(ITestOutputHelper outputHelper) : base(outputHelper) // These line numbers will need to change if src/natvis/main.cpp changes private const int SimpleClassAssignmentLine = 66; - private const int ReturnSourceLine = 90; + private const int ReturnSourceLine = 93; [Theory] [RequiresTestSettings] @@ -674,6 +674,48 @@ public void TestHideRawView(ITestSettings settings) } } + [Theory] + [DependsOnTest(nameof(CompileNatvisDebuggee))] + [RequiresTestSettings] + public void TestTypedefAndBaseClassResolution(ITestSettings settings) + { + this.TestPurpose("This test checks that a typedef alias and a subclass of a visualized type resolve to the underlying type's visualizer."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, NatvisName, DebuggeeMonikers.Natvis.Default); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Configure launch"); + string visFile = Path.Join(debuggee.SourceRoot, "visualizer_files", "Simple.natvis"); + + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath, visFile, false); + runner.RunCommand(launch); + + this.Comment("Set Breakpoint"); + SourceBreakpoints writerBreakpoints = debuggee.Breakpoints(NatvisSourceName, ReturnSourceLine); + runner.SetBreakpoints(writerBreakpoints); + + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + using (IThreadInspector threadInspector = runner.GetThreadInspector()) + { + IFrameInspector currentFrame = threadInspector.Stack.First(); + + this.Comment("Verifying a variable declared via a typedef alias uses the underlying type's visualizer"); + var aliased = currentFrame.GetVariable("aliasedContainer"); + Assert.Equal("{ size=4 }", aliased.Value); + + this.Comment("Verifying a variable of a subclass type uses the base class's visualizer"); + var derived = currentFrame.GetVariable("derivedContainer"); + Assert.Equal("{ size=6 }", derived.Value); + } + + runner.Expects.ExitedEvent(exitCode: 0).TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + } + [Theory] [DependsOnTest(nameof(CompileNatvisDebuggee))] [RequiresTestSettings] diff --git a/test/CppTests/debuggees/natvis/src/SimpleTemplated.h b/test/CppTests/debuggees/natvis/src/SimpleTemplated.h index 68b072e1f..040902860 100644 --- a/test/CppTests/debuggees/natvis/src/SimpleTemplated.h +++ b/test/CppTests/debuggees/natvis/src/SimpleTemplated.h @@ -33,3 +33,15 @@ class SimpleMap Value _value; int _size; }; + +// A typedef alias and a subclass of SimpleContainer: the debugger reports +// variables of these types under the alias/subclass name, which matches no +// visualizer directly — resolving them to SimpleContainer<*> exercises the +// typedef and base-class type-name resolution. +typedef SimpleContainer ContainerAlias; + +class DerivedContainer : public SimpleContainer +{ +public: + DerivedContainer(int val, int count) : SimpleContainer(val, count) {} +}; diff --git a/test/CppTests/debuggees/natvis/src/main.cpp b/test/CppTests/debuggees/natvis/src/main.cpp index 1216a3e13..c428460b4 100644 --- a/test/CppTests/debuggees/natvis/src/main.cpp +++ b/test/CppTests/debuggees/natvis/src/main.cpp @@ -87,5 +87,8 @@ int main(int argc, char** argv) inactiveList.Add(100); inactiveList.Add(200); + ContainerAlias aliasedContainer(9, 4); + DerivedContainer derivedContainer(7, 6); + return 0; }