Skip to content

Commit

Permalink
Improve and unify debug views of dictionaries. (#92534)
Browse files Browse the repository at this point in the history
* Improves and unifies debug views of dictionaries.

The change alows generic and non-generic dictionaries to be displayed in the same way: with separate columns for keys and values with an ability to expand each column.

Fixes #88736

* Fix the DebuggerView tests of dictionaries

Included non-generic dictionaries in tests

* Fix more DebuggerView tests

* Included more types that implement IDictionary in the DebuggerView tests.
* Improved the testing code to support classes with attributes declared by their base classes.
* Fixed .Net Framework 4.8 build error by removing a dependency on the record c# feature.
* Fixed tests remaining tests (outside of the System.Collections subset)

* Fix DebugView.Tests build errors on .Net Framework

* Update DebugView tests to expect different outcomes on .Net Framework

.Net Framwork does not support recent improvements in the way the debugger displays a dictionary. Depending on the framwork used, the debugger view of generic dictionaries and ListDictionaryInternal are different.

* Applied suggested changes and fixes

* mostly code style changes
* restored a rest for an empty HashSet.
* fixed testing of the generic SortedList.

* Minor improvents

Renamed an internal method to match its new behavior and removed unnecessary init accessors.
  • Loading branch information
arturek committed Nov 2, 2023
1 parent 655b177 commit 34bf55c
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 108 deletions.
167 changes: 147 additions & 20 deletions src/libraries/Common/tests/System/Collections/DebugView.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
Expand All @@ -11,14 +12,96 @@ namespace System.Collections.Tests
{
public class DebugView_Tests
{
public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries()
{
yield return new object[] { new Dictionary<int, string>(), new KeyValuePair<string, string>[0] };
yield return new object[] { new ReadOnlyDictionary<int, string>(new Dictionary<int, string>()), new KeyValuePair<string, string>[0] };
yield return new object[] { new SortedDictionary<string, int>(), new KeyValuePair<string, string>[0] };
yield return new object[] { new SortedList<int, string>(), new KeyValuePair<string, string>[0] };

yield return new object[] { new Dictionary<int, string>{{1, "One"}, {2, "Two"}},
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new ReadOnlyDictionary<int,string>(new Dictionary<int, string>{{1, "One"}, {2, "Two"}}),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new SortedDictionary<string, int>{{"One", 1}, {"Two", 2}} ,
new KeyValuePair<string, string>[]
{
new ("[\"One\"]", "1"),
new ("[\"Two\"]", "2"),
}
};
yield return new object[] { new SortedList<string, double> { { "One", 1.0 }, { "Two", 2.0 } },
new KeyValuePair<string, string>[]
{
new ("[\"One\"]", "1"),
new ("[\"Two\"]", "2"),
}
};
}

private static IEnumerable<object[]> TestDebuggerAttributes_NonGenericDictionaries()
{
yield return new object[] { new Hashtable(), new KeyValuePair<string, string>[0] };
yield return new object[] { Hashtable.Synchronized(new Hashtable()), new KeyValuePair<string, string>[0] };
yield return new object[] { new SortedList(), new KeyValuePair<string, string>[0] };
yield return new object[] { SortedList.Synchronized(new SortedList()), new KeyValuePair<string, string>[0] };

yield return new object[] { new Hashtable { { "a", 1 }, { "b", "B" } },
new KeyValuePair<string, string>[]
{
new ("[\"a\"]", "1"),
new ("[\"b\"]", "\"B\""),
}
};
yield return new object[] { Hashtable.Synchronized(new Hashtable { { "a", 1 }, { "b", "B" } }),
new KeyValuePair<string, string>[]
{
new ("[\"a\"]", "1"),
new ("[\"b\"]", "\"B\""),
}
};
yield return new object[] { new SortedList { { "a", 1 }, { "b", "B" } },
new KeyValuePair<string, string>[]
{
new ("[\"a\"]", "1"),
new ("[\"b\"]", "\"B\""),
}
};
yield return new object[] { SortedList.Synchronized(new SortedList { { "a", 1 }, { "b", "B" } }),
new KeyValuePair<string, string>[]
{
new ("[\"a\"]", "1"),
new ("[\"b\"]", "\"B\""),
}
};
#if !NETFRAMEWORK // ListDictionaryInternal in .Net Framework is not annotated with debugger attributes.
yield return new object[] { new Exception().Data, new KeyValuePair<string, string>[0] };
yield return new object[] { new Exception { Data = { { "a", 1 }, { "b", "B" } } }.Data,
new KeyValuePair<string, string>[]
{
new ("[\"a\"]", "1"),
new ("[\"b\"]", "\"B\""),
}
};
#endif
}

private static IEnumerable<object[]> TestDebuggerAttributes_ListInputs()
{
yield return new object[] { new Dictionary<int, string>() };
yield return new object[] { new HashSet<string>() };
yield return new object[] { new LinkedList<object>() };
yield return new object[] { new List<int>() };
yield return new object[] { new Queue<double>() };
yield return new object[] { new SortedDictionary<string, int>() };
yield return new object[] { new SortedList<int, string>() };
yield return new object[] { new SortedSet<int>() };
yield return new object[] { new Stack<object>() };
Expand All @@ -30,39 +113,83 @@ public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
yield return new object[] { new SortedList<string, int>().Keys };
yield return new object[] { new SortedList<float, long>().Values };

yield return new object[] { new Dictionary<int, string>{{1, "One"}, {2, "Two"}} };
yield return new object[] { new HashSet<string>{"One", "Two"} };
yield return new object[] { new HashSet<string> { "One", "Two" } };

LinkedList<object> linkedList = new LinkedList<object>();
LinkedList<object> linkedList = new();
linkedList.AddFirst(1);
linkedList.AddLast(2);
yield return new object[] { linkedList };
yield return new object[] { new List<int>{1, 2} };
yield return new object[] { new List<int> { 1, 2 } };

Queue<double> queue = new Queue<double>();
Queue<double> queue = new();
queue.Enqueue(1);
queue.Enqueue(2);
yield return new object[] { queue };
yield return new object[] { new SortedDictionary<string, int>{{"One", 1}, {"Two", 2}} };
yield return new object[] { new SortedList<int, string>{{1, "One"}, {2, "Two"}} };
yield return new object[] { new SortedSet<int>{1, 2} };
yield return new object[] { new SortedSet<int> { 1, 2 } };

var stack = new Stack<object>();
Stack<object> stack = new();
stack.Push(1);
stack.Push(2);
yield return new object[] { stack };

yield return new object[] { new Dictionary<double, float>{{1.0, 1.0f}, {2.0, 2.0f}}.Keys };
yield return new object[] { new Dictionary<float, double>{{1.0f, 1.0}, {2.0f, 2.0}}.Values };
yield return new object[] { new SortedDictionary<Guid, string>{{Guid.NewGuid(), "One"}, {Guid.NewGuid(), "Two"}}.Keys };
yield return new object[] { new SortedDictionary<long, Guid>{{1L, Guid.NewGuid()}, {2L, Guid.NewGuid()}}.Values };
yield return new object[] { new SortedList<string, int>{{"One", 1}, {"Two", 2}}.Keys };
yield return new object[] { new SortedList<float, long>{{1f, 1L}, {2f, 2L}}.Values };
yield return new object[] { new SortedList<string, int> { { "One", 1 }, { "Two", 2 } }.Keys };
yield return new object[] { new SortedList<float, long> { { 1f, 1L }, { 2f, 2L } }.Values };

yield return new object[] { new Dictionary<double, float> { { 1.0, 1.0f }, { 2.0, 2.0f } }.Keys };
yield return new object[] { new Dictionary<float, double> { { 1.0f, 1.0 }, { 2.0f, 2.0 } }.Values };
yield return new object[] { new SortedDictionary<Guid, string> { { Guid.NewGuid(), "One" }, { Guid.NewGuid(), "Two" } }.Keys };
yield return new object[] { new SortedDictionary<long, Guid> { { 1L, Guid.NewGuid() }, { 2L, Guid.NewGuid() } }.Values };
}

public static IEnumerable<object[]> TestDebuggerAttributes_InputsPresentedAsDictionary()
{
#if !NETFRAMEWORK
return TestDebuggerAttributes_NonGenericDictionaries()
.Concat(TestDebuggerAttributes_GenericDictionaries());
#else
// In .Net Framework only non-generic dictionaries are displayed in a dictionary format by the debugger.
return TestDebuggerAttributes_NonGenericDictionaries();
#endif
}

public static IEnumerable<object[]> TestDebuggerAttributes_InputsPresentedAsList()
{
#if !NETFRAMEWORK
return TestDebuggerAttributes_ListInputs();
#else
// In .Net Framework generic dictionaries are displayed in a list format by the debugger.
return TestDebuggerAttributes_GenericDictionaries()
.Select(t => new[] { t[0] })
.Concat(TestDebuggerAttributes_ListInputs());
#endif
}

public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
{
return TestDebuggerAttributes_InputsPresentedAsDictionary()
.Select(t => new[] { t[0] })
.Concat(TestDebuggerAttributes_InputsPresentedAsList());
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
[MemberData(nameof(TestDebuggerAttributes_Inputs))]
public static void TestDebuggerAttributes(object obj)
[MemberData(nameof(TestDebuggerAttributes_InputsPresentedAsDictionary))]
public static void TestDebuggerAttributes_Dictionary(IDictionary obj, KeyValuePair<string, string>[] expected)
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(obj);
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj);
PropertyInfo itemProperty = info.Properties.Single(pr => pr.GetCustomAttribute<DebuggerBrowsableAttribute>().State == DebuggerBrowsableState.RootHidden);
Array itemArray = (Array)itemProperty.GetValue(info.Instance);
List<KeyValuePair<string, string>> formatted = itemArray.Cast<object>()
.Select(DebuggerAttributes.ValidateFullyDebuggerDisplayReferences)
.Select(formattedResult => new KeyValuePair<string, string>(formattedResult.Key, formattedResult.Value))
.ToList();

CollectionAsserts.EqualUnordered((ICollection)expected, formatted);
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsDebuggerTypeProxyAttributeSupported))]
[MemberData(nameof(TestDebuggerAttributes_InputsPresentedAsList))]
public static void TestDebuggerAttributes_List(object obj)
{
DebuggerAttributes.ValidateDebuggerDisplayReferences(obj);
DebuggerAttributeInfo info = DebuggerAttributes.ValidateDebuggerTypeProxyProperties(obj);
Expand Down
89 changes: 65 additions & 24 deletions src/libraries/Common/tests/System/Diagnostics/DebuggerAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
Expand All @@ -15,6 +15,13 @@ internal class DebuggerAttributeInfo
public IEnumerable<PropertyInfo> Properties { get; set; }
}

