Skip to content

Commit 2b610c4

Browse files
author
msftbot[bot]
authored
Support for nested/pointer/byref types for Type.ToTypeString() (#3468)
## Follow up for #3131 <!-- Add a brief overview here of the feature/bug & fix. --> The `Type.ToTypeString()` introduced with the `Guard` API lacks support for nested types, pointer types and byref types. ## PR Type What kind of change does this PR introduce? <!-- Please uncomment one or more that apply to this PR. --> <!-- - Bugfix --> <!-- - Feature --> <!-- - Code style update (formatting) --> <!-- - Refactoring (no functional changes, no api changes) --> <!-- - Build or CI related changes --> <!-- - Documentation content changes --> <!-- - Sample app changes --> <!-- - Other... Please describe: --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying, or link to a relevant issue. --> The extension doesn't work properly when used with nested, pointer or byref types. It will either crash or just return the wrong formatted type (eg. the declaring type instead of the nested type). ## What is the new behavior? <!-- Describe how was this issue resolved or changed? --> The extension now works correctly with all sorts of nested, pointer and byref types 🎉 ## PR Checklist Please check if your PR fulfills the following requirements: - [X] Tested code with current [supported SDKs](../readme.md#supported) - [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs).~~ - [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~ - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~ - [X] Tests for the changes have been added (for bug fixes / features) (if applicable) - [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*) - [X] Contains **NO** breaking changes
2 parents 79127cf + 2f4a887 commit 2b610c4

File tree

2 files changed

+253
-59
lines changed

2 files changed

+253
-59
lines changed

Microsoft.Toolkit/Extensions/TypeExtensions.cs

Lines changed: 117 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public static class TypeExtensions
3939
[typeof(double)] = "double",
4040
[typeof(decimal)] = "decimal",
4141
[typeof(object)] = "object",
42-
[typeof(string)] = "string"
42+
[typeof(string)] = "string",
43+
[typeof(void)] = "void"
4344
};
4445

4546
/// <summary>
@@ -56,70 +57,146 @@ public static class TypeExtensions
5657
public static string ToTypeString(this Type type)
5758
{
5859
// Local function to create the formatted string for a given type
59-
static string FormatDisplayString(Type type)
60+
static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments)
6061
{
6162
// Primitive types use the keyword name
6263
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
6364
{
6465
return typeName!;
6566
}
6667

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)
68+
// Array types are displayed as Foo[]
69+
if (type.IsArray)
7970
{
80-
var typeArguments = type.GetGenericArguments().Select(FormatDisplayString);
71+
var elementType = type.GetElementType()!;
72+
var rank = type.GetArrayRank();
73+
74+
return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
75+
}
76+
77+
// By checking generic types here we are only interested in specific cases,
78+
// ie. nullable value types or value typles. We have a separate path for custom
79+
// generic types, as we can't rely on this API in that case, as it doesn't show
80+
// a difference between nested types that are themselves generic, or nested simple
81+
// types from a generic declaring type. To deal with that, we need to manually track
82+
// the offset within the array of generic arguments for the whole constructed type.
83+
if (type.IsGenericType())
84+
{
85+
var genericTypeDefinition = type.GetGenericTypeDefinition();
8186

8287
// Nullable<T> types are displayed as T?
83-
var genericType = type.GetGenericTypeDefinition();
84-
if (genericType == typeof(Nullable<>))
88+
if (genericTypeDefinition == typeof(Nullable<>))
8589
{
86-
return $"{typeArguments.First()}?";
90+
var nullableArguments = type.GetGenericArguments();
91+
92+
return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
8793
}
8894

8995
// 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<,,,,,,,>))
96+
if (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<,,,,,,>) ||
103+
genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
98104
{
99-
return $"({string.Join(", ", typeArguments)})";
105+
var formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
106+
107+
return $"({string.Join(", ", formattedTypes)})";
100108
}
109+
}
110+
111+
string displayName;
112+
113+
// Generic types
114+
if (type.Name.Contains('`'))
115+
{
116+
// Retrieve the current generic arguments for the current type (leaf or not)
117+
var tokens = type.Name.Split('`');
118+
var genericArgumentsCount = int.Parse(tokens[1]);
119+
var typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
120+
var currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
121+
var formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
101122

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

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

112-
return $"{FormatDisplayString(elementType)}[{new string(',', rank - 1)}]";
147+
return $"{FormatDisplayString(closedDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
148+
}
149+
150+
return $"{FormatDisplayString(openDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
113151
}
114152

115-
return type.ToString();
153+
return $"{type.Namespace}.{displayName}";
116154
}
117155

