Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bce4b9c
Unify reflection helpers
Evangelink Feb 4, 2026
3a29b63
Simplify further
Evangelink Feb 6, 2026
ed70197
Apply Youssef changes
Evangelink Feb 6, 2026
2ca249e
More updates
Evangelink Feb 6, 2026
c13ed7c
Merge branch 'main' into dev/amauryleve/reflection
Evangelink Feb 6, 2026
c437564
Massive refactoring
Evangelink Feb 6, 2026
5568ca8
Fix glitches
Evangelink Feb 6, 2026
23e4303
Fixes
Evangelink Feb 7, 2026
875ae8d
Even more
Evangelink Feb 7, 2026
68b754d
Again
Evangelink Feb 7, 2026
6ff64bc
Merge remote-tracking branch 'origin/main' into dev/amauryleve/reflec…
Evangelink Feb 9, 2026
fb58d01
Merge branch 'main' into dev/amauryleve/reflection
Evangelink Feb 9, 2026
d57fedf
more
Evangelink Feb 9, 2026
19c1646
Merge remote-tracking branch 'origin/main' into dev/amauryleve/reflec…
Copilot Apr 17, 2026
f6aa41d
Fix reflection helper type mismatches causing CI build failures
Copilot Apr 21, 2026
52b4576
Merge branch 'main' into dev/amauryleve/reflection
Evangelink May 11, 2026
0253483
Fix build errors
Evangelink May 11, 2026
fae0803
Address review comments
Evangelink May 11, 2026
91b4074
Merge origin/main into dev/amauryleve/reflection
Copilot May 11, 2026
6430ccd
Fix CS8625: cast null to IRunSettings? in Mock constructor calls
Evangelink May 11, 2026
dd62008
Merge branch 'main' into dev/amauryleve/reflection
Evangelink May 12, 2026
26610d1
Address PR review comments: fix hot path allocations, remove dead moc…
Evangelink May 12, 2026
99122ee
Address second round of PR review comments: guard unsupported types i…
Evangelink May 12, 2026
16a1b05
Address third round of PR review comments: widen IsAttributeDefined t…
Evangelink May 12, 2026
6d85d11
Address review comments: rename, tests, perf fixes, cache unification
Evangelink May 12, 2026
7563e05
Address review round 2: DI bypass, null guards, test coverage
Evangelink May 12, 2026
85b17e0
Align MockableReflectionOperations exception type with production
Evangelink May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)];
=> [.. GetCustomAttributesImpl(memberInfo)];
#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)];
[.. GetCustomAttributesCoreImpl(memberInfo, type)];
#else
memberInfo.GetCustomAttributes(type, inherit: true);
#endif
Expand All @@ -60,11 +57,267 @@ 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();
GetCustomAttributesFromAssembly(assembly, type).ToArray();
#else
assembly.GetCustomAttributes(type, inherit: true);
#endif

#if NETFRAMEWORK
/// <summary>
/// Gets all the custom attributes adorned on a member.
/// </summary>
/// <param name="memberInfo"> The member. </param>
/// <returns> The list of attributes on the member. Empty list if none found. </returns>
private static IReadOnlyList<object> GetCustomAttributesImpl(MemberInfo memberInfo)
Comment thread
Evangelink marked this conversation as resolved.
Outdated
=> GetCustomAttributesCoreImpl(memberInfo, type: null);

/// <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> GetCustomAttributesCoreImpl(MemberInfo memberInfo, Type? type)
#pragma warning restore CA1859
{
bool shouldGetAllAttributes = type == null;

if (!IsReflectionOnlyLoad(memberInfo))
{
return shouldGetAllAttributes ? memberInfo.GetCustomAttributes(inherit: true) : memberInfo.GetCustomAttributes(type, inherit: true);
}

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 != 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 != 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 != 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> GetCustomAttributesFromAssembly(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 != 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)
{
}
Comment thread
Evangelink marked this conversation as resolved.

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 == null)
{
continue;
}

Type attributeType = attributeInstance.GetType();
IReadOnlyList<object> attributeUsageAttributes = GetCustomAttributesCoreImpl(
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 != null && memberInfo.Module.Assembly.ReflectionOnly;

private static bool IsTypeInheriting(Type? type1, Type type2)
{
while (type1 != null)
{
if (type1.AssemblyQualifiedName.Equals(type2.AssemblyQualifiedName, StringComparison.Ordinal))
{
Comment thread
Evangelink marked this conversation as resolved.
return true;
}

type1 = type1.BaseType;
}

return false;
}
#endif

#pragma warning disable IL2070 // this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'.
#pragma warning disable IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming
#pragma warning disable IL2067 // 'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if !WINDOWS_UWP && !WIN_UI

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;
Expand Down Expand Up @@ -34,7 +35,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 && !WIN_UI

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,7 @@ 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);
List<DeploymentItem> testLevelDeploymentItems = GetDeploymentItems(_reflectHelper.GetAttributes<DeploymentItemAttribute>(method), warnings);

return ToKeyValuePairs(Concat(testLevelDeploymentItems, classLevelDeploymentItems));
}
Expand Down Expand Up @@ -174,11 +172,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 @@ -4,6 +4,7 @@
#if !WINDOWS_UWP && !WIN_UI

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

Expand All @@ -25,7 +26,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