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