Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve and unify debug views of dictionaries. #92534

Merged
merged 11 commits into from
Nov 2, 2023
Merged
154 changes: 136 additions & 18 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,87 @@ namespace System.Collections.Tests
{
public class DebugView_Tests
{
public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries()
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
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 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"),
}
};
}

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>() };
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -29,40 +103,84 @@ public static IEnumerable<object[]> TestDebuggerAttributes_Inputs()
yield return new object[] { new SortedDictionary<long, Guid>().Values };
yield return new object[] { new SortedList<string, int>().Keys };
yield return new object[] { new SortedList<float, long>().Values };
yield return new object[] { new SortedList<int, string>() };

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.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.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.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 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 };
}

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);
var itemArray = itemProperty.GetValue(info.Instance) as Array;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
var 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 (var t = type; t != null; t = t.BaseType)
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
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.");
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
}
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}.");
var 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 @@ -13,8 +13,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>
<Reference Include="System.Runtime" />
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
Loading