From f0dea3464d471042a59e9c0000a5e65e8c9bc46b Mon Sep 17 00:00:00 2001
From: James Newton-King <james@newtonking.com>
Date: Fri, 15 Sep 2023 22:06:15 +0800
Subject: [PATCH] Add .NET debugging attributes

---
 csharp/src/Google.Protobuf/ByteString.cs      | 16 ++++++++++++++++
 .../Google.Protobuf/Collections/MapField.cs   | 16 ++++++++++++++++
 .../Collections/RepeatedField.cs              | 17 +++++++++++++++++
 .../Reflection/CustomOptions.cs               | 19 +++++++++++++++++++
 .../Reflection/DescriptorBase.cs              |  2 ++
 .../Reflection/ExtensionCollection.cs         | 16 ++++++++++++++++
 .../Reflection/GeneratedClrTypeInfo.cs        |  2 ++
 .../Reflection/MessageDescriptor.cs           | 16 ++++++++++++++++
 .../Reflection/TypeRegistry.cs                | 16 ++++++++++++++++
 csharp/src/Google.Protobuf/UnknownFieldSet.cs | 19 ++++++++++++++++++-
 10 files changed, 138 insertions(+), 1 deletion(-)

diff --git a/csharp/src/Google.Protobuf/ByteString.cs b/csharp/src/Google.Protobuf/ByteString.cs
index e361dd11fe..fc5d925fa7 100644
--- a/csharp/src/Google.Protobuf/ByteString.cs
+++ b/csharp/src/Google.Protobuf/ByteString.cs
@@ -10,6 +10,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Security;
@@ -23,6 +24,8 @@ namespace Google.Protobuf
     /// Immutable array of bytes.
     /// </summary>
     [SecuritySafeCritical]
+    [DebuggerDisplay("Length = {Length}")]
+    [DebuggerTypeProxy(typeof(ByteStringDebugView))]
     public sealed class ByteString : IEnumerable<byte>, IEquatable<ByteString>
     {
         private static readonly ByteString empty = new ByteString(new byte[0]);
@@ -400,5 +403,18 @@ namespace Google.Protobuf
                 outputStream.Write(array, 0, array.Length);
             }
         }
+
+        private sealed class ByteStringDebugView
+        {
+            private readonly ByteString data;
+
+            public ByteStringDebugView(ByteString data)
+            {
+                this.data = data;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public byte[] Items => data.bytes.ToArray();
+        }
     }
 }
\ No newline at end of file
diff --git a/csharp/src/Google.Protobuf/Collections/MapField.cs b/csharp/src/Google.Protobuf/Collections/MapField.cs
index 2e18976578..f0be958303 100644
--- a/csharp/src/Google.Protobuf/Collections/MapField.cs
+++ b/csharp/src/Google.Protobuf/Collections/MapField.cs
@@ -11,6 +11,7 @@ using Google.Protobuf.Compatibility;
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Security;
@@ -43,6 +44,8 @@ namespace Google.Protobuf.Collections
     /// in future versions.
     /// </para>
     /// </remarks>
+    [DebuggerDisplay("Count = {Count}")]
+    [DebuggerTypeProxy(typeof(MapField<,>.MapFieldDebugView))]
     public sealed class MapField<TKey, TValue> : IDeepCloneable<MapField<TKey, TValue>>, IDictionary<TKey, TValue>, IEquatable<MapField<TKey, TValue>>, IDictionary, IReadOnlyDictionary<TKey, TValue>
     {
         private static readonly EqualityComparer<TValue> ValueEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<TValue>();
@@ -683,5 +686,18 @@ namespace Google.Protobuf.Collections
                 }
             }
         }
