Skip to content

Commit e296861

Browse files
authored
Merge branch 'master' into rename/guard-apis
2 parents 7839c1b + 2b610c4 commit e296861

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)