|
4 | 4 |
|
5 | 5 | using System;
|
6 | 6 | using System.Collections.Generic;
|
7 |
| -using System.Linq; |
8 | 7 | using System.Runtime.CompilerServices;
|
9 | 8 |
|
10 | 9 | namespace CommunityToolkit.Diagnostics;
|
@@ -43,97 +42,12 @@ public static class TypeExtensions
|
43 | 42 | private static readonly ConditionalWeakTable<Type, string> DisplayNames = new();
|
44 | 43 |
|
45 | 44 | /// <summary>
|
46 |
| - /// Returns a simple string representation of a type. |
| 45 | + /// Returns a simple <see cref="string"/> representation of a type. |
47 | 46 | /// </summary>
|
48 | 47 | /// <param name="type">The input type.</param>
|
49 |
| - /// <returns>The string representation of <paramref name="type"/>.</returns> |
| 48 | + /// <returns>The <see cref="string"/> representation of <paramref name="type"/>.</returns> |
50 | 49 | public static string ToTypeString(this Type type)
|
51 | 50 | {
|
52 |
| - // Local function to create the formatted string for a given type |
53 |
| - static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments) |
54 |
| - { |
55 |
| - // Primitive types use the keyword name |
56 |
| - if (BuiltInTypesMap.TryGetValue(type, out string? typeName)) |
57 |
| - { |
58 |
| - return typeName!; |
59 |
| - } |
60 |
| - |
61 |
| - // Array types are displayed as Foo[] |
62 |
| - if (type.IsArray) |
63 |
| - { |
64 |
| - Type? elementType = type.GetElementType()!; |
65 |
| - int rank = type.GetArrayRank(); |
66 |
| - |
67 |
| - return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]"; |
68 |
| - } |
69 |
| - |
70 |
| - // By checking generic types here we are only interested in specific cases, |
71 |
| - // ie. nullable value types or value tuples. We have a separate path for custom |
72 |
| - // generic types, as we can't rely on this API in that case, as it doesn't show |
73 |
| - // a difference between nested types that are themselves generic, or nested simple |
74 |
| - // types from a generic declaring type. To deal with that, we need to manually track |
75 |
| - // the offset within the array of generic arguments for the whole constructed type. |
76 |
| - if (type.IsGenericType) |
77 |
| - { |
78 |
| - Type? genericTypeDefinition = type.GetGenericTypeDefinition(); |
79 |
| - |
80 |
| - // Nullable<T> types are displayed as T? |
81 |
| - if (genericTypeDefinition == typeof(Nullable<>)) |
82 |
| - { |
83 |
| - Type[]? nullableArguments = type.GetGenericArguments(); |
84 |
| - |
85 |
| - return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?"; |
86 |
| - } |
87 |
| - |
88 |
| - // ValueTuple<T1, T2> types are displayed as (T1, T2) |
89 |
| - if (genericTypeDefinition == typeof(ValueTuple<>) || |
90 |
| - genericTypeDefinition == typeof(ValueTuple<,>) || |
91 |
| - genericTypeDefinition == typeof(ValueTuple<,,>) || |
92 |
| - genericTypeDefinition == typeof(ValueTuple<,,,>) || |
93 |
| - genericTypeDefinition == typeof(ValueTuple<,,,,>) || |
94 |
| - genericTypeDefinition == typeof(ValueTuple<,,,,,>) || |
95 |
| - genericTypeDefinition == typeof(ValueTuple<,,,,,,>) || |
96 |
| - genericTypeDefinition == typeof(ValueTuple<,,,,,,,>)) |
97 |
| - { |
98 |
| - IEnumerable<string>? formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments())); |
99 |
| - |
100 |
| - return $"({string.Join(", ", formattedTypes)})"; |
101 |
| - } |
102 |
| - } |
103 |
| - |
104 |
| - string displayName; |
105 |
| - |
106 |
| - // Generic types |
107 |
| - if (type.Name.Contains('`')) |
108 |
| - { |
109 |
| - // Retrieve the current generic arguments for the current type (leaf or not) |
110 |
| - string[]? tokens = type.Name.Split('`'); |
111 |
| - int genericArgumentsCount = int.Parse(tokens[1]); |
112 |
| - int typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount; |
113 |
| - Type[]? currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray(); |
114 |
| - IEnumerable<string>? formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments())); |
115 |
| - |
116 |
| - // Standard generic types are displayed as Foo<T> |
117 |
| - displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>"; |
118 |
| - |
119 |
| - // Track the current offset for the shared generic arguments list |
120 |
| - genericTypeOffset += genericArgumentsCount; |
121 |
| - } |
122 |
| - else |
123 |
| - { |
124 |
| - // Simple custom types |
125 |
| - displayName = type.Name; |
126 |
| - } |
127 |
| - |
128 |
| - // If the type is nested, recursively format the hierarchy as well |
129 |
| - if (type.IsNested) |
130 |
| - { |
131 |
| - return $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}"; |
132 |
| - } |
133 |
| - |
134 |
| - return $"{type.Namespace}.{displayName}"; |
135 |
| - } |
136 |
| - |
137 | 51 | // Atomically get or build the display string for the current type.
|
138 | 52 | return DisplayNames.GetValue(type, t =>
|
139 | 53 | {
|
@@ -164,4 +78,117 @@ static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan
|
164 | 78 | return FormatDisplayString(t, 0, t.GetGenericArguments());
|
165 | 79 | });
|
166 | 80 | }
|
| 81 | + |
| 82 | + /// <summary> |
| 83 | + /// Formats a given <see cref="Type"/> instance to its <see cref="string"/> representation. |
| 84 | + /// </summary> |
| 85 | + /// <param name="type">The input type.</param> |
| 86 | + /// <param name="genericTypeOffset">The offset into the generic type arguments for <paramref name="type"/>.</param> |
| 87 | + /// <param name="typeArguments">The generic type arguments for <paramref name="type"/>.</param> |
| 88 | + /// <returns>The <see cref="string"/> representation of <paramref name="type"/>.</returns> |
| 89 | + private static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments) |
| 90 | + { |
| 91 | + // Primitive types use the keyword name |
| 92 | + if (BuiltInTypesMap.TryGetValue(type, out string? typeName)) |
| 93 | + { |
| 94 | + return typeName!; |
| 95 | + } |
| 96 | + |
| 97 | + // Array types are displayed as Foo[] |
| 98 | + if (type.IsArray) |
| 99 | + { |
| 100 | + Type elementType = type.GetElementType()!; |
| 101 | + int rank = type.GetArrayRank(); |
| 102 | + |
| 103 | + return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]"; |
| 104 | + } |
| 105 | + |
| 106 | + // By checking generic types here we are only interested in specific cases, |
| 107 | + // ie. nullable value types or value tuples. We have a separate path for custom |
| 108 | + // generic types, as we can't rely on this API in that case, as it doesn't show |
| 109 | + // a difference between nested types that are themselves generic, or nested simple |
| 110 | + // types from a generic declaring type. To deal with that, we need to manually track |
| 111 | + // the offset within the array of generic arguments for the whole constructed type. |
| 112 | + if (type.IsGenericType) |
| 113 | + { |
| 114 | + Type genericTypeDefinition = type.GetGenericTypeDefinition(); |
| 115 | + |
| 116 | + // Nullable<T> types are displayed as T? |
| 117 | + if (genericTypeDefinition == typeof(Nullable<>)) |
| 118 | + { |
| 119 | + Type[] nullableArguments = type.GetGenericArguments(); |
| 120 | + |
| 121 | + return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?"; |
| 122 | + } |
| 123 | + |
| 124 | + // ValueTuple<T1, T2> types are displayed as (T1, T2) |
| 125 | + if (genericTypeDefinition == typeof(ValueTuple<>) || |
| 126 | + genericTypeDefinition == typeof(ValueTuple<,>) || |
| 127 | + genericTypeDefinition == typeof(ValueTuple<,,>) || |
| 128 | + genericTypeDefinition == typeof(ValueTuple<,,,>) || |
| 129 | + genericTypeDefinition == typeof(ValueTuple<,,,,>) || |
| 130 | + genericTypeDefinition == typeof(ValueTuple<,,,,,>) || |
| 131 | + genericTypeDefinition == typeof(ValueTuple<,,,,,,>) || |
| 132 | + genericTypeDefinition == typeof(ValueTuple<,,,,,,,>)) |
| 133 | + { |
| 134 | + IEnumerable<string> formattedTypes = FormatDisplayStringForAllTypes(type.GetGenericArguments()); |
| 135 | + |
| 136 | + return $"({string.Join(", ", formattedTypes)})"; |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + string displayName; |
| 141 | + |
| 142 | + // Generic types |
| 143 | + if (type.Name.AsSpan().IndexOf('`') != -1) |
| 144 | + { |
| 145 | + // Retrieve the current generic arguments for the current type (leaf or not) |
| 146 | + string[] tokens = type.Name.Split('`'); |
| 147 | + int genericArgumentsCount = int.Parse(tokens[1]); |
| 148 | + int typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount; |
| 149 | + Type[] currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray(); |
| 150 | + IEnumerable<string> formattedTypes = FormatDisplayStringForAllTypes(currentTypeArguments); |
| 151 | + |
| 152 | + // Standard generic types are displayed as Foo<T> |
| 153 | + displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>"; |
| 154 | + |
| 155 | + // Track the current offset for the shared generic arguments list |
| 156 | + genericTypeOffset += genericArgumentsCount; |
| 157 | + } |
| 158 | + else |
| 159 | + { |
| 160 | + // Simple custom types |
| 161 | + displayName = type.Name; |
| 162 | + } |
| 163 | + |
| 164 | + // If the type is nested, recursively format the hierarchy as well |
| 165 | + if (type.IsNested) |
| 166 | + { |
| 167 | + return $"{FormatDisplayString(type.DeclaringType!, genericTypeOffset, typeArguments)}.{displayName}"; |
| 168 | + } |
| 169 | + |
| 170 | + return $"{type.Namespace}.{displayName}"; |
| 171 | + } |
| 172 | + |
| 173 | + /// <summary> |
| 174 | + /// Formats a given sequence of <see cref="Type"/> instances to their <see cref="string"/> representations. |
| 175 | + /// </summary> |
| 176 | + /// <param name="types">The input types.</param> |
| 177 | + /// <returns>The <see cref="string"/> representations of <paramref name="types"/>.</returns> |
| 178 | + /// <remarks> |
| 179 | + /// <para> |
| 180 | + /// This method is explicitly an enumerator to avoid having to use LINQ in <see cref="FormatDisplayString"/>, which causes |
| 181 | + /// a noticeable binary size increase in AOT scenarios if reflection is enabled (which is the only supported mode). |
| 182 | + /// </para> |
| 183 | + /// <para> |
| 184 | + /// For future reference, see: <see href="https://github.com/dotnet/runtime/issues/82607#issuecomment-1444443656"/>. |
| 185 | + /// </para> |
| 186 | + /// </remarks> |
| 187 | + private static IEnumerable<string> FormatDisplayStringForAllTypes(Type[] types) |
| 188 | + { |
| 189 | + foreach (Type type in types) |
| 190 | + { |
| 191 | + yield return FormatDisplayString(type, 0, type.GetGenericArguments()); |
| 192 | + } |
| 193 | + } |
167 | 194 | }
|
0 commit comments