Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
#if NETFRAMEWORK
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;
#endif

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;

Expand All @@ -24,7 +21,7 @@ internal sealed class ReflectionOperations : IReflectionOperations
[return: NotNullIfNotNull(nameof(memberInfo))]
public object[]? GetCustomAttributes(MemberInfo memberInfo)
#if NETFRAMEWORK
=> [.. ReflectionUtility.GetCustomAttributes(memberInfo)];
=> [.. GetCustomAttributesCore(memberInfo, type: null)];
#else
{
object[] attributes = memberInfo.GetCustomAttributes(typeof(Attribute), inherit: true);
Expand All @@ -47,7 +44,7 @@ internal sealed class ReflectionOperations : IReflectionOperations
[return: NotNullIfNotNull(nameof(memberInfo))]
public object[]? GetCustomAttributes(MemberInfo memberInfo, Type type) =>
#if NETFRAMEWORK
[.. ReflectionUtility.GetCustomAttributesCore(memberInfo, type)];
[.. GetCustomAttributesCore(memberInfo, type)];
#else
memberInfo.GetCustomAttributes(type, inherit: true);
#endif
Expand All @@ -60,7 +57,7 @@ internal sealed class ReflectionOperations : IReflectionOperations
/// <returns> The list of attributes of the given type on the member. Empty list if none found. </returns>
public object[] GetCustomAttributes(Assembly assembly, Type type) =>
#if NETFRAMEWORK
ReflectionUtility.GetCustomAttributes(assembly, type).ToArray();
GetCustomAttributesForAssembly(assembly, type).ToArray();
#else
assembly.GetCustomAttributes(type, inherit: true);
#endif
Expand Down Expand Up @@ -106,4 +103,254 @@ public MethodInfo[] GetRuntimeMethods(Type type)
#pragma warning restore IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming
#pragma warning restore IL2067 // 'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'.
#pragma warning restore IL2057 // Unrecognized value passed to the typeName parameter of 'System.Type.GetType(String)'

#if NETFRAMEWORK
/// <summary>
/// Get custom attributes on a member for both normal and reflection only load.
/// </summary>
/// <param name="memberInfo">Member for which attributes needs to be retrieved.</param>
/// <param name="type">Type of attribute to retrieve.</param>
/// <returns>All attributes of give type on member.</returns>
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
private static IReadOnlyList<object> GetCustomAttributesCore(MemberInfo memberInfo, Type? type)
#pragma warning restore CA1859
{
bool shouldGetAllAttributes = type is null;

if (!IsReflectionOnlyLoad(memberInfo))
{
return shouldGetAllAttributes ? memberInfo.GetCustomAttributes(inherit: true) : memberInfo.GetCustomAttributes(type, inherit: true);
}
else
{
List<object> nonUniqueAttributes = [];
Dictionary<string, object> uniqueAttributes = [];

int inheritanceThreshold = 10;
int inheritanceLevel = 0;

if (memberInfo.MemberType == MemberTypes.TypeInfo)
{
// This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeType type, RuntimeType caType, bool inherit)
var tempTypeInfo = memberInfo as TypeInfo;

do
{
IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(tempTypeInfo);
AddNewAttributes(
attributes,
shouldGetAllAttributes,
type!,
uniqueAttributes,
nonUniqueAttributes);
tempTypeInfo = tempTypeInfo!.BaseType?.GetTypeInfo();
inheritanceLevel++;
}
while (tempTypeInfo is not null && tempTypeInfo != typeof(object).GetTypeInfo()
&& inheritanceLevel < inheritanceThreshold);
}
else if (memberInfo.MemberType == MemberTypes.Method)
{
// This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeMethodInfo method, RuntimeType caType, bool inherit).
var tempMethodInfo = memberInfo as MethodInfo;

do
{
IList<CustomAttributeData> attributes = CustomAttributeData.GetCustomAttributes(tempMethodInfo);
AddNewAttributes(
attributes,
shouldGetAllAttributes,
type!,
uniqueAttributes,
nonUniqueAttributes);
MethodInfo? baseDefinition = tempMethodInfo!.GetBaseDefinition();

if (baseDefinition is not null
&& string.Equals(
string.Concat(tempMethodInfo.DeclaringType.FullName, tempMethodInfo.Name),
string.Concat(baseDefinition.DeclaringType.FullName, baseDefinition.Name), StringComparison.Ordinal))
{
break;
}

tempMethodInfo = baseDefinition;
inheritanceLevel++;
}
while (tempMethodInfo is not null && inheritanceLevel < inheritanceThreshold);
}
else
{
// Ideally we should not be reaching here. We only query for attributes on types/methods currently.
// Return the attributes that CustomAttributeData returns in this cases not considering inheritance.
IList<CustomAttributeData> firstLevelAttributes =
CustomAttributeData.GetCustomAttributes(memberInfo);
AddNewAttributes(firstLevelAttributes, shouldGetAllAttributes, type!, uniqueAttributes, nonUniqueAttributes);
}

nonUniqueAttributes.AddRange(uniqueAttributes.Values);
return nonUniqueAttributes;
}
}

private static List<Attribute> GetCustomAttributesForAssembly(Assembly assembly, Type type)
{
if (!assembly.ReflectionOnly)
{
return [.. assembly.GetCustomAttributes(type)];
}

List<CustomAttributeData> customAttributes = [.. CustomAttributeData.GetCustomAttributes(assembly)];

List<Attribute> attributesArray = [];

foreach (CustomAttributeData attribute in customAttributes)
{
if (!IsTypeInheriting(attribute.Constructor.DeclaringType, type)
&& !attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals(
type.AssemblyQualifiedName, StringComparison.Ordinal))
{
continue;
}

Attribute? attributeInstance = CreateAttributeInstance(attribute);
if (attributeInstance is not null)
{
attributesArray.Add(attributeInstance);
}
}

return attributesArray;
}

/// <summary>
/// Create instance of the attribute for reflection only load.
/// </summary>
/// <param name="attributeData">The attribute data.</param>
/// <returns>An attribute.</returns>
private static Attribute? CreateAttributeInstance(CustomAttributeData attributeData)
{
object? attribute = null;
try
{
// Create instance of attribute. For some case, constructor param is returned as ReadOnlyCollection
// instead of array. So convert it to array else constructor invoke will fail.
var attributeType = Type.GetType(attributeData.Constructor.DeclaringType.AssemblyQualifiedName);

List<Type> constructorParameters = [];
List<object> constructorArguments = [];
foreach (CustomAttributeTypedArgument parameter in attributeData.ConstructorArguments)
{
var parameterType = Type.GetType(parameter.ArgumentType.AssemblyQualifiedName);
constructorParameters.Add(parameterType);
if (!parameterType.IsArray
|| parameter.Value is not IEnumerable enumerable)
{
constructorArguments.Add(parameter.Value);
continue;
}

ArrayList list = [];
foreach (object? item in enumerable)
{
if (item is CustomAttributeTypedArgument argument)
{
list.Add(argument.Value);
}
else
{
list.Add(item);
}
}

constructorArguments.Add(list.ToArray(parameterType.GetElementType()));
}

ConstructorInfo constructor = attributeType.GetConstructor([.. constructorParameters]);
attribute = constructor.Invoke([.. constructorArguments]);

foreach (CustomAttributeNamedArgument namedArgument in attributeData.NamedArguments)
{
attributeType.GetProperty(namedArgument.MemberInfo.Name).SetValue(attribute, namedArgument.TypedValue.Value, null);
}
}

// If not able to create instance of attribute ignore attribute. (May happen for custom user defined attributes).
catch (BadImageFormatException)
{
}
catch (FileLoadException)
{
}
catch (TypeLoadException)
{
}

return attribute as Attribute;
}

private static void AddNewAttributes(
IList<CustomAttributeData> customAttributes,
bool shouldGetAllAttributes,
Type type,
Dictionary<string, object> uniqueAttributes,
List<object> nonUniqueAttributes)
{
foreach (CustomAttributeData attribute in customAttributes)
{
if (!shouldGetAllAttributes
&& !IsTypeInheriting(attribute.Constructor.DeclaringType, type)
&& !attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals(
type.AssemblyQualifiedName, StringComparison.Ordinal))
{
continue;
}

Attribute? attributeInstance = CreateAttributeInstance(attribute);
if (attributeInstance is null)
{
continue;
}

Type attributeType = attributeInstance.GetType();
IReadOnlyList<object> attributeUsageAttributes = GetCustomAttributesCore(
attributeType,
typeof(AttributeUsageAttribute));
if (attributeUsageAttributes.Count > 0
&& attributeUsageAttributes[0] is AttributeUsageAttribute { AllowMultiple: false })
{
if (!uniqueAttributes.ContainsKey(attributeType.FullName))
{
uniqueAttributes.Add(attributeType.FullName, attributeInstance);
}
}
else
{
nonUniqueAttributes.Add(attributeInstance);
}
}
}

/// <summary>
/// Check whether the member is loaded in a reflection only context.
/// </summary>
/// <param name="memberInfo"> The member Info. </param>
/// <returns> True if the member is loaded in a reflection only context. </returns>
private static bool IsReflectionOnlyLoad(MemberInfo? memberInfo)
=> memberInfo is not null && memberInfo.Module.Assembly.ReflectionOnly;

private static bool IsTypeInheriting(Type? type1, Type type2)
{
while (type1 is not null)
{
if (type1.AssemblyQualifiedName.Equals(type2.AssemblyQualifiedName, StringComparison.Ordinal))
{
return true;
}

type1 = type1.BaseType;
}

return false;
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if !WINDOWS_UWP
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
#endif
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
Expand Down Expand Up @@ -38,7 +39,7 @@ internal sealed class TestDeployment : ITestDeployment
/// Initializes a new instance of the <see cref="TestDeployment"/> class.
/// </summary>
public TestDeployment()
: this(new DeploymentItemUtility(new ReflectionUtility()), new DeploymentUtility(), new FileUtility())
: this(new DeploymentItemUtility(ReflectHelper.Instance), new DeploymentUtility(), new FileUtility())
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#if !WINDOWS_UWP

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -14,8 +15,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Uti
/// </summary>
internal sealed class DeploymentItemUtility
{
// REVIEW: it would be better if this was a ReflectionHelper, because helper is able to cache. But we don't have reflection helper here, because this is platform services dll.
private readonly ReflectionUtility _reflectionUtility;
private readonly ReflectHelper _reflectHelper;

/// <summary>
/// A cache for class level deployment items.
Expand All @@ -25,10 +25,10 @@ internal sealed class DeploymentItemUtility
/// <summary>
/// Initializes a new instance of the <see cref="DeploymentItemUtility"/> class.
/// </summary>
/// <param name="reflectionUtility"> The reflect helper. </param>
internal DeploymentItemUtility(ReflectionUtility reflectionUtility)
/// <param name="reflectHelper"> The reflect helper. </param>
internal DeploymentItemUtility(ReflectHelper reflectHelper)
{
_reflectionUtility = reflectionUtility;
_reflectHelper = reflectHelper;
_classLevelDeploymentItems = [];
}

Expand All @@ -42,9 +42,7 @@ internal IList<DeploymentItem> GetClassLevelDeploymentItems(Type type, ICollecti
{
if (!_classLevelDeploymentItems.TryGetValue(type, out IList<DeploymentItem>? value))
{
IReadOnlyList<object> deploymentItemAttributes = _reflectionUtility.GetCustomAttributes(
type,
typeof(DeploymentItemAttribute));
IEnumerable<DeploymentItemAttribute> deploymentItemAttributes = _reflectHelper.GetAttributes<DeploymentItemAttribute>(type);
value = GetDeploymentItems(deploymentItemAttributes, warnings);
_classLevelDeploymentItems[type] = value;
}
Expand All @@ -61,7 +59,8 @@ internal IList<DeploymentItem> GetClassLevelDeploymentItems(Type type, ICollecti
internal KeyValuePair<string, string>[]? GetDeploymentItems(MethodInfo method, IEnumerable<DeploymentItem> classLevelDeploymentItems,
ICollection<string> warnings)
{
List<DeploymentItem> testLevelDeploymentItems = GetDeploymentItems(_reflectionUtility.GetCustomAttributes(method, typeof(DeploymentItemAttribute)), warnings);
IEnumerable<DeploymentItemAttribute> deploymentItemAttributes = _reflectHelper.GetAttributes<DeploymentItemAttribute>(method);
List<DeploymentItem> testLevelDeploymentItems = GetDeploymentItems(deploymentItemAttributes, warnings);

return ToKeyValuePairs(Concat(testLevelDeploymentItems, classLevelDeploymentItems));
}
Expand Down Expand Up @@ -174,11 +173,11 @@ private static bool IsInvalidPath(string path)
return false;
}

private static List<DeploymentItem> GetDeploymentItems(IEnumerable deploymentItemAttributes, ICollection<string> warnings)
private static List<DeploymentItem> GetDeploymentItems(IEnumerable<DeploymentItemAttribute> deploymentItemAttributes, ICollection<string> warnings)
{
var deploymentItems = new List<DeploymentItem>();

foreach (DeploymentItemAttribute deploymentItemAttribute in deploymentItemAttributes.Cast<DeploymentItemAttribute>())
foreach (DeploymentItemAttribute deploymentItemAttribute in deploymentItemAttributes)
{
if (IsValidDeploymentItem(deploymentItemAttribute.Path, deploymentItemAttribute.OutputDirectory, out string? warning))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#if !WINDOWS_UWP

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions;

Expand All @@ -24,7 +25,7 @@ internal abstract class DeploymentUtilityBase
protected const string DeploymentFolderPrefix = "Deploy";

public DeploymentUtilityBase()
: this(new DeploymentItemUtility(new ReflectionUtility()), new AssemblyUtility(), new FileUtility())
: this(new DeploymentItemUtility(ReflectHelper.Instance), new AssemblyUtility(), new FileUtility())
{
}

Expand Down
Loading
Loading