-
Notifications
You must be signed in to change notification settings - Fork 759
/
TypeUtility.cs
489 lines (418 loc) · 18.6 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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
using System;
using System.Linq;
using System.Reflection;
using Xunit.Internal;
using Xunit.v3;
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)
{
Guard.ArgumentNotNull(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)
{
Guard.ArgumentNotNull(testMethod);
var parameters = testMethod.GetParameters();
var 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 (paramsElementType == null)
throw new InvalidOperationException("Cannot determine params element type");
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 (ArrayTypeMismatchException)
{
throw new InvalidOperationException($"The arguments for this test method did not match the parameters: {ArgumentFormatter.Format(arguments)}");
}
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
var 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.GetDefaultValue();
}
return newArguments;
}
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;
}
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;
}
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)
{
Guard.ArgumentNotNull(method);
Guard.ArgumentNotNull(baseDisplayName);
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>
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 (var 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>
static Type? GetArrayElementTypeOrThis(Type? type) =>
type?.IsArray == true ? type.GetElementType() : type;
/// <summary>
/// Gets the underlying ElementType of a type, if the <see cref="_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>
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>
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?.BaseType;
if (baseType != null && baseType.IsGenericType)
{
var baseGenericTypeArguments = baseType.GetGenericArguments();
for (var 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 (var 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)
{
Guard.ArgumentNotNull(genericType);
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) && matchedType != null)
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)
{
Guard.ArgumentNotNull(method);
Guard.ArgumentNotNull(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;
}
/// <summary>
/// Returns the default value for the given type. For value types, this means a 0-initialized
/// instance of the type; for reference types, this means <c>null</c>.
/// </summary>
/// <param name="type">The type to get the default value of.</param>
/// <returns>The default value for the given type.</returns>
public static object? GetDefaultValue(this Type type)
{
if (type.IsValueType)
return Activator.CreateInstance(type);
return null;
}
}
}