Skip to content

Commit

Permalink
Unify debug views of immutable dictionaries
Browse files Browse the repository at this point in the history
Fixes dotnet#94289

- Updates the way the debugger displays the remaining dictionaries (Frozen, Immutable, ImmutableSorted, Concurrent) to present their keys and values in separate columns.
- Fixes debugger views of Builder classes of immutable collections. Previous custom implementations incorrectly treated the Builder classes as immutable.
  • Loading branch information
arturek committed Apr 7, 2024
1 parent b8964d8 commit 528aeea
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 82 deletions.
50 changes: 48 additions & 2 deletions src/libraries/Common/tests/System/Collections/DebugView.Tests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -47,8 +50,8 @@ private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries(
new ("[\"Two\"]", "2"),
}
};
CustomKeyedCollection<string, int> collection = new ();
collection.GetKeyForItemHandler = value => (2 * value).ToString();
CustomKeyedCollection<string, int> collection = new();
collection.GetKeyForItemHandler = value => (2 * value).ToString();
collection.InsertItem(0, 1);
collection.InsertItem(1, 3);
yield return new object[] { collection,
Expand All @@ -58,6 +61,49 @@ private static IEnumerable<object[]> TestDebuggerAttributes_GenericDictionaries(
new ("[\"6\"]", "3"),
}
};

yield return new object[] { new ConcurrentDictionary<int, string>(new KeyValuePair<int, string>[] { new(1, "One"), new(2, "Two") }),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToFrozenDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableDictionary().ToBuilder(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableSortedDictionary(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
yield return new object[] { new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }.ToImmutableSortedDictionary().ToBuilder(),
new KeyValuePair<string, string>[]
{
new ("[1]", "\"One\""),
new ("[2]", "\"Two\""),
}
};
}

private static IEnumerable<object[]> TestDebuggerAttributes_NonGenericDictionaries()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
Link="System\Collections\HashHelpers.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Concurrent\IProducerConsumerCollectionDebugView.cs"
Link="System\Collections\Concurrent\IProducerConsumerCollectionDebugView.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 @@ -2346,12 +2346,17 @@ public IDictionaryDebugView(IDictionary<TKey, TValue> dictionary)
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Items
public DebugViewDictionaryItem<TKey, TValue>[] Items
{
get
{
var items = new KeyValuePair<TKey, TValue>[_dictionary.Count];
_dictionary.CopyTo(items, 0);
var keyValuePairs = new KeyValuePair<TKey, TValue>[_dictionary.Count];
_dictionary.CopyTo(keyValuePairs, 0);
var items = new DebugViewDictionaryItem<TKey, TValue>[keyValuePairs.Length];
for (int i = 0; i < items.Length; i++)
{
items[i] = new DebugViewDictionaryItem<TKey, TValue>(keyValuePairs[i]);
}
return items;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ The System.Collections.Immutable library is built-in as part of the shared frame
<Compile Include="System\Runtime.InteropServices\ImmutableCollectionsMarshal.cs" />
<Compile Include="Validation\Requires.cs" />
<Compile Include="$(CommonPath)System\Runtime\Versioning\NonVersionableAttribute.cs" Link="Common\System\Runtime\Versioning\NonVersionableAttribute.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\DebugViewDictionaryItem.cs"
Link="Common\System\Collections\Generic\DebugViewDictionaryItem.cs" />
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\IDictionaryDebugView.cs"
Link="Common\System\Collections\Generic\IDictionaryDebugView.cs" />
<None Include="Interfaces.cd" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public sealed partial class ImmutableDictionary<TKey, TValue>
/// </para>
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ImmutableDictionaryBuilderDebuggerProxy<,>))]
[DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))]
public sealed class Builder : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary
{
/// <summary>
Expand Down Expand Up @@ -709,36 +709,4 @@ private bool Apply(MutationResult result)
}
}
}

/// <summary>
/// A simple view of the immutable collection that the debugger can show to the developer.
/// </summary>
internal sealed class ImmutableDictionaryBuilderDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The collection to be enumerated.
/// </summary>
private readonly ImmutableDictionary<TKey, TValue>.Builder _map;

/// <summary>
/// The simple view of the collection.
/// </summary>
private KeyValuePair<TKey, TValue>[]? _contents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableDictionaryBuilderDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="map">The collection to display in the debugger</param>
public ImmutableDictionaryBuilderDebuggerProxy(ImmutableDictionary<TKey, TValue>.Builder map)
{
Requires.NotNull(map, nameof(map));
_map = map;
}

