diff --git a/src/CommunityToolkit.Diagnostics/Extensions/TypeExtensions.cs b/src/CommunityToolkit.Diagnostics/Extensions/TypeExtensions.cs index fb6942ba2..159743812 100644 --- a/src/CommunityToolkit.Diagnostics/Extensions/TypeExtensions.cs +++ b/src/CommunityToolkit.Diagnostics/Extensions/TypeExtensions.cs @@ -131,7 +131,18 @@ private static string FormatDisplayString(Type type, int genericTypeOffset, Read genericTypeDefinition == typeof(ValueTuple<,,,,,,>) || genericTypeDefinition == typeof(ValueTuple<,,,,,,,>)) { - IEnumerable formattedTypes = FormatDisplayStringForAllTypes(type.GetGenericArguments()); + Type[] tupleArguments = type.GetGenericArguments(); + + // If the tuple is using open generics, format in the form (,,,) to match other generic type + // definitions. Note that it's not possible to have a mix of generic type parameters and + // concrete type arguments, so we just need to check the first one to know what to do here. + if (tupleArguments[0].IsGenericParameter) + { + return $"({new string(',', tupleArguments.Length - 1)})"; + } + + // If the tuple type is constructed, format it normally in the (T1, T2, ..., TN) format + IEnumerable formattedTypes = FormatDisplayStringForAllTypes(tupleArguments); return $"({string.Join(", ", formattedTypes)})"; } @@ -147,10 +158,19 @@ private static string FormatDisplayString(Type type, int genericTypeOffset, Read int genericArgumentsCount = int.Parse(tokens[1]); int typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount; Type[] currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray(); - IEnumerable formattedTypes = FormatDisplayStringForAllTypes(currentTypeArguments); - // Standard generic types are displayed as Foo - displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>"; + // Special case generic type parameters (same as with tuples) + if (currentTypeArguments[0].IsGenericParameter) + { + displayName = $"{tokens[0]}<{new string(',', currentTypeArguments.Length - 1)}>"; + } + else + { + IEnumerable formattedTypes = FormatDisplayStringForAllTypes(currentTypeArguments); + + // Standard generic types are displayed as Foo + displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>"; + } // Track the current offset for the shared generic arguments list genericTypeOffset += genericArgumentsCount; @@ -161,8 +181,10 @@ private static string FormatDisplayString(Type type, int genericTypeOffset, Read displayName = type.Name; } - // If the type is nested, recursively format the hierarchy as well - if (type.IsNested) + // If the type is nested, recursively format the hierarchy as well, unless the type is a generic type parameter. In that case, + // the declaring type would return the parent class that defined the generic type parameter. However, the current invocation of + // FormatDisplayString has already been invoked recursively while trying to format the parent class, so we need to stop here. + if (type.IsNested && !type.IsGenericParameter) { return $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}"; } diff --git a/tests/CommunityToolkit.Diagnostics.UnitTests/Extensions/Test_TypeExtensions.cs b/tests/CommunityToolkit.Diagnostics.UnitTests/Extensions/Test_TypeExtensions.cs index b86e87822..a7306596b 100644 --- a/tests/CommunityToolkit.Diagnostics.UnitTests/Extensions/Test_TypeExtensions.cs +++ b/tests/CommunityToolkit.Diagnostics.UnitTests/Extensions/Test_TypeExtensions.cs @@ -41,12 +41,26 @@ public void Test_TypeExtensions_GenericTypes(string name, Type type) Assert.AreEqual(name, type.ToTypeString()); } + [TestMethod] + [DataRow("System.Span<>", typeof(Span<>))] + [DataRow("System.Collections.Generic.List<>", typeof(List<>))] + [DataRow("System.Collections.Generic.Dictionary<,>", typeof(Dictionary<,>))] + [DataRow("(,)", typeof(ValueTuple<,>))] + [DataRow("(,,,,,)", typeof(ValueTuple<,,,,,>))] + [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<>.Foo<>", typeof(Animal.Rabbit<>.Foo<>))] + [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<,>.Foo<>", typeof(Animal.Llama<,>.Foo<>))] + public void Test_TypeExtensions_OpenGenericTypes(string name, Type type) + { + Assert.AreEqual(name, type.ToTypeString()); + } + [TestMethod] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit", typeof(Animal.Rabbit))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit", typeof(Animal.Rabbit))] + [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit", typeof(Animal.Rabbit))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo", typeof(Animal.Rabbit.Foo))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))] [DataRow("CommunityToolkit.Diagnostics.UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo", typeof(Animal.Rabbit.Foo))]