diff --git a/Microsoft.Toolkit/Extensions/TypeExtensions.cs b/Microsoft.Toolkit/Extensions/TypeExtensions.cs
index 3d84b9aed68..ddbdc934cd4 100644
--- a/Microsoft.Toolkit/Extensions/TypeExtensions.cs
+++ b/Microsoft.Toolkit/Extensions/TypeExtensions.cs
@@ -39,7 +39,8 @@ public static class TypeExtensions
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(object)] = "object",
- [typeof(string)] = "string"
+ [typeof(string)] = "string",
+ [typeof(void)] = "void"
};
///
@@ -56,7 +57,7 @@ public static class TypeExtensions
public static string ToTypeString(this Type type)
{
// Local function to create the formatted string for a given type
- static string FormatDisplayString(Type type)
+ static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan typeArguments)
{
// Primitive types use the keyword name
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
@@ -64,62 +65,138 @@ static string FormatDisplayString(Type type)
return typeName!;
}
- // Generic types
- if (
-#if NETSTANDARD1_4
- type.GetTypeInfo().IsGenericType &&
-#else
- type.IsGenericType &&
-#endif
- type.FullName is { } fullName &&
- fullName.Split('`') is { } tokens &&
- tokens.Length > 0 &&
- tokens[0] is { } genericName &&
- genericName.Length > 0)
+ // Array types are displayed as Foo[]
+ if (type.IsArray)
{
- var typeArguments = type.GetGenericArguments().Select(FormatDisplayString);
+ var elementType = type.GetElementType()!;
+ var rank = type.GetArrayRank();
+
+ return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
+ }
+
+ // By checking generic types here we are only interested in specific cases,
+ // ie. nullable value types or value typles. We have a separate path for custom
+ // generic types, as we can't rely on this API in that case, as it doesn't show
+ // a difference between nested types that are themselves generic, or nested simple
+ // types from a generic declaring type. To deal with that, we need to manually track
+ // the offset within the array of generic arguments for the whole constructed type.
+ if (type.IsGenericType())
+ {
+ var genericTypeDefinition = type.GetGenericTypeDefinition();
// Nullable types are displayed as T?
- var genericType = type.GetGenericTypeDefinition();
- if (genericType == typeof(Nullable<>))
+ if (genericTypeDefinition == typeof(Nullable<>))
{
- return $"{typeArguments.First()}?";
+ var nullableArguments = type.GetGenericArguments();
+
+ return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
}
// ValueTuple types are displayed as (T1, T2)
- if (genericType == typeof(ValueTuple<>) ||
- genericType == typeof(ValueTuple<,>) ||
- genericType == typeof(ValueTuple<,,>) ||
- genericType == typeof(ValueTuple<,,,>) ||
- genericType == typeof(ValueTuple<,,,,>) ||
- genericType == typeof(ValueTuple<,,,,,>) ||
- genericType == typeof(ValueTuple<,,,,,,>) ||
- genericType == typeof(ValueTuple<,,,,,,,>))
+ if (genericTypeDefinition == typeof(ValueTuple<>) ||
+ genericTypeDefinition == typeof(ValueTuple<,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,,,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,,,,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,,,,,>) ||
+ genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
{
- return $"({string.Join(", ", typeArguments)})";
+ var formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
+
+ return $"({string.Join(", ", formattedTypes)})";
}
+ }
+
+ string displayName;
+
+ // Generic types
+ if (type.Name.Contains('`'))
+ {
+ // Retrieve the current generic arguments for the current type (leaf or not)
+ var tokens = type.Name.Split('`');
+ var genericArgumentsCount = int.Parse(tokens[1]);
+ var typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
+ var currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
+ var formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
// Standard generic types are displayed as Foo
- return $"{genericName}<{string.Join(", ", typeArguments)}>";
+ displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>";
+
+ // Track the current offset for the shared generic arguments list
+ genericTypeOffset += genericArgumentsCount;
+ }
+ else
+ {
+ // Simple custom types
+ displayName = type.Name;
}
- // Array types are displayed as Foo[]
- if (type.IsArray)
+ // If the type is nested, recursively format the hierarchy as well
+ if (type.IsNested)
{
- var elementType = type.GetElementType();
- var rank = type.GetArrayRank();
+ var openDeclaringType = type.DeclaringType!;
+ var rootGenericArguments = typeArguments.Slice(0, typeArguments.Length - genericTypeOffset).ToArray();
+
+ // If the declaring type is generic, we need to reconstruct the closed type
+ // manually, as the declaring type instance doesn't retain type information.
+ if (rootGenericArguments.Length > 0)
+ {
+ var closedDeclaringType = openDeclaringType.GetGenericTypeDefinition().MakeGenericType(rootGenericArguments);
- return $"{FormatDisplayString(elementType)}[{new string(',', rank - 1)}]";
+ return $"{FormatDisplayString(closedDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
+ }
+
+ return $"{FormatDisplayString(openDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
}
- return type.ToString();
+ return $"{type.Namespace}.{displayName}";
}
// Atomically get or build the display string for the current type.
- // Manually create a static lambda here to enable caching of the generated closure.
- // This is a workaround for the missing caching for method group conversions, and should
- // be removed once this issue is resolved: https://github.com/dotnet/roslyn/issues/5835.
- return DisplayNames.GetValue(type, t => FormatDisplayString(t));
+ return DisplayNames.GetValue(type, t =>
+ {
+ // By-ref types are displayed as T&
+ if (t.IsByRef)
+ {
+ t = t.GetElementType()!;
+
+ return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}&";
+ }
+
+ // Pointer types are displayed as T*
+ if (t.IsPointer)
+ {
+ int depth = 0;
+
+ // Calculate the pointer indirection level
+ while (t.IsPointer)
+ {
+ depth++;
+ t = t.GetElementType()!;
+ }
+
+ return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}{new string('*', depth)}";
+ }
+
+ // Standard path for concrete types
+ return FormatDisplayString(t, 0, t.GetGenericArguments());
+ });
+ }
+
+ ///
+ /// Returns whether or not a given type is generic.
+ ///
+ /// The input type.
+ /// Whether or not the input type is generic.
+ [Pure]
+ private static bool IsGenericType(this Type type)
+ {
+#if NETSTANDARD1_4
+ return type.GetTypeInfo().IsGenericType;
+#else
+ return type.IsGenericType;
+#endif
}
#if NETSTANDARD1_4
@@ -128,6 +205,7 @@ tokens[0] is { } genericName &&
///
/// The input type.
/// An array of types representing the generic arguments.
+ [Pure]
private static Type[] GetGenericArguments(this Type type)
{
return type.GetTypeInfo().GenericTypeParameters;
@@ -139,6 +217,7 @@ private static Type[] GetGenericArguments(this Type type)
/// The input type.
/// The type to check against.
/// if is an instance of , otherwise.
+ [Pure]
internal static bool IsInstanceOfType(this Type type, object value)
{
return type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());
diff --git a/UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs b/UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs
index 5acb2392faa..00fa6df6a8a 100644
--- a/UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs
+++ b/UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using Microsoft.Toolkit.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -15,32 +14,148 @@ public class Test_TypeExtensions
{
[TestCategory("TypeExtensions")]
[TestMethod]
- public void Test_TypeExtensions_BuiltInTypes()
+ [DataRow("bool", typeof(bool))]
+ [DataRow("int", typeof(int))]
+ [DataRow("float", typeof(float))]
+ [DataRow("double", typeof(double))]
+ [DataRow("decimal", typeof(decimal))]
+ [DataRow("object", typeof(object))]
+ [DataRow("string", typeof(string))]
+ [DataRow("void", typeof(void))]
+ public void Test_TypeExtensions_BuiltInTypes(string name, Type type)
{
- Assert.AreEqual("bool", typeof(bool).ToTypeString());
- Assert.AreEqual("int", typeof(int).ToTypeString());
- Assert.AreEqual("float", typeof(float).ToTypeString());
- Assert.AreEqual("double", typeof(double).ToTypeString());
- Assert.AreEqual("decimal", typeof(decimal).ToTypeString());
- Assert.AreEqual("object", typeof(object).ToTypeString());
- Assert.AreEqual("string", typeof(string).ToTypeString());
+ Assert.AreEqual(name, type.ToTypeString());
}
[TestCategory("TypeExtensions")]
[TestMethod]
- [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "Nullable value tuple type")]
- public void Test_TypeExtensions_GenericTypes()
+ [DataRow("int?", typeof(int?))]
+ [DataRow("System.DateTime?", typeof(DateTime?))]
+ [DataRow("(int, float)", typeof((int, float)))]
+ [DataRow("(double?, string, int)?", typeof((double?, string, int)?))]
+ [DataRow("int[]", typeof(int[]))]
+ [DataRow("int[,]", typeof(int[,]))]
+ [DataRow("System.Span", typeof(Span))]
+ [DataRow("System.Memory", typeof(Memory))]
+ [DataRow("System.Collections.Generic.IEnumerable", typeof(IEnumerable))]
+ [DataRow("System.Collections.Generic.Dictionary>", typeof(Dictionary>))]
+ public void Test_TypeExtensions_GenericTypes(string name, Type type)
{
- Assert.AreEqual("int?", typeof(int?).ToTypeString());
- Assert.AreEqual("System.DateTime?", typeof(DateTime?).ToTypeString());
- Assert.AreEqual("(int, float)", typeof((int, float)).ToTypeString());
- Assert.AreEqual("(double?, string, int)?", typeof((double?, string, int)?).ToTypeString());
- Assert.AreEqual("int[]", typeof(int[]).ToTypeString());
- Assert.AreEqual(typeof(int[,]).ToTypeString(), "int[,]");
- Assert.AreEqual("System.Span", typeof(Span).ToTypeString());
- Assert.AreEqual("System.Memory", typeof(Memory).ToTypeString());
- Assert.AreEqual("System.Collections.Generic.IEnumerable", typeof(IEnumerable).ToTypeString());
- Assert.AreEqual(typeof(Dictionary>).ToTypeString(), "System.Collections.Generic.Dictionary>");
+ Assert.AreEqual(name, type.ToTypeString());
}
+
+ [TestCategory("TypeExtensions")]
+ [TestMethod]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit", typeof(Animal.Rabbit))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit", typeof(Animal.Rabbit))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo", typeof(Animal.Rabbit.Foo))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo", typeof(Animal.Rabbit.Foo))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo", typeof(Animal.Rabbit.Foo))]
+ [DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit.Foo