Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
81d1146
refactor(formatting): escape dots in strings to prevent namespace int…
thomhurst Oct 23, 2025
f1b749d
test(aot): add tests for AOT compatibility with enum arguments to pre…
thomhurst Oct 23, 2025
36a6f49
refactor(assertions): simplify assertion calls by removing unnecessar…
thomhurst Oct 23, 2025
fe7681d
refactor(matrix): streamline enum handling and remove unnecessary dyn…
thomhurst Oct 23, 2025
fa548ef
refactor(attributes): replace RequiresDynamicCode with RequiresUnrefe…
thomhurst Oct 23, 2025
735f0aa
test(aot): enhance AOT compatibility with property injection and dyna…
thomhurst Oct 23, 2025
a1d045b
refactor(tests): remove unused methods and improve AOT compatibility …
thomhurst Oct 23, 2025
3701435
chore(dependencies): update TUnit package version to 0.75.38-PullRequ…
thomhurst Oct 23, 2025
e581a0c
refactor(attributes): skip compiler-internal attributes during attrib…
thomhurst Oct 23, 2025
abb79d3
refactor(PropertyInjection): use non-nullable type names for improved…
thomhurst Oct 23, 2025
3b3f622
refactor(CastHelper): remove unnecessary suppress message for AOT com…
thomhurst Oct 23, 2025
4062403
refactor(CastHelper): simplify casting logic by removing unnecessary …
thomhurst Oct 23, 2025
099962b
refactor(CastHelper): enhance casting logic with AOT-safe conversions…
thomhurst Oct 23, 2025
a2c182b
refactor(CastHelper): optimize casting method by adding direct type c…
thomhurst Oct 23, 2025
2e31d58
refactor(CastHelper): improve AOT handling in TryReflectionConversion…
thomhurst Oct 24, 2025
c4052e9
feat: enhance AOT converter by scanning for conversion operators in s…
thomhurst Oct 24, 2025
9376b1e
feat: improve AOT converter by scanning all types in compilation for …
thomhurst Oct 24, 2025
f24825d
feat: add support for scanning closed generic types in method paramet…
thomhurst Oct 24, 2025
05063b5
feat: add AOT and single file publishing steps to CI pipeline
thomhurst Oct 24, 2025
0cdc612
feat: update TUnit package version references to use TUnitVersion var…
thomhurst Oct 24, 2025
917ea59
feat: add additional suppression message for trimming in reflection mode
thomhurst Oct 24, 2025
4a8ca00
feat: introduce IMemberMetadata interface and update related metadata…
thomhurst Oct 24, 2025
37e1e0a
fix: refine DynamicallyAccessedMembers attributes in Cast methods for…
thomhurst Oct 24, 2025
ecabc37
fix: update DynamicallyAccessedMembers attributes in Cast methods for…
thomhurst Oct 24, 2025
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
Prev Previous commit
Next Next commit
refactor(attributes): replace RequiresDynamicCode with RequiresUnrefe…
…rencedCode for AOT compatibility
  • Loading branch information
thomhurst committed Oct 24, 2025
commit fa548ef8abf22aaf9bb8a608f47a1b2dc24d3e80
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}

// Check for RequiresDynamicCode attribute
var requiresDynamicCodeAttr = classSymbol.GetAttributes()
.FirstOrDefault(attr => attr.AttributeClass?.Name == "RequiresDynamicCodeAttribute");
string? requiresDynamicCodeMessage = null;
if (requiresDynamicCodeAttr != null && requiresDynamicCodeAttr.ConstructorArguments.Length > 0)
// Check for RequiresUnreferencedCode attribute
var RequiresUnreferencedCodeAttr = classSymbol.GetAttributes()
.FirstOrDefault(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute");
string? RequiresUnreferencedCodeMessage = null;
if (RequiresUnreferencedCodeAttr != null && RequiresUnreferencedCodeAttr.ConstructorArguments.Length > 0)
{
requiresDynamicCodeMessage = requiresDynamicCodeAttr.ConstructorArguments[0].Value?.ToString();
RequiresUnreferencedCodeMessage = RequiresUnreferencedCodeAttr.ConstructorArguments[0].Value?.ToString();
}

return new AssertionExtensionData(
Expand All @@ -108,7 +108,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
assertionBaseType,
constructors,
overloadPriority,
requiresDynamicCodeMessage
RequiresUnreferencedCodeMessage
);
}

Expand Down Expand Up @@ -153,7 +153,7 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As

sourceBuilder.AppendLine($"namespace TUnit.Assertions.Extensions;");
sourceBuilder.AppendLine();

