Skip to content

Commit e8f98f2

Browse files
committed
Added support for nested types
1 parent b6c7859 commit e8f98f2

File tree

2 files changed

+169
-34
lines changed

2 files changed

+169
-34
lines changed

Microsoft.Toolkit/Extensions/TypeExtensions.cs

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -56,70 +56,122 @@ public static class TypeExtensions
5656
public static string ToTypeString(this Type type)
5757
{
5858
// Local function to create the formatted string for a given type
59-
static string FormatDisplayString(Type type)
59+
static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments)
6060
{
6161
// Primitive types use the keyword name
6262
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
6363
{
6464
return typeName!;
6565
}
6666

67-
// Generic types
68-
if (
69-
#if NETSTANDARD1_4
70-
type.GetTypeInfo().IsGenericType &&
71-
#else
72-
type.IsGenericType &&
73-
#endif
74-
type.FullName is { } fullName &&
75-
fullName.Split('`') is { } tokens &&
76-
tokens.Length > 0 &&
77-
tokens[0] is { } genericName &&
78-
genericName.Length > 0)
67+
// Array types are displayed as Foo[]
68+
if (type.IsArray)
69+
{
70+
var elementType = type.GetElementType()!;
71+
var rank = type.GetArrayRank();
72+
73+
return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
74+
}
75+
76+
// By checking generic types here we are only interested in specific cases,
77+
// ie. nullable value types or value typles. We have a separate path for custom
78+
// generic types, as we can't rely on this API in that case, as it doesn't show
79+
// a difference between nested types that are themselves generic, or nested simple
80+
// types from a generic declaring type. To deal with that, we need to manually track
81+
// the offset within the array of generic arguments for the whole constructed type.
82+
if (type.IsGenericType())
7983
{
80-
var typeArguments = type.GetGenericArguments().Select(FormatDisplayString);
84+
var genericTypeDefinition = type.GetGenericTypeDefinition();
8185

8286
// Nullable<T> types are displayed as T?
83-
var genericType = type.GetGenericTypeDefinition();
84-
if (genericType == typeof(Nullable<>))
87+
if (genericTypeDefinition == typeof(Nullable<>))
8588
{
86-
return $"{typeArguments.First()}?";
89+
var nullableArguments = type.GetGenericArguments();
90+
91+
return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
8792
}
8893

8994
// ValueTuple<T1, T2> types are displayed as (T1, T2)
90-
if (genericType == typeof(ValueTuple<>) ||
91-
genericType == typeof(ValueTuple<,>) ||
92-
genericType == typeof(ValueTuple<,,>) ||
93-
genericType == typeof(ValueTuple<,,,>) ||
94-
genericType == typeof(ValueTuple<,,,,>) ||
95-
genericType == typeof(ValueTuple<,,,,,>) ||
96-
genericType == typeof(ValueTuple<,,,,,,>) ||
97-
genericType == typeof(ValueTuple<,,,,,,,>))
95+
if (genericTypeDefinition == typeof(ValueTuple<>) ||
96+
genericTypeDefinition == typeof(ValueTuple<,>) ||
97+
genericTypeDefinition == typeof(ValueTuple<,,>) ||
98+
genericTypeDefinition == typeof(ValueTuple<,,,>) ||
99+
genericTypeDefinition == typeof(ValueTuple<,,,,>) ||
100+
genericTypeDefinition == typeof(ValueTuple<,,,,,>) ||
101+
genericTypeDefinition == typeof(ValueTuple<,,,,,,>) ||
102+
genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
98103
{
99-
return $"({string.Join(", ", typeArguments)})";
104+
var formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
105+
106+
return $"({string.Join(", ", formattedTypes)})";
100107
}
108+
}
109+
110+
string displayName;
111+
112+
// Generic types
113+
if (type.Name.Contains('`'))
114+
{
115+
// Retrieve the current generic arguments for the current type (leaf or not)
116+
var tokens = type.Name.Split('`');
117+
var genericArgumentsCount = int.Parse(tokens[1]);
118+
var typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
119+
var currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
120+
var formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
101121

102122
// Standard generic types are displayed as Foo<T>
103-
return $"{genericName}<{string.Join(", ", typeArguments)}>";
123+
displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>";
124+
125+
// Track the current offset for the shared generic arguments list
126+
genericTypeOffset += genericArgumentsCount;
127+
}
128+
else
129+
{
130+
// Simple custom types
131+
displayName = type.Name;
104132
}
105133

106-
// Array types are displayed as Foo[]
107-
if (type.IsArray)
134+
// If the type is nested, recursively format the hierarchy as well
135+
if (type.IsNested)
108136
{
109-
var elementType = type.GetElementType();
110-
var rank = type.GetArrayRank();
137+
var openDeclaringType = type.DeclaringType!;
138+
var rootGenericArguments = typeArguments.Slice(0, typeArguments.Length - genericTypeOffset).ToArray();
139+
140+
// If the declaring type is generic, we need to reconstruct the closed type
141+
// manually, as the declaring type instance doesn't retain type information.
142+
if (rootGenericArguments.Length > 0)
143+
{
144+
var closedDeclaringType = openDeclaringType.GetGenericTypeDefinition().MakeGenericType(rootGenericArguments);
145+
146+
return $"{FormatDisplayString(closedDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
147+
}
111148

112-
return $"{FormatDisplayString(elementType)}[{new string(',', rank - 1)}]";
149+
return $"{FormatDisplayString(openDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
113150
}
114151

115-
return type.ToString();
152+
return $"{type.Namespace}.{displayName}";
116153
}
117154

118155
// Atomically get or build the display string for the current type.
119156
// Manually create a static lambda here to enable caching of the generated closure.
120157
// This is a workaround for the missing caching for method group conversions, and should
121158
// be removed once this issue is resolved: https://github.com/dotnet/roslyn/issues/5835.
122-
return DisplayNames.GetValue(type, t => FormatDisplayString(t));
159+
return DisplayNames.GetValue(type, t => FormatDisplayString(t, 0, type.GetGenericArguments()));
160+
}
161+
162+
/// <summary>
163+
/// Returns whether or not a given type is generic.
164+
/// </summary>
165+
/// <param name="type">The input type.</param>
166+
/// <returns>Whether or not the input type is generic.</returns>
167+
[Pure]
168+
private static bool IsGenericType(this Type type)
169+
{
170+
#if NETSTANDARD1_4
171+
return type.GetTypeInfo().IsGenericType;
172+
#else
173+
return type.IsGenericType;
174+
#endif
123175
}
124176