internal class DebuggerDisplayResult
{
public string Value { get; set; }
public string Key { get; set; }
public string Type { get; set; }
}

internal static class DebuggerAttributes
{
internal static object GetFieldValue(object obj, string fieldName)
Expand Down Expand Up @@ -86,18 +93,52 @@ public static IEnumerable<PropertyInfo> GetDebuggerVisibleProperties(Type debugg

public static Type GetProxyType(Type type) => GetProxyType(type, type.GenericTypeArguments);

private static Type GetProxyType(Type type, Type[] genericTypeArguments)
internal static DebuggerDisplayResult ValidateFullyDebuggerDisplayReferences(object obj)
{
CustomAttributeData cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute));

// Get the text of the DebuggerDisplayAttribute
string attrText = (string)cad.ConstructorArguments[0].Value;
string formattedValue = EvaluateDisplayString(attrText, obj);

string formattedKey = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Name), cad, obj);
string formattedType = FormatDebuggerDisplayNamedArgument(nameof(DebuggerDisplayAttribute.Type), cad, obj);

return new DebuggerDisplayResult { Value = formattedValue, Key = formattedKey, Type = formattedType };
}

internal static string ValidateDebuggerDisplayReferences(object obj)
{
CustomAttributeData cad = FindAttribute(obj.GetType(), attributeType: typeof(DebuggerDisplayAttribute));

// Get the text of the DebuggerDisplayAttribute
string attrText = (string)cad.ConstructorArguments[0].Value;

return EvaluateDisplayString(attrText, obj);
}

