From 43acd8114cdef61c41fbcade02d3c80607e4d28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 19:27:33 +0200 Subject: [PATCH 1/3] Convert AOT/trim pragma suppressions to attributes so they propagate to consumers #pragma warning disable IL2xxx/IL3xxx only silences C# compile-time warnings; it does not affect ILC analyzer warnings at downstream consumers' publish time. Convert MSTest's existing pragma suppressions into attribute-based suppressions (UnconditionalSuppressMessage / RequiresUnreferencedCode / RequiresDynamicCode) that survive into IL and are honored by ILC. This is motivated by #8586 which switches the NativeAOT integration test to reference MSTest.TestAdapter, surfacing all of MSTest's previously-pragma'd warnings as errors. This change addresses ~31 of those warnings independently of #8586. Out of scope (future work): warnings from vstest's Microsoft.TestPlatform.ObjectModel (submodule), the MSTestSourceGeneratedReflectionMetadata.g.cs IL2070 (belongs in #8586's generator emitter), and transitive DataContract warnings inside System.Private.DataContractSerialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AssemblyResolver.cs | 4 +- .../Extensions/MethodInfoExtensions.cs | 2 + .../Helpers/DataSerializationHelper.cs | 55 +++++++------------ .../Helpers/ManagedNameHelper.cs | 2 + .../Services/ReflectionOperations.cs | 19 ++++--- .../Services/TestSourceHost.cs | 3 +- .../TestMethodFilter.cs | 1 + .../Utilities/DeploymentUtilityBase.cs | 3 +- ...ngleSessionVSTestAndTestAnywhereAdapter.cs | 7 +-- .../Internal/ReflectionTestMethodInfo.cs | 6 ++ 10 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs b/src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs index 345e8a2a27..7f92518fde 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/AssemblyResolver.cs @@ -307,15 +307,13 @@ private static /// /// The path of the assembly. /// The loaded . + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = "AssemblyResolver is part of the legacy reflection-mode loader and is not used in source-generator / Native AOT execution mode.")] #if NETFRAMEWORK protected virtual #else private static #endif - // This whole class is not used in source generator mode. -#pragma warning disable IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming Assembly LoadAssemblyFrom(string path) => Assembly.LoadFrom(path); -#pragma warning restore IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming #if NETFRAMEWORK /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/MethodInfoExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Extensions/MethodInfoExtensions.cs index 99ebd885cc..aba95115cf 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/MethodInfoExtensions.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Extensions/MethodInfoExtensions.cs @@ -240,6 +240,8 @@ private static void InferGenerics(Type parameterType, Type argumentType, List<(T // // [DataRow(0, "Hello")] // public void TestMethod(T2 p0, T1, p1) { } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed.", Justification = "Generic test methods with substituted type arguments are part of MSTest's reflection-mode adapter. Native AOT support relies on MSTest source-generated metadata, not on this code path.")] + [UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = "Generic test methods with substituted type arguments are part of MSTest's reflection-mode adapter. Native AOT support relies on MSTest source-generated metadata, not on this code path.")] private static MethodInfo ConstructGenericMethod(MethodInfo methodInfo, object?[]? arguments) { DebugEx.Assert(methodInfo.IsGenericMethod, "ConstructGenericMethod should only be called for a generic method."); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs index c7c980e9fd..b848f88591 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/DataSerializationHelper.cs @@ -12,6 +12,11 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; internal static class DataSerializationHelper { + private const string DataContractSerializationJustification = + "Data contract serialization is used for cross-process VSTest payloads. " + + "This should be safe as long as our generator mentions getting fields / properties of the target type. " + + "https://github.com/dotnet/runtime/issues/71350#issuecomment-1168140551"; + private static readonly ConcurrentDictionary SerializerCache = new(); private static readonly DataContractJsonSerializerSettings SerializerSettings = new() { @@ -30,6 +35,8 @@ internal static class DataSerializationHelper /// /// Data array to serialize. /// Serialized array. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)] + [UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)] public static string?[]? Serialize(object?[]? data) { if (data == null) @@ -61,14 +68,7 @@ internal static class DataSerializationHelper #endif using var memoryStream = new MemoryStream(); - // This should be safe as long as our generator mentions - // getting fields / properties of the target type. https://github.com/dotnet/runtime/issues/71350#issuecomment-1168140551 - // Not the best solution, maybe we can replace this with System.Text.Json, but the we need one generator calling the other. -#pragma warning disable IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning disable IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming serializer.WriteObject(memoryStream, data[i]); -#pragma warning restore IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning restore IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming byte[] serializerData = memoryStream.ToArray(); serializedData[dataIndex] = Encoding.UTF8.GetString(serializerData, 0, serializerData.Length); @@ -82,6 +82,8 @@ internal static class DataSerializationHelper /// /// Serialized data array to deserialize. /// Deserialized array. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)] + [UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)] public static object?[]? Deserialize(string?[]? serializedData) { if (serializedData == null || serializedData.Length % 2 != 0) @@ -111,14 +113,7 @@ internal static class DataSerializationHelper byte[] serializedDataBytes = Encoding.UTF8.GetBytes(serializedValue); using var memoryStream = new MemoryStream(serializedDataBytes); - // This should be safe as long as our generator mentions - // getting fields / properties of the target type. https://github.com/dotnet/runtime/issues/71350#issuecomment-1168140551 - // Not the best solution, maybe we can replace this with System.Text.Json, but the we need one generator calling the other. -#pragma warning disable IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning disable IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming data[i] = serializer.ReadObject(memoryStream); -#pragma warning restore IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning restore IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming // For some reason, we don't get SerializationSurrogateProvider.GetDeserializedObject to be called by .NET runtime. // So we manually call it. data[i] = SerializationSurrogateProvider.GetDeserializedObject(data[i]!); @@ -128,26 +123,20 @@ internal static class DataSerializationHelper } private static DataContractJsonSerializer GetSerializer(string assemblyQualifiedName) - => SerializerCache.GetOrAdd( - assemblyQualifiedName, - // This should be safe as long as our generator mentions - // getting fields / properties of the target type. https://github.com/dotnet/runtime/issues/71350#issuecomment-1168140551 - // Not the best solution, maybe we can replace this with System.Text.Json, but the we need one generator calling the other. -#pragma warning disable IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning disable IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming - _ => new DataContractJsonSerializer(PlatformServiceProvider.Instance.ReflectionOperations.GetType(assemblyQualifiedName) ?? typeof(object), SerializerSettings)); -#pragma warning restore IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning restore IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming + => SerializerCache.GetOrAdd(assemblyQualifiedName, CreateSerializerForAssemblyQualifiedName); private static DataContractJsonSerializer GetSerializer(Type type) - => SerializerCache.GetOrAdd( - type.AssemblyQualifiedName!, - // This should be safe as long as our generator mentions - // getting fields / properties of the target type. https://github.com/dotnet/runtime/issues/71350#issuecomment-1168140551 - // Not the best solution, maybe we can replace this with System.Text.Json, but the we need one generator calling the other. -#pragma warning disable IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning disable IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming - _ => new DataContractJsonSerializer(type, SerializerSettings)); + => SerializerCache.GetOrAdd(type.AssemblyQualifiedName!, _ => CreateSerializerForType(type)); + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)] + [UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)] + private static DataContractJsonSerializer CreateSerializerForAssemblyQualifiedName(string assemblyQualifiedName) + => new(PlatformServiceProvider.Instance.ReflectionOperations.GetType(assemblyQualifiedName) ?? typeof(object), SerializerSettings); + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = DataContractSerializationJustification)] + [UnconditionalSuppressMessage("Aot", "IL3050:Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT", Justification = DataContractSerializationJustification)] + private static DataContractJsonSerializer CreateSerializerForType(Type type) + => new(type, SerializerSettings); [DataContract] private sealed class SurrogatedDateOnly @@ -235,6 +224,4 @@ public Type GetSurrogateType(Type type) return type; } } -#pragma warning restore IL3050 // IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as Native AOT -#pragma warning restore IL2026 // IL2026: Members attributed with RequiresUnreferencedCode may break when trimming } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ManagedNameHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ManagedNameHelper.cs index e60eb80e27..8c871c4c7c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ManagedNameHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ManagedNameHelper.cs @@ -112,6 +112,7 @@ public static void GetManagedNameAndHierarchy(MethodBase method, out string mana /// More information about and can be found in /// the RFC. /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = "Reflection-based name resolution is used by the legacy VSTest bridge for managed-name lookup. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public static MethodInfo GetMethod(Assembly assembly, string managedTypeName, string managedMethodName) { Type type = assembly.GetType(managedTypeName, throwOnError: false, ignoreCase: false) @@ -128,6 +129,7 @@ public static MethodInfo GetMethod(Assembly assembly, string managedTypeName, st return method ?? throw new InvalidManagedNameException(); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based name resolution is used by the legacy VSTest bridge for managed-name lookup. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] private static MethodInfo? FindMethod(Type type, string methodName, int methodArity, string[]? parameterTypes) { bool Filter(MemberInfo mbr, object? param) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs index 4cb8ce6729..c9745dd68f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs @@ -48,47 +48,50 @@ internal sealed class ReflectionOperations : MarshalByRefObject, IReflectionOper public object[] GetCustomAttributes(Assembly assembly, Type type) => assembly.GetCustomAttributes(type, inherit: true); -#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'. -#pragma warning disable IL2057 // Unrecognized value passed to the typeName parameter of 'System.Type.GetType(String)' + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public ConstructorInfo[] GetDeclaredConstructors(Type classType) => classType.GetConstructors(DeclaredOnlyLookup); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public MethodInfo[] GetDeclaredMethods(Type classType) => classType.GetMethods(DeclaredOnlyLookup); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public PropertyInfo[] GetDeclaredProperties(Type type) => type.GetProperties(DeclaredOnlyLookup); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public Type[] GetDefinedTypes(Assembly assembly) => assembly.GetTypes(); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public MethodInfo[] GetRuntimeMethods(Type type) => type.GetMethods(Everything); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public MethodInfo? GetRuntimeMethod(Type declaringType, string methodName, Type[] parameters, bool includeNonPublic) => includeNonPublic ? declaringType.GetMethod(methodName, Everything, null, parameters, null) : declaringType.GetMethod(methodName, parameters); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public PropertyInfo? GetRuntimeProperty(Type classType, string testContextPropertyName, bool includeNonPublic) => includeNonPublic ? classType.GetProperty(testContextPropertyName, Everything) : classType.GetProperty(testContextPropertyName); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2057:Unrecognized value passed to the typeName parameter of 'System.Type.GetType(String)'.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public Type? GetType(string typeName) => Type.GetType(typeName); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:Members attributed with RequiresUnreferencedCode may break when trimming", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public Type? GetType(Assembly assembly, string typeName) => assembly.GetType(typeName); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "Reflection-based discovery/execution is the MSTest reflection-mode adapter path. Native AOT support relies on MSTest source-generated reflection metadata, not on this code path.")] public object? CreateInstance(Type type, object?[] parameters) => Activator.CreateInstance(type, parameters); -#pragma warning restore IL2070 // this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'. -#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)' /// /// Checks to see if a member or type is decorated with the given attribute, or an attribute that derives from it. e.g. [MyTestClass] from [TestClass] will match if you look for [TestClass]. The inherit parameter does not impact this checking. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index 0e8c0c8be6..60f818f82f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -350,6 +350,7 @@ internal string GetTargetFrameworkVersionString(string sourceFileName) /// /// A list of path. /// + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Assembly.Location is explicitly checked for empty before use; in single-file/Native AOT scenarios it returns empty and the affected blocks are skipped.")] internal virtual List GetResolutionPaths(string sourceFileName, bool isPortableMode) { List resolutionPaths = @@ -380,7 +381,6 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP // We check for the empty path, and in single file mode, or on source gen mode we don't allow // loading dependencies than from the current folder, which is what the default loader handles by itself. -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file if (!string.IsNullOrEmpty(typeof(TestSourceHost).Assembly.Location)) { // Adding adapter folder to resolution paths @@ -398,7 +398,6 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP resolutionPaths.Add(Path.GetDirectoryName(typeof(AssemblyHelper).Assembly.Location)!); } } -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file return resolutionPaths; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs index 33f554a96c..481220ad0c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs @@ -117,6 +117,7 @@ internal TestProperty PropertyProvider(string propertyName) /// Discovery context. /// The logger to log exception messages too. /// Filter expression. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "GetTestCaseFilter is part of the VSTest discovery contract on the concrete DiscoveryContext type; the runtime guarantees the method exists on supported hosts.")] private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IMessageLogger logger) { try diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index f86755558b..dd6918a934 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -136,6 +136,7 @@ public static string GetTestResultsDirectory(IRunContext? runContext) => !String /// The deployment directory. /// Root results directory. /// Returns a list of deployment warnings. + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Deployment is a reflection-mode/legacy adapter feature; in single-file/Native AOT scenarios Assembly.Location returns an empty string and the comparison falls through without error.")] protected IEnumerable Deploy(IList deploymentItems, string testSourceHandler, string deploymentDirectory, string resultsDirectory) { Ensure.NotNullOrWhiteSpace(deploymentDirectory); @@ -194,9 +195,7 @@ protected IEnumerable Deploy(IList deploymentItems, stri // Ignore the test platform files. string tempFile = Path.GetFileName(fileToDeploy); // We throw when we run in source gen mode. -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file string assemblyName = Path.GetFileName(GetType().Assembly.Location); -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file if (tempFile.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs index e2d903229e..977788a3d8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs @@ -149,9 +149,7 @@ protected sealed override Task ExecuteRequestAsync(TestExecutionRequest request, CancellationToken cancellationToken) => ExecuteRequestWithRequestCountGuardAsync(async () => { -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file string[] testAssemblyPaths = [.. _getTestAssemblies().Select(GetAssemblyPath)]; -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file switch (request) { case DiscoverTestExecutionRequest discoverRequest: @@ -184,12 +182,10 @@ public void Dispose() /// returns an empty string (e.g. on Android CoreCLR /// where assemblies are memory-mapped). /// -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file + [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "Empty Assembly.Location is handled explicitly by falling back to the assembly simple name; see method body.")] internal static string GetAssemblyPath(Assembly assembly) { -#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file string location = assembly.Location; -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file if (!string.IsNullOrEmpty(location)) { return location; @@ -204,7 +200,6 @@ internal static string GetAssemblyPath(Assembly assembly) return name + ".dll"; } -#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file private async Task ExecuteRequestWithRequestCountGuardAsync(Func asyncFunc) { diff --git a/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs b/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs index 6eee7ba64e..8d2ffd3632 100644 --- a/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs +++ b/src/TestFramework/TestFramework/Internal/ReflectionTestMethodInfo.cs @@ -47,6 +47,12 @@ public ReflectionTestMethodInfo(MethodInfo methodInfo, string? displayName) public override bool IsDefined(Type attributeType, bool inherit) => _methodInfo.IsDefined(attributeType, inherit); +#if NET5_0_OR_GREATER + [RequiresUnreferencedCode("The native code for the generic method instantiation might not be available at runtime.")] +#endif +#if NET7_0_OR_GREATER + [RequiresDynamicCode("The native code for the generic method instantiation might not be available at runtime.")] +#endif public override MethodInfo MakeGenericMethod(params Type[] typeArguments) => new ReflectionTestMethodInfo(_methodInfo.MakeGenericMethod(typeArguments), DisplayName); public override Type[] GetGenericArguments() => _methodInfo.GetGenericArguments(); From d0547ce0bf4e533859b761303f5f8ce6e35e03a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 19:47:36 +0200 Subject: [PATCH 2/3] Add acceptance test that demonstrates the suppressions take effect Adds Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSources to MSTest.Acceptance.IntegrationTests/TrimTests.cs. Publishes a small project that references MSTest.TestAdapter with PublishTrimmed=true and TrimmerRootAssembly forcing trim analysis of MSTestAdapter.PlatformServices, Microsoft.Testing.Extensions.VSTestBridge, and MSTest.TestFramework. Asserts that the source files we suppressed in this PR no longer appear in publish output (the IL trimmer includes source paths in its warnings, so absence == suppression worked). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimTests.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs index 7fe25eae29..d7de06f91b 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Platform.Acceptance.IntegrationTests; +using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; namespace MSTest.Acceptance.IntegrationTests; @@ -61,5 +62,104 @@ await DotnetCli.RunAsync( cancellationToken: TestContext.CancellationToken); } + // Source code for a project that references MSTest.TestAdapter (the reflection-mode adapter) + // and runs trim analysis with TrimmerRootAssembly to scan the full surface of the assemblies + // we own. Used by Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSources. + // + // Note: We do not enable TreatWarningsAsErrors here because the reflection-mode adapter still + // depends on the vstest Microsoft.TestPlatform.ObjectModel submodule and on + // System.Private.DataContractSerialization internals, both of which emit trim warnings that + // are outside this repo's control. The test instead asserts that specific source files we + // suppressed in this repo no longer appear in publish output. + private const string TrimAnalysisWithTestAdapterSourceCode = """ +#file MSTestTrimAnalysisWithTestAdapter.csproj + + + $TargetFramework$ + Exe + true + true + + false + + + + + + + + + + + + + + +#file UnitTest1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MyTests; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} +"""; + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))] + public async Task Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSources(string tfm) + { + // Regression test for the suppressions added in https://github.com/microsoft/testfx/pull/8686. + // + // Before that PR, the listed source files emitted trim warnings (IL20xx/IL30xx) when a + // downstream consumer published a trimmed project that referenced MSTest.TestAdapter. + // The original C# `#pragma warning disable ILxxxx` directives in those files only + // silenced the compile-time warning during MSTest's own build; they did NOT propagate to + // the IL, so the linker still emitted the warnings at the consumer's publish time. + // + // After converting those pragmas to [UnconditionalSuppressMessage] / [RequiresUnreferencedCode] + // / [RequiresDynamicCode] attributes, the suppressions are honored by the IL trimmer and + // these source-file references should no longer appear in publish output. + using TestAsset generator = await TestAsset.GenerateAssetAsync( + $"MSTestTrimAnalysisWithTestAdapter_{tfm}", + TrimAnalysisWithTestAdapterSourceCode + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", tfm), + addPublicFeeds: true); + + DotnetMuxerResult result = await DotnetCli.RunAsync( + $"publish {generator.TargetAssetPath} -r {RID} -f {tfm}", + cancellationToken: TestContext.CancellationToken); + + // Files in MSTest's own source whose trim warnings are suppressed by this PR. + // The trimmer includes source file paths in its IL2xxx/IL3xxx warnings, so the absence + // of these file names in publish output is evidence that the suppression attributes work. + string[] suppressedSourceFiles = + [ + "TestSourceHost.cs", + "DeploymentUtilityBase.cs", + "ReflectionOperations.cs", + "AssemblyResolver.cs", + "DataSerializationHelper.cs", + "ManagedNameHelper.cs", + "MethodInfoExtensions.cs", + "TestMethodFilter.cs", + "SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs", + "ReflectionTestMethodInfo.cs", + ]; + + foreach (string fileName in suppressedSourceFiles) + { + result.AssertOutputDoesNotContain(fileName); + } + } + public TestContext TestContext { get; set; } } From d28a0ce3bf6ab6d1625000612548653447158935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 29 May 2026 22:13:48 +0200 Subject: [PATCH 3/3] Disable warnAsError in Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSources DotnetCli.RunAsync defaults warnAsError to true, which auto-injects -p:MSBuildTreatWarningsAsErrors=true -p:TreatWarningsAsErrors=true into the publish command. The acceptance test for this PR was written assuming TreatWarningsAsErrors is OFF (so out-of-repo trim warnings from the vstest ObjectModel submodule and App Insights stay as warnings, and the test can grep the publish output for the absence of suppressed source file names). Without this fix the publish fails with NETSDK1144 (Optimizing assemblies for size failed) due to dozens of trim warnings that are explicitly out of scope for this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MSTest.Acceptance.IntegrationTests/TrimTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs index d7de06f91b..cfd469dded 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs @@ -134,8 +134,13 @@ public async Task Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSo .PatchCodeWithReplace("$TargetFramework$", tfm), addPublicFeeds: true); + // Do NOT pass warnAsError: true here. The reflection-mode adapter still depends on the + // vstest Microsoft.TestPlatform.ObjectModel submodule and on System.Private.DataContractSerialization + // internals, both of which emit trim warnings that are outside this repo's control. Promoting them + // to errors would fail the publish with NETSDK1144 before we get a chance to inspect the warning list. DotnetMuxerResult result = await DotnetCli.RunAsync( $"publish {generator.TargetAssetPath} -r {RID} -f {tfm}", + warnAsError: false, cancellationToken: TestContext.CancellationToken); // Files in MSTest's own source whose trim warnings are suppressed by this PR.