@@ -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 ( ) ) ;
0 commit comments