private static CustomAttributeData FindAttribute(Type type, Type attributeType)
{
// Get the DebuggerTypeProxyAttribute for obj
CustomAttributeData[] attrs =
type.GetTypeInfo().CustomAttributes
.Where(a => a.AttributeType == typeof(DebuggerTypeProxyAttribute))
.ToArray();
if (attrs.Length != 1)
for (Type t = type; t != null; t = t.BaseType)
{
throw new InvalidOperationException($"Expected one DebuggerTypeProxyAttribute on {type}.");
CustomAttributeData[] attributes = t.GetTypeInfo().CustomAttributes
.Where(a => a.AttributeType == attributeType)
.ToArray();
if (attributes.Length != 0)
{
if (attributes.Length > 1)
{
throw new InvalidOperationException($"Expected one {attributeType.Name} on {type} but found more.");
}
return attributes[0];
}
}
CustomAttributeData cad = attrs[0];
throw new InvalidOperationException($"Expected one {attributeType.Name} on {type}.");
}

private static Type GetProxyType(Type type, Type[] genericTypeArguments)
{
CustomAttributeData cad = FindAttribute(type, attributeType: typeof(DebuggerTypeProxyAttribute));

Type proxyType = cad.ConstructorArguments[0].ArgumentType == typeof(Type) ?
(Type)cad.ConstructorArguments[0].Value :
Expand All @@ -110,24 +151,24 @@ private static Type GetProxyType(Type type, Type[] genericTypeArguments)
return proxyType;
}