118156
// Atomically get or build the display string for the current type.
119-
// Manually create a static lambda here to enable caching of the generated closure.
120-
// This is a workaround for the missing caching for method group conversions, and should
121-
// be removed once this issue is resolved: https://github.com/dotnet/roslyn/issues/5835.
122-
return DisplayNames.GetValue(type, t => FormatDisplayString(t));
157+
return DisplayNames.GetValue(type, t =>
158+
{
159+
// By-ref types are displayed as T&
160+
if (t.IsByRef)
161+
{
162+
t = t.GetElementType()!;
163+
164+
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}&";
165+
}
166+
167+
// Pointer types are displayed as T*
168+
if (t.IsPointer)
169+
{
170+
int depth = 0;
171+
172+
// Calculate the pointer indirection level
173+
while (t.IsPointer)
174+
{
175+
depth++;
176+
t = t.GetElementType()!;
177+
}
178+
179+
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}{new string('*', depth)}";
180+
}
181+
182+
// Standard path for concrete types
183+
return FormatDisplayString(t, 0, t.GetGenericArguments());
184+
});
185+
}
186+
187+
/// <summary>
188+
/// Returns whether or not a given type is generic.
189+
/// </summary>
190+
/// <param name="type">The input type.</param>
191+
/// <returns>Whether or not the input type is generic.</returns>
192+
[Pure]
193+
private static bool IsGenericType(this Type type)
194+
{
195+
#if NETSTANDARD1_4
196+
return type.GetTypeInfo().IsGenericType;
197+
#else
198+
return type.IsGenericType;
199+
#endif
123200
}
124201

