Skip to content

Commit cbf1e69

Browse files
authored
add AOT mode checks to prevent unsupported reflection usage and suppress AOT warnings in reflection classes (#3239)
1 parent ff5efac commit cbf1e69

15 files changed

+2642
-1384
lines changed

TUnit.Core/DynamicTestBuilderContext.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace TUnit.Core;
24

35
/// <summary>
@@ -20,6 +22,7 @@ public DynamicTestBuilderContext(string filePath, int lineNumber)
2022

2123
public IReadOnlyList<AbstractDynamicTest> Tests => _tests.AsReadOnly();
2224

25+
[RequiresDynamicCode("Adding dynamic tests requires reflection which is not supported in native AOT scenarios.")]
2326
public void AddTest(AbstractDynamicTest test)
2427
{
2528
// Set creator location if the test implements IDynamicTestCreatorLocation
@@ -28,7 +31,7 @@ public void AddTest(AbstractDynamicTest test)
2831
testWithLocation.CreatorFilePath = FilePath;
2932
testWithLocation.CreatorLineNumber = LineNumber;
3033
}
31-
34+
3235
_tests.Add(test);
3336
}
3437
}

TUnit.Core/Extensions/TestContextExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static string GetClassTypeName(this TestContext context)
2424
return $"{context.TestDetails.ClassType.Name}({string.Join(", ", context.TestDetails.TestClassArguments.Select(a => ArgumentFormatter.Format(a, context.ArgumentDisplayFormatters)))})";
2525
}
2626

27+
[RequiresDynamicCode("Adding dynamic tests requires reflection which is not supported in native AOT scenarios.")]
2728
public static async Task AddDynamicTest<[DynamicallyAccessedMembers(
2829
DynamicallyAccessedMemberTypes.PublicConstructors
2930
| DynamicallyAccessedMemberTypes.NonPublicConstructors

TUnit.Core/Interfaces/ITestRegistry.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ public interface ITestRegistry
1414
/// <param name="context">The current test context</param>
1515
/// <param name="dynamicTest">The dynamic test instance to add</param>
1616
/// <returns>A task that completes when the test has been queued for execution</returns>
17+
[RequiresDynamicCode("Adding dynamic tests requires runtime compilation and reflection which are not supported in native AOT scenarios.")]
1718
Task AddDynamicTest<[DynamicallyAccessedMembers(
1819
DynamicallyAccessedMemberTypes.PublicConstructors
1920
| DynamicallyAccessedMemberTypes.NonPublicConstructors
2021
| DynamicallyAccessedMemberTypes.PublicProperties
2122
| DynamicallyAccessedMemberTypes.PublicMethods
2223
| DynamicallyAccessedMemberTypes.NonPublicMethods
2324
| DynamicallyAccessedMemberTypes.PublicFields
24-
| DynamicallyAccessedMemberTypes.NonPublicFields)] T>(TestContext context, DynamicTest<T> dynamicTest)
25+
| DynamicallyAccessedMemberTypes.NonPublicFields)] T>(TestContext context, DynamicTest<T> dynamicTest)
2526
where T : class;
2627
}

TUnit.Core/PropertyInjection/Initialization/Strategies/ReflectionPropertyStrategy.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.CompilerServices;
23
using System.Threading.Tasks;
34
using TUnit.Core.DataSources;
45
using TUnit.Core.Initialization;
@@ -33,6 +34,13 @@ public bool CanHandle(PropertyInitializationContext context)
3334
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Reflection mode support")]
3435
public async Task InitializePropertyAsync(PropertyInitializationContext context)
3536
{
37+
#if NET
38+
if (!RuntimeFeature.IsDynamicCodeSupported)
39+
{
40+
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
41+
}
42+
#endif
43+
3644
if (context.PropertyInfo == null || context.DataSource == null)
3745
{
3846
return;
@@ -58,4 +66,4 @@ public async Task InitializePropertyAsync(PropertyInitializationContext context)
5866
PropertyTrackingService.AddToTestContext(context, resolvedValue);
5967
}
6068

61-
}
69+
}

TUnit.Engine/Building/Collectors/AotTestDataCollector.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ private async IAsyncEnumerable<TestMetadata> CollectDynamicTestsStreaming(
7676
}
7777
}
7878

79+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
80+
Justification = "Dynamic tests are opt-in and users are warned via RequiresDynamicCode on the method they call")]
7981
private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreaming(
8082
AbstractDynamicTest abstractDynamicTest,
8183
[EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -92,6 +94,7 @@ private async IAsyncEnumerable<TestMetadata> ConvertDynamicTestToMetadataStreami
9294
}
9395
}
9496

97+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic tests require runtime compilation of lambda expressions and are not supported in native AOT scenarios.")]
9598
private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDiscoveryResult result)
9699
{
97100
if (result.TestClassType == null || result.TestMethod == null)
@@ -144,6 +147,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
144147
});
145148
}
146149