/// <summary>
/// Gets a simple debugger-viewable collection.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Contents => _contents ??= _map.ToArray(_map.Count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,38 @@ namespace System.Collections.Immutable
/// </summary>
/// <typeparam name="TKey">The type of the dictionary's keys.</typeparam>
/// <typeparam name="TValue">The type of the dictionary's values.</typeparam>
internal sealed class ImmutableDictionaryDebuggerProxy<TKey, TValue> : ImmutableEnumerableDebuggerProxy<KeyValuePair<TKey, TValue>> where TKey : notnull
/// <remarks>
/// This class should only be used with immutable dictionaries, since it
/// caches the dictionary into an array for display in the debugger.
/// </remarks>
internal sealed class ImmutableDictionaryDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The dictionary to show to the debugger.
/// </summary>
private readonly IReadOnlyDictionary<TKey, TValue> _dictionary;

/// <summary>
/// The contents of the dictionary, cached into an array.
/// </summary>
private DebugViewDictionaryItem<TKey, TValue>[]? _cachedContents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableDictionaryDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="dictionary">The enumerable to show in the debugger.</param>
/// <param name="dictionary">The dictionary to show in the debugger.</param>
public ImmutableDictionaryDebuggerProxy(IReadOnlyDictionary<TKey, TValue> dictionary)
: base(enumerable: dictionary)
{
Requires.NotNull(dictionary, nameof(dictionary));
_dictionary = dictionary;
}

/// <summary>
/// Gets the contents of the dictionary for display in the debugger.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public DebugViewDictionaryItem<TKey, TValue>[] Contents => _cachedContents
??= _dictionary.Select(kv => new DebugViewDictionaryItem<TKey, TValue>(kv)).ToArray(_dictionary.Count);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed partial class ImmutableSortedDictionary<TKey, TValue> where TKey :
/// </para>
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(ImmutableSortedDictionaryBuilderDebuggerProxy<,>))]
[DebuggerTypeProxy(typeof(IDictionaryDebugView<,>))]
public sealed class Builder : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary
{
/// <summary>
Expand Down Expand Up @@ -645,35 +645,4 @@ public ImmutableSortedDictionary<TKey, TValue> ToImmutable()
#endregion
}
}
/// <summary>
/// A simple view of the immutable collection that the debugger can show to the developer.
/// </summary>
internal sealed class ImmutableSortedDictionaryBuilderDebuggerProxy<TKey, TValue> where TKey : notnull
{
/// <summary>
/// The collection to be enumerated.
/// </summary>
private readonly ImmutableSortedDictionary<TKey, TValue>.Builder _map;

/// <summary>
/// The simple view of the collection.
/// </summary>
private KeyValuePair<TKey, TValue>[]? _contents;

/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSortedDictionaryBuilderDebuggerProxy{TKey, TValue}"/> class.
/// </summary>
/// <param name="map">The collection to display in the debugger</param>
public ImmutableSortedDictionaryBuilderDebuggerProxy(ImmutableSortedDictionary<TKey, TValue>.Builder map)
{
Requires.NotNull(map, nameof(map));
_map = map;
}

/// <summary>
/// Gets a simple debugger-viewable collection.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<TKey, TValue>[] Contents => _contents ??= _map.ToArray(_map.Count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ internal sealed class IDictionaryDebugView<TKey, TValue> where TKey : notnull

public IDictionaryDebugView(IDictionary<TKey, TValue> dictionary)
{
ArgumentNullException.ThrowIfNull(dictionary);

_dict = dictionary;
_dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
Expand All @@ -39,9 +37,7 @@ internal sealed class DictionaryKeyCollectionDebugView<TKey, TValue>

public DictionaryKeyCollectionDebugView(ICollection<TKey> collection)
{
ArgumentNullException.ThrowIfNull(collection);

_collection = collection;
_collection = collection ?? throw new ArgumentNullException(nameof(collection));
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
Expand All @@ -62,9 +58,7 @@ internal sealed class DictionaryValueCollectionDebugView<TKey, TValue>

public DictionaryValueCollectionDebugView(ICollection<TValue> collection)
{
ArgumentNullException.ThrowIfNull(collection);

_collection = collection;
_collection = collection ?? throw new ArgumentNullException(nameof(collection));
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
Expand Down

0 comments on commit 528aeea

Please sign in to comment.