125177
#if NETSTANDARD1_4
@@ -128,6 +180,7 @@ tokens[0] is { } genericName &&
128180
/// </summary>
129181
/// <param name="type">The input type.</param>
130182
/// <returns>An array of types representing the generic arguments.</returns>
183+
[Pure]
131184
private static Type[] GetGenericArguments(this Type type)
132185
{
133186
return type.GetTypeInfo().GenericTypeParameters;
@@ -139,6 +192,7 @@ private static Type[] GetGenericArguments(this Type type)
139192
/// <param name="type">The input type.</param>
140193
/// <param name="value">The type to check against.</param>
141194
/// <returns><see langword="true"/> if <paramref name="type"/> is an instance of <paramref name="value"/>, <see langword="false"/> otherwise.</returns>
195+
[Pure]
142196
internal static bool IsInstanceOfType(this Type type, object value)
143197
{
144198
return type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());

UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,86 @@ public void Test_TypeExtensions_GenericTypes(string name, Type type)
4242
{
4343
Assert.AreEqual(name, type.ToTypeString());
4444
}
45+
46+
[TestCategory("TypeExtensions")]
47+
[TestMethod]
48+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))]
49+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))]
50+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))]
51+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>", typeof(Animal.Rabbit<int?>))]
52+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string>", typeof(Animal.Rabbit<string>))]
53+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo", typeof(Animal.Rabbit<int>.Foo))]
54+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))]
55+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<string>", typeof(Animal.Rabbit<int>.Foo<string>))]
56+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<int[]>", typeof(Animal.Rabbit<int>.Foo<int[]>))]
57+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string[]>.Foo<object>", typeof(Animal.Rabbit<string[]>.Foo<object>))]
58+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo<(int, int?)>", typeof(Animal.Rabbit<(string, int)?>.Foo<(int, int?)>))]
59+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<float, System.DateTime>", typeof(Animal.Giraffe<float, DateTime>))]
60+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<string, (int?, object)>", typeof(Animal.Giraffe<string, (int?, object)>))]
61+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<string, (int?, object)?>.Foo", typeof(Animal.Giraffe<string, (int?, object)?>.Foo))]
62+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<float, System.DateTime>.Foo", typeof(Animal.Giraffe<float, DateTime>.Foo))]
63+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<string, (int?, object)?>.Foo<string>", typeof(Animal.Giraffe<string, (int?, object)?>.Foo<string>))]
64+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<float, System.DateTime>.Foo<(float?, int)?>", typeof(Animal.Giraffe<float, DateTime>.Foo<(float?, int)?>))]
65+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<double>", typeof(Vehicle<double>))]
66+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<int?>[]", typeof(Vehicle<int?>[]))]
67+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Vehicle<int>>", typeof(List<Vehicle<int>>))]
68+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>>", typeof(List<Animal.Rabbit<int?>>))]
69+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Giraffe<float, System.DateTime[]>>", typeof(List<Animal.Giraffe<float, DateTime[]>>))]
70+
public void Test_TypeExtensions_NestedTypes(string name, Type type)
71+
{
72+
Assert.AreEqual(name, type.ToTypeString());
73+
}
74+
75+
private class Animal
76+
{
77+
public struct Cat
78+
{
79+
}
80+
81+
public struct Cat<T1>
82+
{
83+
public struct Bar
84+
{
85+
}
86+
87+
public struct Bar<T2>
88+
{
89+
}
90+
}
91+
92+
public class Dog
93+
{
94+
}
95+
96+
public class Rabbit<T>
97+
{
98+
public class Foo
99+
{
100+
}
101+
102+
public class Foo<T2>
103+
{
104+
}
105+
}
106+
107+
public class Giraffe<T1, T2>
108+
{
109+
public class Foo
110+
{
111+
}
112+
113+
public class Foo<T3>
114+
{
115+
}
116+
}
117+
}
118+
119+
private class Vehicle<T>
120+
{
121+
}
122+
}
123+
124+
internal struct Foo<T>
125+
{
45126
}
46127
}

0 commit comments

Comments
 (0)