150+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic test instance creation requires Activator.CreateInstance and MakeGenericType which are not supported in native AOT scenarios.")]
147151
[UnconditionalSuppressMessage("Trimming",
148152
"IL2070:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'System.Type.GetConstructors()'",
149153
Justification = "AOT mode uses source-generated factories")]
@@ -187,6 +191,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco
187191
};
188192
}
189193

194+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Dynamic test invocation requires LambdaExpression.Compile() which is not supported in native AOT scenarios.")]
190195
private static Func<object, object?[], Task> CreateAotDynamicTestInvoker(DynamicDiscoveryResult result)
191196
{
192197
return async (instance, args) =>

TUnit.Engine/Building/ReflectionMetadataBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.CompilerServices;
23
using TUnit.Core;
34

45
namespace TUnit.Engine.Building;

TUnit.Engine/Building/TestDataCollectorFactory.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ internal static class TestDataCollectorFactory
2323
/// Creates a test data collector based on the specified or detected mode.
2424
/// Source generation mode is preferred for AOT compatibility.
2525
/// </summary>
26-
[UnconditionalSuppressMessage("Trimming", "IL2026:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code", Justification = "Reflection mode is explicitly chosen and cannot support trimming")]
27-
[UnconditionalSuppressMessage("AOT", "IL3050:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling", Justification = "Reflection mode is explicitly chosen and cannot support AOT")]
2826
public static ITestDataCollector Create(bool? useSourceGeneration = null, Assembly[]? assembliesToScan = null)
2927
{
3028
var isSourceGenerationEnabled = useSourceGeneration ?? SourceRegistrar.IsEnabled;
@@ -43,8 +41,6 @@ public static ITestDataCollector Create(bool? useSourceGeneration = null, Assemb
4341
/// Attempts AOT mode first, falls back to reflection if no source-generated tests found.
4442
/// This provides automatic mode selection for optimal performance and compatibility.
4543
/// </summary>
46-
[UnconditionalSuppressMessage("Trimming", "IL2026:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code", Justification = "Reflection mode is a fallback and cannot support trimming")]
47-
[UnconditionalSuppressMessage("AOT", "IL3050:Using member 'TUnit.Engine.Discovery.ReflectionTestDataCollector.ReflectionTestDataCollector()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling", Justification = "Reflection mode is a fallback and cannot support AOT")]
4844
public static async Task<ITestDataCollector> CreateAutoDetectAsync(string testSessionId, Assembly[]? assembliesToScan = null)
4945
{
5046
// Try AOT mode first (check if any tests were registered)

TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Concurrent;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Reflection;
4+
using System.Runtime.CompilerServices;
45
using TUnit.Core;
56

67
namespace TUnit.Engine.Discovery;
@@ -59,8 +60,15 @@ public override int GetHashCode()
5960
/// </summary>
6061
public static T? GetAttribute<T>(Type testClass, MethodInfo? testMethod = null) where T : Attribute
6162
{
63+
#if NET
64+
if (!RuntimeFeature.IsDynamicCodeSupported)
65+
{
66+
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
67+
}
68+
#endif
69+
6270
var cacheKey = new AttributeCacheKey(testClass, testMethod, typeof(T));
63-
71+
6472
return (T?)_attributeCache.GetOrAdd(cacheKey, key =>
6573
{
6674
// Original lookup logic preserved
@@ -88,6 +96,13 @@ public override int GetHashCode()
8896
/// </summary>
8997
public static IEnumerable<T> GetAttributes<T>(Type testClass, MethodInfo? testMethod = null) where T : Attribute
9098
{
99+
#if NET
100+
if (!RuntimeFeature.IsDynamicCodeSupported)
101+
{
102+
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
103+
}
104+
#endif
105+
91106
var attributes = new List<T>();
92107

93108
attributes.AddRange(testClass.Assembly.GetCustomAttributes<T>());
@@ -104,7 +119,7 @@ public static IEnumerable<T> GetAttributes<T>(Type testClass, MethodInfo? testMe
104119
public static string[] ExtractCategories(Type testClass, MethodInfo testMethod)
105120
{
106121
var categories = new HashSet<string>();
107-
122+
108123
foreach (var attr in GetAttributes<CategoryAttribute>(testClass, testMethod))
109124
{
110125
categories.Add(attr.Category);
@@ -128,7 +143,7 @@ public static bool CanRunInParallel(Type testClass, MethodInfo testMethod)
128143
public static TestDependency[] ExtractDependencies(Type testClass, MethodInfo testMethod)
129144
{
130145
var dependencies = new List<TestDependency>();
131-
146+
132147
foreach (var attr in GetAttributes<DependsOnAttribute>(testClass, testMethod))
133148
{
134149
dependencies.Add(attr.ToTestDependency());
@@ -155,13 +170,13 @@ public static IDataSourceAttribute[] ExtractDataSources(ICustomAttributeProvider
155170
public static Attribute[] GetAllAttributes(Type testClass, MethodInfo testMethod)
156171
{
157172
var attributes = new List<Attribute>();
158-
173+
159174
// Add in reverse order of precedence so method attributes come first
160175
// This ensures ScopedAttributeFilter will keep method-level attributes over class/assembly
161176
attributes.AddRange(testMethod.GetCustomAttributes());
162177
attributes.AddRange(testClass.GetCustomAttributes());
163178
attributes.AddRange(testClass.Assembly.GetCustomAttributes());
164-
179+
165180
return attributes.ToArray();
166181
}
167182

@@ -193,4 +208,4 @@ public static PropertyDataSource[] ExtractPropertyDataSources([DynamicallyAccess
193208

194209
return propertyDataSources.ToArray();
195210
}
196-
}
211+
}

TUnit.Engine/Discovery/ReflectionGenericTypeResolver.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Reflection;
3+
using System.Runtime.CompilerServices;
34
using TUnit.Core;
45

56
namespace TUnit.Engine.Discovery;
67

78
/// <summary>
89
/// Handles generic type resolution and instantiation for reflection-based test discovery
910
/// </summary>
10-
[RequiresUnreferencedCode("Reflection-based generic type resolution requires unreferenced code")]
11+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Reflection mode cannot support trimming")]
12+
[UnconditionalSuppressMessage("Trimming", "IL2055:Call to 'System.Type.MakeGenericType' can not be statically analyzed", Justification = "Reflection mode requires dynamic access")]
13+
[UnconditionalSuppressMessage("Trimming", "IL2065:Value passed to implicit 'this' parameter of method can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements", Justification = "Reflection mode requires dynamic access")]
14+
[UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter does not satisfy annotation requirements", Justification = "Reflection mode requires dynamic access")]
15+
[UnconditionalSuppressMessage("Trimming", "IL2070:Target method does not satisfy annotation requirements", Justification = "Reflection mode requires dynamic access")]
16+
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods(BindingFlags)'", Justification = "Reflection mode requires dynamic access")]
17+
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Reflection mode cannot support AOT")]
1118
internal static class ReflectionGenericTypeResolver
1219
{
1320
/// <summary>
1421
/// Determines generic type arguments from data row values
1522
/// </summary>
1623
public static Type[]? DetermineGenericTypeArguments(Type genericTypeDefinition, object?[] dataRow)
1724
{
25+
#if NET
26+
if (!RuntimeFeature.IsDynamicCodeSupported)
27+
{
28+
throw new Exception("Using TUnit Reflection mechanisms isn't supported in AOT mode");
29+
}
30+
#endif
31+
1832
var genericParameters = genericTypeDefinition.GetGenericArguments();
1933

2034
// If no data row or empty data, can't determine types
@@ -68,9 +82,6 @@ internal static class ReflectionGenericTypeResolver
6882
/// <summary>
6983
/// Extracts generic type information including constraints
7084
/// </summary>
71-
[UnconditionalSuppressMessage("Trimming",
72-
"IL2065:Value passed to implicit 'this' parameter of method 'System.Type.GetInterfaces()' can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements",
73-
Justification = "Reflection mode requires dynamic access")]
7485
public static GenericTypeInfo? ExtractGenericTypeInfo(Type testClass)
7586
{
7687
if (!testClass.IsGenericTypeDefinition)
@@ -106,9 +117,6 @@ internal static class ReflectionGenericTypeResolver
106117
/// <summary>
107118
/// Extracts generic method information including parameter positions
108119
/// </summary>
109-
[UnconditionalSuppressMessage("Trimming",
110-
"IL2065:Value passed to implicit 'this' parameter of method 'System.Type.GetInterfaces()' can not be statically determined and may not meet 'DynamicallyAccessedMembersAttribute' requirements",
111-
Justification = "Reflection mode requires dynamic access")]
112120
public static GenericMethodInfo? ExtractGenericMethodInfo(MethodInfo method)
113121
{
114122
if (!method.IsGenericMethodDefinition)
@@ -157,10 +165,6 @@ internal static class ReflectionGenericTypeResolver
157165
/// <summary>
158166
/// Creates a concrete type from a generic type definition and validates the type arguments
159167
/// </summary>
160-
[UnconditionalSuppressMessage("Trimming", "IL2055:Call to 'System.Type.MakeGenericType' can not be statically analyzed",
161-
Justification = "Reflection mode requires dynamic access")]
162-
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
163-
Justification = "Reflection mode cannot support AOT")]
164168
public static Type CreateConcreteType(Type genericTypeDefinition, Type[] typeArguments)
165169
{
166170
var genericParams = genericTypeDefinition.GetGenericArguments();
@@ -174,4 +178,4 @@ public static Type CreateConcreteType(Type genericTypeDefinition, Type[] typeArg
174178

175179
return genericTypeDefinition.MakeGenericType(typeArguments);
176180
}
177-
}
181+
}

0 commit comments

Comments
 (0)