125202
#if NETSTANDARD1_4
@@ -128,6 +205,7 @@ tokens[0] is { } genericName &&
128205
/// </summary>
129206
/// <param name="type">The input type.</param>
130207
/// <returns>An array of types representing the generic arguments.</returns>
208+
[Pure]
131209
private static Type[] GetGenericArguments(this Type type)
132210
{
133211
return type.GetTypeInfo().GenericTypeParameters;
@@ -139,6 +217,7 @@ private static Type[] GetGenericArguments(this Type type)
139217
/// <param name="type">The input type.</param>
140218
/// <param name="value">The type to check against.</param>
141219
/// <returns><see langword="true"/> if <paramref name="type"/> is an instance of <paramref name="value"/>, <see langword="false"/> otherwise.</returns>
220+
[Pure]
142221
internal static bool IsInstanceOfType(this Type type, object value)
143222
{
144223
return type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());

UnitTests/UnitTests.Shared/Extensions/Test_TypeExtensions.cs

Lines changed: 136 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System;
66
using System.Collections.Generic;
7-
using System.Diagnostics.CodeAnalysis;
87
using Microsoft.Toolkit.Extensions;
98
using Microsoft.VisualStudio.TestTools.UnitTesting;
109

@@ -15,32 +14,148 @@ public class Test_TypeExtensions
1514
{
1615
[TestCategory("TypeExtensions")]
1716
[TestMethod]
18-
public void Test_TypeExtensions_BuiltInTypes()
17+
[DataRow("bool", typeof(bool))]
18+
[DataRow("int", typeof(int))]
19+
[DataRow("float", typeof(float))]
20+
[DataRow("double", typeof(double))]
21+
[DataRow("decimal", typeof(decimal))]
22+
[DataRow("object", typeof(object))]
23+
[DataRow("string", typeof(string))]
24+
[DataRow("void", typeof(void))]
25+
public void Test_TypeExtensions_BuiltInTypes(string name, Type type)
1926
{
20-
Assert.AreEqual("bool", typeof(bool).ToTypeString());
21-
Assert.AreEqual("int", typeof(int).ToTypeString());
22-
Assert.AreEqual("float", typeof(float).ToTypeString());
23-
Assert.AreEqual("double", typeof(double).ToTypeString());
24-
Assert.AreEqual("decimal", typeof(decimal).ToTypeString());
25-
Assert.AreEqual("object", typeof(object).ToTypeString());
26-
Assert.AreEqual("string", typeof(string).ToTypeString());
27+
Assert.AreEqual(name, type.ToTypeString());
2728
}
2829

2930
[TestCategory("TypeExtensions")]
3031
[TestMethod]
31-
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "Nullable value tuple type")]
32-
public void Test_TypeExtensions_GenericTypes()
32+
[DataRow("int?", typeof(int?))]
33+
[DataRow("System.DateTime?", typeof(DateTime?))]
34+
[DataRow("(int, float)", typeof((int, float)))]
35+
[DataRow("(double?, string, int)?", typeof((double?, string, int)?))]
36+
[DataRow("int[]", typeof(int[]))]
37+
[DataRow("int[,]", typeof(int[,]))]
38+
[DataRow("System.Span<float>", typeof(Span<float>))]
39+
[DataRow("System.Memory<char>", typeof(Memory<char>))]
40+
[DataRow("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>))]
41+
[DataRow("System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>", typeof(Dictionary<int, List<float>>))]
42+
public void Test_TypeExtensions_GenericTypes(string name, Type type)
3343
{
34-
Assert.AreEqual("int?", typeof(int?).ToTypeString());
35-
Assert.AreEqual("System.DateTime?", typeof(DateTime?).ToTypeString());
36-
Assert.AreEqual("(int, float)", typeof((int, float)).ToTypeString());
37-
Assert.AreEqual("(double?, string, int)?", typeof((double?, string, int)?).ToTypeString());
38-
Assert.AreEqual("int[]", typeof(int[]).ToTypeString());
39-
Assert.AreEqual(typeof(int[,]).ToTypeString(), "int[,]");
40-
Assert.AreEqual("System.Span<float>", typeof(Span<float>).ToTypeString());
41-
Assert.AreEqual("System.Memory<char>", typeof(Memory<char>).ToTypeString());
42-
Assert.AreEqual("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>).ToTypeString());
43-
Assert.AreEqual(typeof(Dictionary<int, List<float>>).ToTypeString(), "System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>");
44+
Assert.AreEqual(name, type.ToTypeString());
4445
}
46+
47+
[TestCategory("TypeExtensions")]
48+
[TestMethod]
49+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))]
50+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))]
51+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))]
52+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>", typeof(Animal.Rabbit<int?>))]
53+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string>", typeof(Animal.Rabbit<string>))]
54+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo", typeof(Animal.Rabbit<int>.Foo))]
55+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))]
56+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<string>", typeof(Animal.Rabbit<int>.Foo<string>))]
57+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<int[]>", typeof(Animal.Rabbit<int>.Foo<int[]>))]
58+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string[]>.Foo<object>", typeof(Animal.Rabbit<string[]>.Foo<object>))]
59+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo<(int, int?)>", typeof(Animal.Rabbit<(string, int)?>.Foo<(int, int?)>))]
60+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>", typeof(Animal.Llama<float, DateTime>))]
61+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)>", typeof(Animal.Llama<string, (int?, object)>))]
62+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo", typeof(Animal.Llama<string, (int?, object)?>.Foo))]
63+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo", typeof(Animal.Llama<float, DateTime>.Foo))]
64+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo<string>", typeof(Animal.Llama<string, (int?, object)?>.Foo<string>))]
65+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo<(float?, int)?>", typeof(Animal.Llama<float, DateTime>.Foo<(float?, int)?>))]
66+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<double>", typeof(Vehicle<double>))]
67+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<int?>[]", typeof(Vehicle<int?>[]))]
68+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Vehicle<int>>", typeof(List<Vehicle<int>>))]
69+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>>", typeof(List<Animal.Rabbit<int?>>))]
70+
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime[]>>", typeof(List<Animal.Llama<float, DateTime[]>>))]
71+
public void Test_TypeExtensions_NestedTypes(string name, Type type)
72+
{
73+
Assert.AreEqual(name, type.ToTypeString());
74+
}
75+
76+
#pragma warning disable SA1015 // Closing generic brackets should be spaced correctly
77+
[TestCategory("TypeExtensions")]
78+
[TestMethod]
79+
[DataRow("void*", typeof(void*))]
80+
[DataRow("int**", typeof(int**))]
81+
[DataRow("byte***", typeof(byte***))]
82+
[DataRow("System.Guid*", typeof(Guid*))]
83+
[DataRow("UnitTests.Extensions.Foo<int>*", typeof(Foo<int>*))]
84+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat**", typeof(Animal.Cat**))]
85+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>*", typeof(Animal.Cat<int>*))]
86+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar**", typeof(Animal.Cat<float>.Bar**))]
87+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>***", typeof(Animal.Cat<double>.Bar<int>***))]
88+
public void Test_TypeExtensions_PointerTypes(string name, Type type)
89+
{
90+
Assert.AreEqual(name, type.ToTypeString());
91+
}
92+
#pragma warning restore SA1015
93+
94+
[TestCategory("TypeExtensions")]
95+
[TestMethod]
96+
[DataRow("int&", typeof(int))]
97+
[DataRow("byte&", typeof(byte))]
98+
[DataRow("System.Guid&", typeof(Guid))]
99+
[DataRow("UnitTests.Extensions.Foo<int>&", typeof(Foo<int>))]
100+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat&", typeof(Animal.Cat))]
101+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>&", typeof(Animal.Cat<int>))]
102+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar&", typeof(Animal.Cat<float>.Bar))]
103+
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>&", typeof(Animal.Cat<double>.Bar<int>))]
104+
public void Test_TypeExtensions_RefTypes(string name, Type type)
105+
{
106+
Assert.AreEqual(name, type.MakeByRefType().ToTypeString());
107+
}
108+
109+
private class Animal
110+
{
111+
public struct Cat
112+
{
113+
}
114+
115+
public struct Cat<T1>
116+
{
117+
public struct Bar
118+
{
119+
}
120+
121+
public struct Bar<T2>
122+
{
123+
}
124+
}
125+
126+
public class Dog
127+
{
128+
}
129+
130+
public class Rabbit<T>
131+
{
132+
public class Foo
133+
{
134+
}
135+
136+
public class Foo<T2>
137+
{
138+
}
139+
}
140+
141+
public class Llama<T1, T2>
142+
{
143+
public class Foo
144+
{
145+
}
146+
147+
public class Foo<T3>
148+
{
149+
}
150+
}
151+
}
152+
153+
private class Vehicle<T>
154+
{
155+
}
156+
}
157+
158+
internal struct Foo<T>
159+
{
45160
}
46161
}

0 commit comments

Comments
 (0)