// Extension class
var extensionClassName = $"{data.ClassSymbol.Name}Extensions";
sourceBuilder.AppendLine($"/// <summary>");
Expand Down Expand Up @@ -309,11 +309,11 @@ private static void GenerateExtensionMethod(
sourceBuilder.AppendLine($" /// Extension method for {assertionType.Name}.");
sourceBuilder.AppendLine(" /// </summary>");

// Add RequiresDynamicCode attribute if present
if (!string.IsNullOrEmpty(data.RequiresDynamicCodeMessage))
// Add RequiresUnreferencedCode attribute if present
if (!string.IsNullOrEmpty(data.RequiresUnreferencedCodeMessage))
{
var escapedMessage = data.RequiresDynamicCodeMessage!.Replace("\"", "\\\"");
sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresDynamicCode(\"{escapedMessage}\")]");
var escapedMessage = data.RequiresUnreferencedCodeMessage!.Replace("\"", "\\\"");
sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escapedMessage}\")]");
}

// Add OverloadResolutionPriority attribute only if priority > 0
Expand Down Expand Up @@ -454,6 +454,6 @@ private record AssertionExtensionData(
INamedTypeSymbol AssertionBaseType,
ImmutableArray<IMethodSymbol> Constructors,
int OverloadResolutionPriority,
string? RequiresDynamicCodeMessage
string? RequiresUnreferencedCodeMessage
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace TUnit.Assertions.Conditions.Helpers;
/// For complex objects, performs deep comparison of properties and fields.
/// </summary>
/// <typeparam name="T">The type of objects to compare</typeparam>
[RequiresDynamicCode("Structural equality comparison uses reflection to access object members and is not compatible with AOT")]
[RequiresUnreferencedCode("Structural equality comparison uses reflection to access object members and is not compatible with AOT")]
public sealed class StructuralEqualityComparer<T> : IEqualityComparer<T>
{
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Assertions/Conditions/IsEquivalentToAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace TUnit.Assertions.Conditions;
/// Inherits from CollectionComparerBasedAssertion to preserve collection type awareness in And/Or chains.
/// </summary>
[AssertionExtension("IsEquivalentTo")]
[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
[RequiresUnreferencedCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
public class IsEquivalentToAssertion<TCollection, TItem> : CollectionComparerBasedAssertion<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Assertions/Conditions/NotEquivalentToAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace TUnit.Assertions.Conditions;
/// Inherits from CollectionComparerBasedAssertion to preserve collection type awareness in And/Or chains.
/// </summary>
[AssertionExtension("IsNotEquivalentTo")]
[RequiresDynamicCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
[RequiresUnreferencedCode("Collection equivalency uses structural comparison for complex objects, which requires reflection and is not compatible with AOT")]
public class NotEquivalentToAssertion<TCollection, TItem> : CollectionComparerBasedAssertion<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace TUnit.Assertions.Conditions;
/// Asserts that two objects are structurally equivalent by comparing their properties and fields.
/// Supports partial equivalency and member exclusion.
/// </summary>
[RequiresUnreferencedCode("Uses reflection to compare object properties and fields.")]
public class StructuralEquivalencyAssertion<TValue> : Assertion<TValue>
{
private readonly object? _expected;
Expand Down Expand Up @@ -166,9 +167,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string
}

// Compare properties and fields
#pragma warning disable IL2072 // GetType() does not preserve DynamicallyAccessedMembers - acceptable for runtime structural comparison
var expectedMembers = GetMembersToCompare(expectedType);
#pragma warning restore IL2072

foreach (var member in expectedMembers)
{
Expand Down Expand Up @@ -199,9 +198,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string
// In partial equivalency mode, skip members that don't exist on actual
if (_usePartialEquivalency)
{
#pragma warning disable IL2072 // GetType() does not preserve DynamicallyAccessedMembers - acceptable for runtime structural comparison
var actualMember = GetMemberInfo(actualType, member.Name);
#pragma warning restore IL2072
if (actualMember == null)
{
continue;
Expand All @@ -211,9 +208,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string
}
else
{
#pragma warning disable IL2072 // GetType() does not preserve DynamicallyAccessedMembers - acceptable for runtime structural comparison
var actualMember = GetMemberInfo(actualType, member.Name);
#pragma warning restore IL2072
if (actualMember == null)
{
return AssertionResult.Failed($"Property {memberPath} did not match{Environment.NewLine}Expected: {FormatValue(expectedValue)}{Environment.NewLine}Received: null");
Expand All @@ -231,9 +226,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string
// In non-partial mode, check for extra properties on actual
if (!_usePartialEquivalency)
{
#pragma warning disable IL2072 // GetType() does not preserve DynamicallyAccessedMembers - acceptable for runtime structural comparison
var actualMembers = GetMembersToCompare(actualType);
#pragma warning restore IL2072
var expectedMemberNames = new HashSet<string>(expectedMembers.Select(m => m.Name));

foreach (var member in actualMembers)
Expand Down
35 changes: 15 additions & 20 deletions TUnit.Core/AsyncConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ public static async ValueTask Convert(Func<Task> action)
| MethodImplOptions.AggressiveOptimization
#endif
)]
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("ConvertObject uses reflection to handle custom awaitable types and F# async. For AOT compatibility, use Task or ValueTask directly.")]
[RequiresDynamicCode("ConvertObject may require dynamic invocation for custom awaitable types. For AOT compatibility, use Task or ValueTask directly.")]
#endif

public static async ValueTask ConvertObject(object? invoke)
{
if (invoke is Delegate @delegate)
Expand Down Expand Up @@ -116,17 +113,23 @@ public static async ValueTask ConvertObject(object? invoke)
}
}

[System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods, "Microsoft.FSharp.Control.FSharpAsync", "FSharp.Core")]
[System.Diagnostics.CodeAnalysis.DynamicDependency("StartAsTask", "Microsoft.FSharp.Control.FSharpAsync", "FSharp.Core")]
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("F# async support requires FSharp.Core types and reflection. For AOT, use Task-based APIs.")]
[RequiresDynamicCode("F# async interop requires MakeGenericMethod. For AOT, use Task-based APIs.")]
#endif
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "Microsoft.FSharp.Control.FSharpAsync", "FSharp.Core")]
[DynamicDependency("StartAsTask", "Microsoft.FSharp.Control.FSharpAsync", "FSharp.Core")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
[UnconditionalSuppressMessage("Trimming", "IL2060:Call to \'System.Reflection.MethodInfo.MakeGenericMethod\' can not be statically analyzed. It\'s not possible to guarantee the availability of requirements of the generic method.")]
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")]
[UnconditionalSuppressMessage("Trimming", "IL2077:Target parameter argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The source field does not have matching annotations.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with \'RequiresDynamicCodeAttribute\' may break functionality when AOT compiling.")]
private static ValueTask StartAsFSharpTask(object invoke, Type type)
{
var startAsTaskOpenGenericMethod = (_fSharpAsyncType ??= type.Assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"))!
.GetRuntimeMethods()
.First(m => m.Name == "StartAsTask");
.FirstOrDefault(m => m.Name == "StartAsTask");

if (startAsTaskOpenGenericMethod is null)
{
return default;
}

var fSharpTask = (Task) startAsTaskOpenGenericMethod.MakeGenericMethod(type.GetGenericArguments()[0])
.Invoke(null, [invoke, null, null])!;
Expand All @@ -143,16 +146,11 @@ private static ValueTask StartAsFSharpTask(object invoke, Type type)
/// <summary>
/// Safely invokes F# async conversion with proper suppression for the call site.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("F# async is an optional feature. AOT applications should use Task-based APIs.")]
[RequiresDynamicCode("F# async requires runtime code generation. This is documented as not AOT-compatible.")]
#endif
private static async ValueTask StartAsFSharpTaskSafely(object invoke, Type type)
{
if (IsFSharpAsyncSupported())
{
await StartAsFSharpTask(invoke, type);
return;
}
}

Expand Down Expand Up @@ -181,10 +179,7 @@ private static bool IsFSharpAsyncSupported()
}
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("GetAwaiter pattern detection requires reflection for custom awaitable types. For AOT, use Task/ValueTask.")]
[RequiresDynamicCode("Custom awaitable handling may require dynamic invocation. For AOT, use Task/ValueTask.")]
#endif
[UnconditionalSuppressMessage("Trimming", "IL2075:\'this\' argument does not satisfy \'DynamicallyAccessedMembersAttribute\' in call to target method. The return value of the source method does not have matching annotations.")]
public static bool TryGetAwaitableTask(object awaitable, [NotNullWhen(true)] out Task? task)
{
var getAwaiter = awaitable.GetType().GetMethod("GetAwaiter", Type.EmptyTypes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
namespace TUnit.Core;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)]
#if NET6_0_OR_GREATER
[RequiresDynamicCode("AsyncUntypedDataSourceGeneratorAttribute requires dynamic code generation for runtime data source creation. Consider using strongly-typed AsyncDataSourceGeneratorAttribute<T> overloads for AOT compatibility.")]
[RequiresUnreferencedCode("AsyncUntypedDataSourceGeneratorAttribute may require unreferenced code for runtime data source creation. Consider using strongly-typed AsyncDataSourceGeneratorAttribute<T> overloads for AOT compatibility.")]
#endif
public abstract class AsyncUntypedDataSourceGeneratorAttribute : Attribute, IAsyncUntypedDataSourceGeneratorAttribute
{
protected abstract IAsyncEnumerable<Func<Task<object?[]?>>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata);
Expand Down
Loading