internal static string ValidateDebuggerDisplayReferences(object obj)
private static string FormatDebuggerDisplayNamedArgument(string argumentName, CustomAttributeData debuggerDisplayAttributeData, object obj)
{
// Get the DebuggerDisplayAttribute for obj
Type objType = obj.GetType();
CustomAttributeData[] attrs =
objType.GetTypeInfo().CustomAttributes
.Where(a => a.AttributeType == typeof(DebuggerDisplayAttribute))
.ToArray();
if (attrs.Length != 1)
CustomAttributeNamedArgument namedAttribute = debuggerDisplayAttributeData.NamedArguments.FirstOrDefault(na => na.MemberName == argumentName);
if (namedAttribute != default)
{
throw new InvalidOperationException($"Expected one DebuggerDisplayAttribute on {objType}.");
string? value = (string?)namedAttribute.TypedValue.Value;
if (!string.IsNullOrEmpty(value))
{
return EvaluateDisplayString(value, obj);
}
}
CustomAttributeData cad = attrs[0];

// Get the text of the DebuggerDisplayAttribute
string attrText = (string)cad.ConstructorArguments[0].Value;
return "";
}

string[] segments = attrText.Split(new[] { '{', '}' });
private static string EvaluateDisplayString(string displayString, object obj)
{
Type objType = obj.GetType();
string[] segments = displayString.Split(['{', '}']);

if (segments.Length % 2 == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<Compile Include="System\Collections\SortedList.cs" />
<Compile Include="System\Collections\Stack.cs" />
<Compile Include="System\Collections\Specialized\CollectionsUtil.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\KeyValuePairs.cs"
Link="Common\System\Collections\KeyValuePairs.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs"
Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
**
===========================================================*/

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
Expand Down Expand Up @@ -351,12 +352,12 @@ public virtual void CopyTo(Array array, int arrayIndex)
// KeyValuePairs is different from Dictionary Entry in that it has special
// debugger attributes on its fields.

internal virtual KeyValuePairs[] ToKeyValuePairsArray()
internal virtual DebugViewDictionaryItem<object, object?>[] ToDebugViewDictionaryItemArray()
{
KeyValuePairs[] array = new KeyValuePairs[Count];
var array = new DebugViewDictionaryItem<object, object?>[Count];
for (int i = 0; i < Count; i++)
{
array[i] = new KeyValuePairs(keys[i], values[i]);
array[i] = new DebugViewDictionaryItem<object, object?>(keys[i], values[i]);
}
return array;
}
Expand Down Expand Up @@ -766,9 +767,9 @@ public override void SetByIndex(int index, object? value)
}
}

internal override KeyValuePairs[] ToKeyValuePairsArray()
internal override DebugViewDictionaryItem<object, object?>[] ToDebugViewDictionaryItemArray()
{
return _list.ToKeyValuePairsArray();
return _list.ToDebugViewDictionaryItemArray();
}

public override void TrimToSize()
Expand Down Expand Up @@ -1097,11 +1098,11 @@ public SortedListDebugView(SortedList sortedList)
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePairs[] Items
public DebugViewDictionaryItem<object, object?>[] Items
{
get
{
return _sortedList.ToKeyValuePairsArray();
return _sortedList.ToDebugViewDictionaryItemArray();
}
}
}
Expand Down
Loading

0 comments on commit 34bf55c

Please sign in to comment.