3
3
4
4
using System ;
5
5
using System . Collections . Generic ;
6
+ using System . Diagnostics . CodeAnalysis ;
6
7
using System . Linq ;
7
- using System . Text ;
8
8
using Microsoft . CodeAnalysis ;
9
9
10
10
namespace Microsoft . AspNetCore . OpenApi . SourceGenerators . Xml ;
@@ -14,14 +14,19 @@ internal sealed record MemberKey(
14
14
MemberType MemberKind ,
15
15
string ? Name ,
16
16
string ? ReturnType ,
17
- string [ ] ? Parameters ) : IEquatable < MemberKey >
17
+ List < string > ? Parameters ) : IEquatable < MemberKey >
18
18
{
19
- private static readonly SymbolDisplayFormat _typeKeyFormat = new (
19
+ private static readonly SymbolDisplayFormat _withTypeParametersFormat = new (
20
20
globalNamespaceStyle : SymbolDisplayGlobalNamespaceStyle . Included ,
21
21
typeQualificationStyle : SymbolDisplayTypeQualificationStyle . NameAndContainingTypesAndNamespaces ,
22
22
genericsOptions : SymbolDisplayGenericsOptions . IncludeTypeParameters ) ;
23
23
24
- public static MemberKey FromMethodSymbol ( IMethodSymbol method , Compilation compilation )
24
+ private static readonly SymbolDisplayFormat _sansTypeParametersFormat = new (
25
+ globalNamespaceStyle : SymbolDisplayGlobalNamespaceStyle . Included ,
26
+ typeQualificationStyle : SymbolDisplayTypeQualificationStyle . NameAndContainingTypesAndNamespaces ,
27
+ genericsOptions : SymbolDisplayGenericsOptions . None ) ;
28
+
29
+ public static MemberKey ? FromMethodSymbol ( IMethodSymbol method , Compilation compilation )
25
30
{
26
31
string returnType ;
27
32
if ( method . ReturnsVoid )
@@ -44,94 +49,193 @@ public static MemberKey FromMethodSymbol(IMethodSymbol method, Compilation compi
44
49
}
45
50
}
46
51
47
- returnType = actualReturnType . TypeKind == TypeKind . TypeParameter
48
- ? "typeof(object)"
49
- : $ "typeof({ ReplaceGenericArguments ( actualReturnType . ToDisplayString ( _typeKeyFormat ) ) } )";
52
+ if ( actualReturnType . TypeKind == TypeKind . TypeParameter )
53
+ {
54
+ returnType = "typeof(object)" ;
55
+ }
56
+ else if ( TryGetFormattedTypeName ( actualReturnType , out var formattedReturnType ) )
57
+ {
58
+ returnType = $ "typeof({ formattedReturnType } )";
59
+ }
60
+ else
61
+ {
62
+ return null ;
63
+ }
50
64
}
51
65
52
66
// Handle extension methods by skipping the 'this' parameter
53
- var parameters = method . Parameters
54
- . Where ( p => ! p . IsThis )
55
- . Select ( p =>
67
+ List < string > parameters = [ ] ;
68
+ foreach ( var parameter in method . Parameters )
69
+ {
70
+ if ( parameter . IsThis )
71
+ {
72
+ continue ;
73
+ }
74
+
75
+ if ( parameter . Type . TypeKind == TypeKind . TypeParameter )
76
+ {
77
+ parameters . Add ( "typeof(object)" ) ;
78
+ }
79
+ else if ( parameter . IsParams && parameter . Type is IArrayTypeSymbol arrayType )
56
80
{
57
- if ( p . Type . TypeKind == TypeKind . TypeParameter )
81
+ if ( TryGetFormattedTypeName ( arrayType . ElementType , out var formattedArrayType ) )
58
82
{
59
- return "typeof(object)" ;
83
+ parameters . Add ( $ "typeof({ formattedArrayType } [])" ) ;
60
84
}
61
-
62
- // For params arrays, use the array type
63
- if ( p . IsParams && p . Type is IArrayTypeSymbol arrayType )
85
+ else
64
86
{
65
- return $ "typeof( { ReplaceGenericArguments ( arrayType . ToDisplayString ( _typeKeyFormat ) ) } )" ;
87
+ return null ;
66
88
}
89
+ }
90
+ else if ( TryGetFormattedTypeName ( parameter . Type , out var formattedParameterType ) )
91
+ {
92
+ parameters . Add ( $ "typeof({ formattedParameterType } )") ;
93
+ }
94
+ else
95
+ {
96
+ return null ;
97
+ }
98
+ }
67
99
68
- return $ "typeof({ ReplaceGenericArguments ( p . Type . ToDisplayString ( _typeKeyFormat ) ) } )";
69
- } )
70
- . ToArray ( ) ;
71
-
72
- // For generic methods, use the containing type with generic parameters
73
- var declaringType = method . ContainingType ;
74
- var typeDisplay = declaringType . ToDisplayString ( _typeKeyFormat ) ;
75
-
76
- // If the method is in a generic type, we need to handle the type parameters
77
- if ( declaringType . IsGenericType )
100
+ if ( TryGetFormattedTypeName ( method . ContainingType , out var formattedDeclaringType ) )
78
101
{
79
- typeDisplay = ReplaceGenericArguments ( typeDisplay ) ;
102
+ return new MemberKey (
103
+ $ "typeof({ formattedDeclaringType } )",
104
+ MemberType . Method ,
105
+ method . MetadataName , // Use MetadataName to match runtime MethodInfo.Name
106
+ returnType ,
107
+ parameters ) ;
80
108
}
81
-
82
- return new MemberKey (
83
- $ "typeof({ typeDisplay } )",
84
- MemberType . Method ,
85
- method . MetadataName , // Use MetadataName to match runtime MethodInfo.Name
86
- returnType ,
87
- parameters ) ;
109
+ return null ;
88
110
}
89
111
90
- public static MemberKey FromPropertySymbol ( IPropertySymbol property )
112
+ public static MemberKey ? FromPropertySymbol ( IPropertySymbol property )
91
113
{
92
- return new MemberKey (
93
- $ "typeof({ ReplaceGenericArguments ( property . ContainingType . ToDisplayString ( _typeKeyFormat ) ) } )",
94
- MemberType . Property ,
95
- property . Name ,
96
- null ,
97
- null ) ;
114
+ if ( TryGetFormattedTypeName ( property . ContainingType , out var typeName ) )
115
+ {
116
+ return new MemberKey (
117
+ $ "typeof({ typeName } )",
118
+ MemberType . Property ,
119
+ property . Name ,
120
+ null ,
121
+ null ) ;
122
+ }
123
+ return null ;
98
124
}
99
125
100
- public static MemberKey FromTypeSymbol ( INamedTypeSymbol type )
126
+ public static MemberKey ? FromTypeSymbol ( INamedTypeSymbol type )
101
127
{
102
- return new MemberKey (
103
- $ "typeof({ ReplaceGenericArguments ( type . ToDisplayString ( _typeKeyFormat ) ) } )",
104
- MemberType . Type ,
105
- null ,
106
- null ,
107
- null ) ;
128
+ if ( TryGetFormattedTypeName ( type , out var typeName ) )
129
+ {
130
+ return new MemberKey (
131
+ $ "typeof({ typeName } )",
132
+ MemberType . Type ,
133
+ null ,
134
+ null ,
135
+ null ) ;
136
+ }
137
+ return null ;
108
138
}
109
139
110
140
/// Supports replacing generic type arguments to support use of open
111
141
/// generics in `typeof` expressions for the declaring type.
112
- private static string ReplaceGenericArguments ( string typeName )
142
+ private static bool TryGetFormattedTypeName ( ITypeSymbol typeSymbol , [ NotNullWhen ( true ) ] out string ? typeName , bool isNestedCall = false )
113
143
{
114
- var stack = new Stack < int > ( ) ;
115
- var result = new StringBuilder ( typeName ) ;
116
- for ( var i = 0 ; i < result . Length ; i ++ )
144
+ if ( typeSymbol is INamedTypeSymbol { OriginalDefinition . SpecialType : SpecialType . System_Nullable_T } nullableType )
117
145
{
118
- if ( result [ i ] == '<' )
146
+ typeName = typeSymbol . ToDisplayString ( _withTypeParametersFormat ) ;
147
+ return true ;
148
+ }
149
+
150
+ // Handle tuples specially since they are represented as generic
151
+ // ValueTuple types and trigger the logic for handling generics in
152
+ // nested values.
153
+ if ( typeSymbol is INamedTypeSymbol { IsTupleType : true } namedType )
154
+ {
155
+ return TryHandleTupleType ( namedType , out typeName ) ;
156
+ }
157
+
158
+ if ( typeSymbol is INamedTypeSymbol { IsGenericType : true } genericType )
159
+ {
160
+ // If any of the type arguments are type parameters, then they have not
161
+ // been substituted for a concrete type and we need to model them as open
162
+ // generics if possible to avoid emitting a type with type parameters that
163
+ // cannot be used in a typeof expression.
164
+ var hasTypeParameters = genericType . TypeArguments . Any ( t => t . TypeKind == TypeKind . TypeParameter ) ;
165
+ var baseTypeName = genericType . ToDisplayString ( _sansTypeParametersFormat ) ;
166
+
167
+ if ( ! hasTypeParameters )
168
+ {
169
+ var typeArgStrings = new List < string > ( ) ;
170
+ var allArgumentsResolved = true ;
171
+
172
+ // Loop through each type argument to handle nested generics.
173
+ foreach ( var typeArg in genericType . TypeArguments )
174
+ {
175
+ if ( TryGetFormattedTypeName ( typeArg , out var argTypeName , isNestedCall : true ) )
176
+ {
177
+ typeArgStrings . Add ( argTypeName ) ;
178
+ }
179
+ else
180
+ {
181
+ typeName = null ;
182
+ return false ;
183
+ }
184
+ }
185
+
186
+ if ( allArgumentsResolved )
187
+ {
188
+ typeName = $ "{ baseTypeName } <{ string . Join ( ", " , typeArgStrings ) } >";
189
+ return true ;
190
+ }
191
+ }
192
+ else
119
193
{
120
- stack . Push ( i ) ;
194
+ if ( isNestedCall )
195
+ {
196
+ // If this is a nested call, we can't use open generics so there's no way
197
+ // for us to emit a member key. Return false and skip over this type in the code
198
+ // generation.
199
+ typeName = null ;
200
+ return false ;
201
+ }
202
+
203
+ // If we got here, we can successfully emit a member key for the open generic type.
204
+ var genericArgumentsCount = genericType . TypeArguments . Length ;
205
+ var openGenericsPlaceholder = "<" + new string ( ',' , genericArgumentsCount - 1 ) + ">" ;
206
+
207
+ typeName = baseTypeName + openGenericsPlaceholder ;
208
+ return true ;
121
209
}
122
- else if ( result [ i ] == '>' && stack . Count > 0 )
210
+ }
211
+
212
+ typeName = typeSymbol . ToDisplayString ( _withTypeParametersFormat ) ;
213
+ return true ;
214
+ }
215
+
216
+ private static bool TryHandleTupleType ( INamedTypeSymbol tupleType , [ NotNullWhen ( true ) ] out string ? typeName )
217
+ {
218
+ List < string > elementTypes = [ ] ;
219
+ foreach ( var element in tupleType . TupleElements )
220
+ {
221
+ if ( element . Type . TypeKind == TypeKind . TypeParameter )
123
222
{
124
- var start = stack . Pop ( ) ;
125
- // Replace everything between < and > with empty strings separated by commas
126
- var segment = result . ToString ( start + 1 , i - start - 1 ) ;
127
- var commaCount = segment . Count ( c => c == ',' ) ;
128
- var replacement = new string ( ',' , commaCount ) ;
129
- result . Remove ( start + 1 , i - start - 1 ) ;
130
- result . Insert ( start + 1 , replacement ) ;
131
- i = start + replacement . Length + 1 ;
223
+ elementTypes . Add ( "object" ) ;
224
+ }
225
+ else
226
+ {
227
+ // Process each tuple element and handle nested generics
228
+ if ( ! TryGetFormattedTypeName ( element . Type , out var elementTypeName , isNestedCall : true ) )
229
+ {
230
+ typeName = null ;
231
+ return false ;
232
+ }
233
+ elementTypes . Add ( elementTypeName ) ;
132
234
}
133
235
}
134
- return result . ToString ( ) ;
236
+
237
+ typeName = $ "global::System.ValueTuple<{ string . Join ( ", " , elementTypes ) } >";
238
+ return true ;
135
239
}
136
240
}
137
241
0 commit comments