-
Notifications
You must be signed in to change notification settings - Fork 759
/
TypeUtility.cs
434 lines (368 loc) · 21.1 KB
/
TypeUtility.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
using System;
using System.Linq;
using System.Reflection;
using Xunit.Abstractions;
namespace Xunit.Sdk
{
/// <summary>
/// Extension methods for <see cref="ITypeInfo"/>.
/// </summary>
public static class TypeUtility
{
readonly static ITypeInfo ObjectTypeInfo = Reflector.Wrap(typeof(object));
/// <summary>
/// Converts a type into a name string.
/// </summary>
/// <param name="type">The type to convert.</param>
/// <returns>Name string of type.</returns>
public static string ConvertToSimpleTypeName(ITypeInfo type)
{
var baseTypeName = type.Name;
var backTickIdx = baseTypeName.IndexOf('`');
if (backTickIdx >= 0)
baseTypeName = baseTypeName.Substring(0, backTickIdx);
var lastIndex = baseTypeName.LastIndexOf('.');
if (lastIndex >= 0)
baseTypeName = baseTypeName.Substring(lastIndex + 1);
if (!type.IsGenericType)
return baseTypeName;
var genericTypes = type.GetGenericArguments().ToArray();
var simpleNames = new string[genericTypes.Length];
for (var idx = 0; idx < genericTypes.Length; idx++)
simpleNames[idx] = ConvertToSimpleTypeName(genericTypes[idx]);
return $"{baseTypeName}<{string.Join(", ", simpleNames)}>";
}
/// <summary>
/// Resolves argument values for the test method, including support for optional method
/// arguments.
/// </summary>
/// <param name="testMethod">The test method to resolve.</param>
/// <param name="arguments">The user-supplied method arguments.</param>
/// <returns>The argument values</returns>
public static object[] ResolveMethodArguments(this MethodBase testMethod, object[] arguments)
{
var parameters = testMethod.GetParameters();
bool hasParamsParameter = false;
// Params can only be added at the end of the parameter list
if (parameters.Length > 0)
hasParamsParameter = parameters[parameters.Length - 1].GetCustomAttribute(typeof(ParamArrayAttribute)) != null;
var nonOptionalParameterCount = parameters.Count(p => !p.IsOptional);
if (hasParamsParameter)
nonOptionalParameterCount--;
// We can't call a method if we provided fewer parameters than the number of non-optional parameters in the method.
if (arguments.Length < nonOptionalParameterCount)
return arguments;
// We can't call a non-params method if we have provided more parameters than the total number of parameters in the method.
if (!hasParamsParameter && arguments.Length > parameters.Length)
return arguments;
var newArguments = new object[parameters.Length];
var resolvedArgumentsCount = 0;
if (hasParamsParameter)
{
var paramsParameter = parameters[parameters.Length - 1];
var paramsElementType = paramsParameter.ParameterType.GetElementType();
if (arguments.Length < parameters.Length)
{
// Didn't include the params parameter
var emptyParamsArray = Array.CreateInstance(paramsElementType, 0);
newArguments[newArguments.Length - 1] = emptyParamsArray;
}
else if (arguments.Length == parameters.Length &&
(arguments[arguments.Length - 1] == null ||
(arguments[arguments.Length - 1].GetType().IsArray &&
arguments[arguments.Length - 1].GetType().GetElementType() == paramsElementType)))
{
// Passing null or the same type array as the params parameter
newArguments[newArguments.Length - 1] = arguments[arguments.Length - 1];
resolvedArgumentsCount = 1;
}
else
{
// Parameters need adjusting into an array
var paramsArrayLength = arguments.Length - parameters.Length + 1;
var paramsArray = Array.CreateInstance(paramsElementType, paramsArrayLength);
try
{
Array.Copy(arguments, parameters.Length - 1, paramsArray, 0, paramsArray.Length);
}
catch (InvalidCastException)
{
throw new InvalidOperationException($"The arguments for this test method did not match the parameters: {ArgumentFormatter.Format(arguments)}");
}
newArguments[newArguments.Length - 1] = paramsArray;
resolvedArgumentsCount = paramsArrayLength;
}
}
// If the argument has been provided, pass the argument value
for (var i = 0; i < arguments.Length - resolvedArgumentsCount; i++)
newArguments[i] = TryConvertObject(arguments[i], parameters[i].ParameterType);
// If the argument has not been provided, pass the default value
int unresolvedParametersCount = hasParamsParameter ? parameters.Length - 1 : parameters.Length;
for (var i = arguments.Length; i < unresolvedParametersCount; i++)
{
var parameter = parameters[i];
if (parameter.HasDefaultValue)
newArguments[i] = parameter.DefaultValue;
else
newArguments[i] = parameter.ParameterType.GetTypeInfo().GetDefaultValue();
}
return newArguments;
}
private static object TryConvertObject(object argumentValue, Type parameterType)
{
if (argumentValue == null)
return null;
// No need to perform conversion
if (parameterType.IsAssignableFrom(argumentValue.GetType()))
return argumentValue;
return PerformDefinedConversions(argumentValue, parameterType) ?? argumentValue;
}
private static object PerformDefinedConversions(object argumentValue,
Type parameterType)
{
// argumentValue is known to not be null when we're called from TryConvertObject
var argumentValueType = argumentValue.GetType();
var methodArguments = new object[] { argumentValue };
bool isMatchingOperator(MethodInfo m, string name) =>
m.Name.Equals(name) &&
m.IsSpecialName && // Filter out non-operator methods that might bear this reserved name
m.IsStatic &&
!IsByRefLikeType(m.ReturnType) &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == argumentValueType &&
parameterType.IsAssignableFrom(m.ReturnType);
// Implicit & explicit conversions to/from a type can be declared on either side of the relationship.
// We need to check both possibilities.
foreach (var conversionDeclaringType in new[] { parameterType, argumentValueType })
{
var runtimeMethods = conversionDeclaringType.GetRuntimeMethods();
var implicitMethod = runtimeMethods.FirstOrDefault(m => isMatchingOperator(m, "op_Implicit"));
if (implicitMethod != null)
return implicitMethod.Invoke(null, methodArguments);
var explicitMethod = runtimeMethods.FirstOrDefault(m => isMatchingOperator(m, "op_Explicit"));
if (explicitMethod != null)
return explicitMethod.Invoke(null, methodArguments);
}
return null;
}
private static bool IsByRefLikeType(Type type)
{
var val = type.GetType().GetRuntimeProperty("IsByRefLike")?.GetValue(type);
if (val is bool isByRefLike)
return isByRefLike;
// The type can't be a byreflike type if the property doesn't exist.
return false;
}
/// <summary>
/// Formulates the extended portion of the display name for a test method. For tests with no arguments, this will
/// return just the base name; for tests with arguments, attempts to format the arguments and appends the argument
/// list to the test name.
/// </summary>
/// <param name="method">The test method</param>
/// <param name="baseDisplayName">The base part of the display name</param>
/// <param name="arguments">The test method arguments</param>
/// <param name="genericTypes">The test method's generic types</param>
/// <returns>The full display name for the test method</returns>
public static string GetDisplayNameWithArguments(this IMethodInfo method, string baseDisplayName, object[] arguments, ITypeInfo[] genericTypes)
{
baseDisplayName += ResolveGenericDisplay(genericTypes);
if (arguments == null)
return baseDisplayName;
var parameterInfos = method.GetParameters().CastOrToArray();
var displayValues = new string[Math.Max(arguments.Length, parameterInfos.Length)];
int idx;
for (idx = 0; idx < arguments.Length; idx++)
displayValues[idx] = ParameterToDisplayValue(GetParameterName(parameterInfos, idx), arguments[idx]);
for (; idx < parameterInfos.Length; idx++)
{
var reflectionParameterInfo = parameterInfos[idx] as IReflectionParameterInfo;
var parameterName = GetParameterName(parameterInfos, idx);
if (reflectionParameterInfo?.ParameterInfo.IsOptional ?? false)
displayValues[idx] = ParameterToDisplayValue(parameterName, reflectionParameterInfo.ParameterInfo.DefaultValue);
else
displayValues[idx] = parameterName + ": ???";
}
return $"{baseDisplayName}({string.Join(", ", displayValues)})";
}
static string GetParameterName(IParameterInfo[] parameters, int index)
{
if (index >= parameters.Length)
return "???";
return parameters[index].Name;
}
static string ParameterToDisplayValue(string parameterName, object parameterValue)
=> $"{parameterName}: {ArgumentFormatter.Format(parameterValue)}";
static string ResolveGenericDisplay(ITypeInfo[] genericTypes)
{
if (genericTypes == null || genericTypes.Length == 0)
return string.Empty;
var typeNames = new string[genericTypes.Length];
for (var idx = 0; idx < genericTypes.Length; idx++)
typeNames[idx] = ConvertToSimpleTypeName(genericTypes[idx]);
return $"<{string.Join(", ", typeNames)}>";
}
/// <summary>
/// Resolves an individual generic type given an intended generic parameter type and the type of an object passed to that type.
/// </summary>
/// <param name="genericType">The generic type, e.g. T, to resolve.</param>
/// <param name="methodParameterType">The non-generic or open generic type, e.g. T, to try to match with the type of the object passed to that type.</param>
/// <param name="passedParameterType">The non-generic or closed generic type, e.g. string, used to resolve the method parameter.</param>
/// <param name="resultType">The resolved type, e.g. the parameters (T, T, string, typeof(object)) -> (T, T, string, typeof(string)).</param>
/// <returns>True if resolving was successful, else false.</returns>
private static bool ResolveGenericParameter(this ITypeInfo genericType, ITypeInfo methodParameterType, Type passedParameterType, ref Type resultType)
{
// Is a parameter a generic array, e.g. T[] or List<T>[]
var isGenericArray = false;
var strippedMethodParameterType = StripElementType(methodParameterType, ref isGenericArray);
if (isGenericArray)
passedParameterType = GetArrayElementTypeOrThis(passedParameterType);
// Is the parameter generic type (e.g. List<T>, Dictionary<T, U>, List<T>[], List<string>)
if (strippedMethodParameterType.IsGenericType)
{
// We recursively drill down both the method parameter and the passed parameter
// to get and resolve inner generic arguments
// E.g. (List<T>, List<string>) -> (T, string)
// E.g. (List<List<T>, List<List<string>>) -> (List<T>, List<string>) -> (T, string)
var methodParameterGenericArguments = strippedMethodParameterType.GetGenericArguments().CastOrToArray();
var passedParameterGenericArguments = passedParameterType?.GetGenericArguments();
// We can't pass List<T> to Dictionary<T, U>
// But we can pass Class : Interface<T> to Interface<T>
if (methodParameterGenericArguments.Length != passedParameterGenericArguments?.Length)
{
if (genericType.ResolveMismatchedGenericArguments(passedParameterType, methodParameterGenericArguments, ref resultType))
return true;
}
else
{
for (int i = 0; i < methodParameterGenericArguments.Length; i++)
{
// Drill down through the generic arguments of the parameter provided,
// and find one matching the genericType
// This allows us to resolve complex generics with any number of parameters
// and at any level deep
// e.g. Dictionary<T, U>, Dictionary<string, U>, Dictionary<T, int> etc.
// e.g. Dictionary<Dictionary<T, U>, List<Dictionary<V, W>>
var methodGenericArgument = methodParameterGenericArguments[i];
var passedTypeArgument = passedParameterGenericArguments[i];
if (genericType.ResolveGenericParameter(methodGenericArgument, passedTypeArgument, ref resultType))
return true;
}
}
}
// Now finally check if we found a matching type (e.g. T -> T, List<T> -> List<T> etc.)
if (strippedMethodParameterType.IsGenericParameter && strippedMethodParameterType.Name == genericType.Name)
{
if (resultType == null)
resultType = passedParameterType;
else if (resultType.Name != passedParameterType?.FullName)
resultType = null;
}
return resultType != null;
}
/// <summary>
/// Gets the ElementType of a type, only if it is an array.
/// </summary>
/// <param name="type">The type to get the ElementType of.</param>
/// <returns>If type is an array, the ElementType of the type, else the original type.</returns>
private static Type GetArrayElementTypeOrThis(Type type)
=> type?.IsArray == true ? type.GetElementType() : type;
/// <summary>
/// Gets the underlying ElementType of a type, if the ITypeInfo supports reflection.
/// </summary>
/// <param name="type">The type to get the ElementType of.</param>
/// <param name="isArray">A flag indicating whether the type is an array.</param>
/// <returns>If type has an element type, underlying ElementType of a type, else the original type.</returns>
private static ITypeInfo StripElementType(ITypeInfo type, ref bool isArray)
{
if (type is IReflectionTypeInfo parameterReflectionType && parameterReflectionType.Type.HasElementType)
{
// We have a T[] or T&
isArray = parameterReflectionType.Type.IsArray;
return Reflector.Wrap(parameterReflectionType.Type.GetElementType());
}
return type;
}
/// <summary>
/// Resolves an individual generic type given an intended generic parameter type and the type of an object passed to that type.
/// </summary>
/// <param name="genericType">The generic type, e.g. T, to resolve.</param>
/// <param name="passedParameterType">The non-generic or closed generic type, e.g. string, used to resolve the method parameter.</param>
/// <param name="methodGenericTypeArguments">The generic arguments of the open generic type to match with the passed parameter.</param>
/// <param name="resultType">The resolved type.</param>
/// <returns>True if resolving was successful, else false.</returns>
private static bool ResolveMismatchedGenericArguments(this ITypeInfo genericType, Type passedParameterType, ITypeInfo[] methodGenericTypeArguments, ref Type resultType)
{
// Do we have Class : BaseClass<T>, Class: BaseClass<T, U> etc.
var baseType = passedParameterType?.GetTypeInfo()?.BaseType;
if (baseType != null && baseType.IsGenericType())
{
var baseGenericTypeArguments = baseType.GetGenericArguments();
for (int i = 0; i < baseGenericTypeArguments.Length; i++)
{
var methodGenericTypeArgument = methodGenericTypeArguments[i];
var baseGenericTypeArgument = baseGenericTypeArguments[i];
if (genericType.ResolveGenericParameter(methodGenericTypeArgument, baseGenericTypeArgument, ref resultType))
return true;
}
}
// Do we have Class : Interface<T>, Class : Interface<T, U> etc.
if (passedParameterType != null)
foreach (var interfaceType in passedParameterType.GetInterfaces().Where(i => i.IsGenericType()))
{
var interfaceGenericArguments = interfaceType.GetGenericArguments();
for (int i = 0; i < interfaceGenericArguments.Length; i++)
{
var methodGenericTypeArgument = methodGenericTypeArguments[i];
var baseGenericTypeArgument = interfaceGenericArguments[i];
if (genericType.ResolveGenericParameter(methodGenericTypeArgument, baseGenericTypeArgument, ref resultType))
return true;
}
}
return false;
}
/// <summary>
/// Resolves a generic type for a test method. The test parameters (and associated parameter infos) are
/// used to determine the best matching generic type for the test method that can be satisfied by all
/// the generic parameters and their values.
/// </summary>
/// <param name="genericType">The generic type to be resolved</param>
/// <param name="parameters">The parameter values being passed to the test method</param>
/// <param name="parameterInfos">The parameter infos for the test method</param>
/// <returns>The best matching generic type</returns>
public static ITypeInfo ResolveGenericType(this ITypeInfo genericType, object[] parameters, IParameterInfo[] parameterInfos)
{
for (var idx = 0; idx < parameterInfos.Length; ++idx)
{
var methodParameterType = parameterInfos[idx].ParameterType;
var passedParameterType = parameters[idx]?.GetType();
Type matchedType = null;
if (ResolveGenericParameter(genericType, methodParameterType, passedParameterType, ref matchedType))
return Reflector.Wrap(matchedType);
}
return ObjectTypeInfo;
}
/// <summary>
/// Resolves all the generic types for a test method. The test parameters are used to determine
/// the best matching generic types for the test method that can be satisfied by all
/// the generic parameters and their values.
/// </summary>
/// <param name="method">The test method</param>
/// <param name="parameters">The parameter values being passed to the test method</param>
/// <returns>The best matching generic types</returns>
public static ITypeInfo[] ResolveGenericTypes(this IMethodInfo method, object[] parameters)
{
var genericTypes = method.GetGenericArguments().ToArray();
var resolvedTypes = new ITypeInfo[genericTypes.Length];
var parameterInfos = method.GetParameters().CastOrToArray();
for (var idx = 0; idx < genericTypes.Length; ++idx)
resolvedTypes[idx] = ResolveGenericType(genericTypes[idx], parameters, parameterInfos);
return resolvedTypes;
}
internal static object GetDefaultValue(this TypeInfo typeInfo)
{
if (typeInfo.IsValueType)
return Activator.CreateInstance(typeInfo.AsType());
return null;
}
}
}