+
+        private sealed class MapFieldDebugView
+        {
+            private readonly MapField<TKey, TValue> map;
+
+            public MapFieldDebugView(MapField<TKey, TValue> map)
+            {
+                this.map = map;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<TKey, TValue>[] Items => map.list.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
index 38e70156c9..8bf410aa85 100644
--- a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
+++ b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
@@ -10,7 +10,9 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
+using System.Linq;
 using System.Security;
 
 namespace Google.Protobuf.Collections
@@ -24,6 +26,8 @@ namespace Google.Protobuf.Collections
     /// supported by Protocol Buffers but nor does it guarantee that all operations will work in such cases.
     /// </remarks>
     /// <typeparam name="T">The element type of the repeated field.</typeparam>
+    [DebuggerDisplay("Count = {Count}")]
+    [DebuggerTypeProxy(typeof(RepeatedField<>.RepeatedFieldDebugView))]
     public sealed class RepeatedField<T> : IList<T>, IList, IDeepCloneable<RepeatedField<T>>, IEquatable<RepeatedField<T>>, IReadOnlyList<T>
     {
         private static readonly EqualityComparer<T> EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<T>();
@@ -642,5 +646,18 @@ namespace Google.Protobuf.Collections
             }
         }
         #endregion        
+
+        private sealed class RepeatedFieldDebugView
+        {
+            private readonly RepeatedField<T> list;
+
+            public RepeatedFieldDebugView(RepeatedField<T> list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public T[] Items => list.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs b/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
index 21ec57a386..43b5a4c5ab 100644
--- a/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
+++ b/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
@@ -8,8 +8,10 @@
 #endregion
 
 using Google.Protobuf.Collections;
+using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
@@ -38,6 +40,8 @@ namespace Google.Protobuf.Reflection
     /// all the set values are merged together.
     /// </para>
     /// </remarks>
+    [DebuggerDisplay("Count = {DebugCount}")]
+    [DebuggerTypeProxy(typeof(CustomOptionsDebugView))]
     public sealed class CustomOptions
     {
         private const string UnreferencedCodeMessage = "CustomOptions is incompatible with trimming.";
@@ -290,5 +294,20 @@ namespace Google.Protobuf.Reflection
             value = default;
             return false;
         }
+
+        private int DebugCount => values?.Count ?? 0;
+
+        private sealed class CustomOptionsDebugView
+        {
+            private readonly CustomOptions customOptions;
+
+            public CustomOptionsDebugView(CustomOptions customOptions)
+            {
+                this.customOptions = customOptions;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<int, IExtensionValue>[] Items => customOptions.values?.ToArray() ?? new KeyValuePair<int, IExtensionValue>[0];
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs b/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
index 34a73d6abe..e70b23ad1f 100644
--- a/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
+++ b/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
@@ -8,12 +8,14 @@
 #endregion
 
 using System.Collections.Generic;
+using System.Diagnostics;
 
 namespace Google.Protobuf.Reflection
 {
     /// <summary>
     /// Base class for nearly all descriptors, providing common functionality.
     /// </summary>
+    [DebuggerDisplay("Type = {GetType().Name,nq}, FullName = {FullName}")]
     public abstract class DescriptorBase : IDescriptor
     {
         internal DescriptorBase(FileDescriptor file, string fullName, int index)
diff --git a/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs b/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
index 9fcad8942f..5ce2cfe50f 100644
--- a/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
+++ b/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
@@ -9,6 +9,7 @@
 
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 
 namespace Google.Protobuf.Reflection
@@ -16,6 +17,8 @@ namespace Google.Protobuf.Reflection
     /// <summary>
     /// A collection to simplify retrieving the descriptors of extensions in a descriptor for a message
     /// </summary>
+    [DebuggerDisplay("Count = {UnorderedExtensions.Count}")]
+    [DebuggerTypeProxy(typeof(ExtensionCollectionDebugView))]
     public sealed class ExtensionCollection
     {
         private IDictionary<MessageDescriptor, IList<FieldDescriptor>> extensionsByTypeInDeclarationOrder;
@@ -98,5 +101,18 @@ namespace Google.Protobuf.Reflection
             extensionsByTypeInNumberOrder = declarationOrder
                 .ToDictionary(kvp => kvp.Key, kvp => (IList<FieldDescriptor>)new ReadOnlyCollection<FieldDescriptor>(kvp.Value.OrderBy(field => field.FieldNumber).ToArray()));
         }
+
+        private sealed class ExtensionCollectionDebugView
+        {
+            private readonly ExtensionCollection list;
+
+            public ExtensionCollectionDebugView(ExtensionCollection list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public FieldDescriptor[] Items => list.UnorderedExtensions.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs b/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
index f5ef62c9b8..a4748c53db 100644
--- a/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
+++ b/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
@@ -8,6 +8,7 @@
 #endregion
 
 using System;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Google.Protobuf.Reflection
@@ -17,6 +18,7 @@ namespace Google.Protobuf.Reflection
     /// These are constructed as required, and are not long-lived. Hand-written code should
     /// never need to use this type.
     /// </summary>
+    [DebuggerDisplay("ClrType = {ClrType}")]
     public sealed class GeneratedClrTypeInfo
     {
         private static readonly string[] EmptyNames = new string[0];
diff --git a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
index dfa63e47cf..37ca0f152e 100644
--- a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
+++ b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
@@ -10,6 +10,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
@@ -326,6 +327,8 @@ namespace Google.Protobuf.Reflection
         /// <summary>
         /// A collection to simplify retrieving the field accessor for a particular field.
         /// </summary>
+        [DebuggerDisplay("Count = {InFieldNumberOrder().Count}")]
+        [DebuggerTypeProxy(typeof(FieldCollectionDebugView))]
         public sealed class FieldCollection
         {
             private readonly MessageDescriptor messageDescriptor;
@@ -398,6 +401,19 @@ namespace Google.Protobuf.Reflection
                     return fieldDescriptor;
                 }
             }
+
+            private sealed class FieldCollectionDebugView
+            {
+                private readonly FieldCollection collection;
+
+                public FieldCollectionDebugView(FieldCollection collection)
+                {
+                    this.collection = collection;
+                }
+
+                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+                public FieldDescriptor[] Items => collection.InFieldNumberOrder().ToArray();
+            }
         }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs b/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
index b1bf30b714..f0ab81cbe7 100644
--- a/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
+++ b/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
@@ -8,6 +8,7 @@
 #endregion
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 
 namespace Google.Protobuf.Reflection
@@ -15,6 +16,8 @@ namespace Google.Protobuf.Reflection
     /// <summary>
     /// An immutable registry of types which can be looked up by their full name.
     /// </summary>
+    [DebuggerDisplay("Count = {fullNameToMessageMap.Count}")]
+    [DebuggerTypeProxy(typeof(TypeRegistryDebugView))]
     public sealed class TypeRegistry
     {
         /// <summary>
@@ -156,5 +159,18 @@ namespace Google.Protobuf.Reflection
                 return new TypeRegistry(types);
             }
         }
+
+        private sealed class TypeRegistryDebugView
+        {
+            private readonly TypeRegistry list;
+
+            public TypeRegistryDebugView(TypeRegistry list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<string, MessageDescriptor>[] Items => list.fullNameToMessageMap.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/UnknownFieldSet.cs b/csharp/src/Google.Protobuf/UnknownFieldSet.cs
index 4fab6156dc..d0963d257d 100644
--- a/csharp/src/Google.Protobuf/UnknownFieldSet.cs
+++ b/csharp/src/Google.Protobuf/UnknownFieldSet.cs
@@ -9,6 +9,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
 using System.Security;
 
 namespace Google.Protobuf
@@ -22,6 +24,8 @@ namespace Google.Protobuf
     ///
     /// Most users will never need to use this class directly.
     /// </summary>
+    [DebuggerDisplay("Count = {fields.Count}")]
+    [DebuggerTypeProxy(typeof(UnknownFieldSetDebugView))]
     public sealed partial class UnknownFieldSet
     {
         private readonly IDictionary<int, UnknownField> fields = new Dictionary<int, UnknownField>();
@@ -93,7 +97,7 @@ namespace Google.Protobuf
             }
             UnknownFieldSet otherSet = other as UnknownFieldSet;
             IDictionary<int, UnknownField> otherFields = otherSet.fields;
-            if (fields.Count  != otherFields.Count)
+            if (fields.Count != otherFields.Count)
             {
                 return false;
             }
@@ -360,6 +364,19 @@ namespace Google.Protobuf
             unknownFields.MergeFrom(other);
             return unknownFields;
         }
+
+        private sealed class UnknownFieldSetDebugView
+        {
+            private readonly UnknownFieldSet set;
+
+            public UnknownFieldSetDebugView(UnknownFieldSet set)
+            {
+                this.set = set;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<int, UnknownField>[] Items => set.fields.ToArray();
+        }
     }
 }