From f5ec8793fbc3473e6219b54a96e1f76691afaae0 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:21:04 +0100
Subject: [PATCH 1/7] fix(source-gen): stop parameter resolver keeping every
non-public test-class method (IL2111)
#6923 annotated ParameterMetadataFactory.ForMethod/ForGenericMethod/ForConstructor's
declaring type with NonPublicMethods. The trimmer then kept every private method of
each test class and reported IL2111 for any private helper with
DynamicallyAccessedMembers parameters, failing AOT publishes that passed on 1.71.0.
The annotation now matches what ClassMetadata.Type already keeps (public constructors
and methods), and the resolver looks up public members only. Non-public test methods
use the new ForMethodLookup, whose generated intrinsic GetMethod call roots only that
method, as the pre-#6923 per-parameter lambdas did.
---
.../Utilities/MetadataGenerationHelper.cs | 24 ++++++++
src/TUnit.Core/ParameterMetadataFactory.cs | 58 ++++++++++++++-----
...Has_No_API_Changes.DotNet10_0.verified.txt | 7 ++-
..._Has_No_API_Changes.DotNet8_0.verified.txt | 7 ++-
..._Has_No_API_Changes.DotNet9_0.verified.txt | 7 ++-
...ary_Has_No_API_Changes.Net4_7.verified.txt | 1 +
.../ParameterReflectionInfoTests.cs | 46 ++++++++++++---
7 files changed, 118 insertions(+), 32 deletions(-)
diff --git a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index 5c75d3b5f8..c2323dfcbe 100644
--- a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -236,6 +236,13 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
{
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForConstructor(typeof({containingType}), {usesTypeParameters.ToString().ToLowerInvariant()}, ");
}
+ else if (method.DeclaredAccessibility != Accessibility.Public)
+ {
+ // The factory's declaring-type annotation only keeps public methods, so trimming stays limited to what
+ // ClassMetadata.Type already keeps. A non-public method is rooted with its own intrinsic lookup instead
+ // of annotating the type with NonPublicMethods, which would keep (and warn about) every private helper.
+ writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForMethodLookup(\"{method.Name}\", static () => {GenerateNonPublicMethodLookup(method, containingType, usesTypeParameters)}, ");
+ }
else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof({containingType}), \"{method.Name}\", ");
@@ -246,6 +253,23 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
}
}
+ private static string GenerateNonPublicMethodLookup(IMethodSymbol method, string containingType, bool usesTypeParameters)
+ {
+ if (method.TypeParameters.Length > 0 || usesTypeParameters)
+ {
+ return $"global::System.Linq.Enumerable.FirstOrDefault(typeof({containingType}).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Static), m => m.Name == \"{method.Name}\" && m.GetParameters().Length == {method.Parameters.Length})";
+ }
+
+ var bindingFlags = method.IsStatic
+ ? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
+ : "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
+ var parameterTypes = method.Parameters.Length == 0
+ ? "global::System.Type.EmptyTypes"
+ : $"new global::System.Type[] {{ {string.Join(", ", method.Parameters.Select(p => $"typeof({p.Type.GloballyQualified()})"))} }}";
+
+ return $"typeof({containingType}).GetMethod(\"{method.Name}\", {bindingFlags}, null, {parameterTypes}, null)";
+ }
+
///
/// Generates code for creating a PropertyMetadata instance
///
diff --git a/src/TUnit.Core/ParameterMetadataFactory.cs b/src/TUnit.Core/ParameterMetadataFactory.cs
index 1372fb6836..2b353e9aa6 100644
--- a/src/TUnit.Core/ParameterMetadataFactory.cs
+++ b/src/TUnit.Core/ParameterMetadataFactory.cs
@@ -12,10 +12,15 @@ namespace TUnit.Core;
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ParameterMetadataFactory
{
+ ///
+ /// Deliberately limited to what already keeps for a test class. Requesting
+ /// non-public methods here would keep every private helper of the class and report IL2111 for any helper
+ /// with parameters. Non-public test methods go through
+ /// , whose generated lookup roots only that method.
+ ///
internal const DynamicallyAccessedMemberTypes DeclaringTypeMembers =
DynamicallyAccessedMemberTypes.PublicConstructors
- | DynamicallyAccessedMemberTypes.PublicMethods
- | DynamicallyAccessedMemberTypes.NonPublicMethods;
+ | DynamicallyAccessedMemberTypes.PublicMethods;
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067",
Justification = "Factory is only called from generated code that always passes concrete types")]
@@ -66,6 +71,19 @@ public static ParameterMetadata[] ForGenericMethod(
ParameterInfoResolver.Kind.MethodByParameterCount, parameters));
}
+ ///
+ /// Attaches lazy resolution to the parameters of a non-public
+ /// method. runs once, on first access; the generator emits it as an intrinsic
+ /// reflection call so trimming keeps exactly that method.
+ ///
+ public static ParameterMetadata[] ForMethodLookup(
+ string methodName,
+ Func lookup,
+ params ParameterMetadata[] parameters)
+ {
+ return Attach(parameters, new ParameterInfoResolver(lookup, methodName, parameters));
+ }
+
///
/// Attaches lazy resolution to the parameters of a public
/// constructor. When is true the constructor is matched by
@@ -110,12 +128,14 @@ internal enum Kind
MethodByParameterCount,
Constructor,
ConstructorByParameterCount,
+ Lookup,
}
- private const BindingFlags AllMethods = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ private const BindingFlags PublicMethods = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
[DynamicallyAccessedMembers(ParameterMetadataFactory.DeclaringTypeMembers)]
- private readonly Type _declaringType;
+ private readonly Type? _declaringType;
+ private readonly Func? _lookup;
private readonly string? _methodName;
private readonly Kind _kind;
private readonly ParameterMetadata[] _parameters;
@@ -133,6 +153,14 @@ public ParameterInfoResolver(
_parameters = parameters;
}
+ public ParameterInfoResolver(Func lookup, string methodName, ParameterMetadata[] parameters)
+ {
+ _lookup = lookup;
+ _methodName = methodName;
+ _kind = Kind.Lookup;
+ _parameters = parameters;
+ }
+
public ParameterInfo? Get(int index)
{
// Benign race: concurrent first accesses may each run Resolve(), but the lookup is idempotent and
@@ -145,11 +173,12 @@ internal string Describe()
{
return _kind switch
{
- Kind.InstanceMethod => $"instance method '{_declaringType.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
- Kind.StaticMethod => $"static method '{_declaringType.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
- Kind.MethodByParameterCount => $"method '{_declaringType.FullName}.{_methodName}' matched by parameter count ({_parameters.Length})",
- Kind.Constructor => $"constructor of '{_declaringType.FullName}' with {_parameters.Length} parameter(s) matched by parameter types",
- _ => $"constructor of '{_declaringType.FullName}' matched by parameter count ({_parameters.Length})",
+ Kind.InstanceMethod => $"instance method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
+ Kind.StaticMethod => $"static method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
+ Kind.MethodByParameterCount => $"method '{_declaringType!.FullName}.{_methodName}' matched by parameter count ({_parameters.Length})",
+ Kind.Constructor => $"constructor of '{_declaringType!.FullName}' with {_parameters.Length} parameter(s) matched by parameter types",
+ Kind.ConstructorByParameterCount => $"constructor of '{_declaringType!.FullName}' matched by parameter count ({_parameters.Length})",
+ _ => $"non-public method '{_methodName}' with {_parameters.Length} parameter(s)",
};
}
@@ -157,11 +186,12 @@ private ParameterInfo[] Resolve()
{
MethodBase? member = _kind switch
{
- Kind.InstanceMethod => _declaringType.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, GetParameterTypes(), null),
- Kind.StaticMethod => _declaringType.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, GetParameterTypes(), null),
- Kind.MethodByParameterCount => FindByParameterCount(_declaringType.GetMethods(AllMethods)),
- Kind.Constructor => _declaringType.GetConstructor(GetParameterTypes()),
- _ => FindByParameterCount(_declaringType.GetConstructors()),
+ Kind.InstanceMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Instance, null, GetParameterTypes(), null),
+ Kind.StaticMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Static, null, GetParameterTypes(), null),
+ Kind.MethodByParameterCount => FindByParameterCount(_declaringType!.GetMethods(PublicMethods)),
+ Kind.Constructor => _declaringType!.GetConstructor(GetParameterTypes()),
+ Kind.ConstructorByParameterCount => FindByParameterCount(_declaringType!.GetConstructors()),
+ _ => _lookup!(),
};
return member?.GetParameters() ?? [];
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index de68f07dba..fc421d104e 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1140,9 +1140,10 @@ namespace
{
[.("ReflectionAnalysis", "IL2067", Justification="Factory is only called from generated code that always passes concrete types")]
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
- public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index bc7a367711..4cd9a171cc 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1140,9 +1140,10 @@ namespace
{
[.("ReflectionAnalysis", "IL2067", Justification="Factory is only called from generated code that always passes concrete types")]
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
- public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index 73f1393bdc..b24080d8b6 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1140,9 +1140,10 @@ namespace
{
[.("ReflectionAnalysis", "IL2067", Justification="Factory is only called from generated code that always passes concrete types")]
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
- public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index 53d92395da..25a3b5b00c 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1103,6 +1103,7 @@ namespace
public static .ParameterMetadata[] ForConstructor( declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod( declaringType, string methodName, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod( declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
index 8729d2d7c0..f2b22304b4 100644
--- a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
+++ b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using TUnit.TestProject.Attributes;
@@ -48,16 +49,40 @@ public async Task Params_Array(params int[] values)
await Assert.That(parameter.IsParams).IsTrue();
}
+ [Test]
+ [Arguments(7, "seven")]
+ internal async Task Internal_Method(int value, string text)
+ {
+ var parameters = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters;
+
+ await AssertParameter(parameters[0], nameof(value), 0, typeof(int));
+ await AssertParameter(parameters[1], nameof(text), 1, typeof(string));
+ await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Method));
+ }
+
+ [Test]
+ public async Task Private_Helper_With_Annotated_Parameter()
+ {
+ // Regression for IL2111: generated metadata must not ask the trimmer to keep every non-public method of
+ // the test class, or a private helper like this one is reported as accessed via reflection.
+ await Assert.That(CountFields(typeof(ParameterReflectionInfoTests))).IsGreaterThanOrEqualTo(0);
+ }
+
+ private static int CountFields([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type type)
+ {
+ return type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Length;
+ }
+
[Test]
public async Task Static_Method_Via_Factory()
{
- var parameters = ParameterMetadataFactory.ForMethod(typeof(ParameterReflectionInfoTests), nameof(StaticHelper), true,
+ var parameters = ParameterMetadataFactory.ForMethod(typeof(Helpers), nameof(Helpers.StaticHelper), true,
ParameterMetadataFactory.Create(typeof(int), "value", new ConcreteType(typeof(int)), false),
ParameterMetadataFactory.Create(typeof(string), "text", new ConcreteType(typeof(string)), false));
await AssertParameter(parameters[0], "value", 0, typeof(int));
await AssertParameter(parameters[1], "text", 1, typeof(string));
- await Assert.That(parameters[1].ReflectionInfo.Member.Name).IsEqualTo(nameof(StaticHelper));
+ await Assert.That(parameters[1].ReflectionInfo.Member.Name).IsEqualTo(nameof(Helpers.StaticHelper));
}
[Test]
@@ -76,11 +101,11 @@ public async Task Open_Generic_Constructor_Matched_By_Parameter_Count()
[Test]
public async Task Generic_Method_Via_Factory()
{
- var parameters = ParameterMetadataFactory.ForGenericMethod(typeof(ParameterReflectionInfoTests), nameof(GenericHelper),
+ var parameters = ParameterMetadataFactory.ForGenericMethod(typeof(Helpers), nameof(Helpers.GenericHelper),
ParameterMetadataFactory.Create(typeof(object), "item", new ConcreteType(typeof(object)), false));
await Assert.That(parameters[0].ReflectionInfo.Name).IsEqualTo("item");
- await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(GenericHelper));
+ await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(Helpers.GenericHelper));
}
[Test]
@@ -95,15 +120,18 @@ public async Task Unresolvable_Member_Reports_Lookup_In_Exception()
await Assert.That(exception.Message).Contains(nameof(ParameterReflectionInfoTests));
}
- private static void StaticHelper(int value, string text)
+ public static class Helpers
{
- }
+ public static void StaticHelper(int value, string text)
+ {
+ }
- private static void GenericHelper(T item)
- {
+ public static void GenericHelper(T item)
+ {
+ }
}
- private sealed class GenericHolder
+ public sealed class GenericHolder
{
public GenericHolder(T value, int count)
{
From 0522fd8eb7c14a21bbd9b2e7607cf514775bf34d Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:29:24 +0100
Subject: [PATCH 2/7] test(aot): cover private DynamicallyAccessedMembers
helpers and internal test methods in the AOT NuGet tester
The NuGet tester is the only AOT publish that treats trim warnings as errors, but no
test class in it had a private helper with a [DynamicallyAccessedMembers] parameter,
so the 1.72.0 IL2111 regression passed CI. The new test fails against 1.72.0 and
passes with the fix. It also covers an internal test method whose parameters resolve
through ParameterMetadataFactory.ForMethodLookup.
---
.../TrimmedMemberRootingTests.cs | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
create mode 100644 tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
new file mode 100644
index 0000000000..f67f52b0e6
--- /dev/null
+++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace TUnit.NugetTester;
+
+///
+/// Regression tests for trimming roots added by generated test metadata.
+/// Generated code must not ask the trimmer to keep every non-public method of a test class. If it does, a
+/// private helper with a [DynamicallyAccessedMembers] parameter (like the one below) is reported as IL2111
+/// "accessed via reflection", which fails AOT publishes that treat warnings as errors (TUnit 1.72.0 regression).
+///
+public class TrimmedMemberRootingTests
+{
+ private int _field = 1;
+
+ [Test]
+ public async Task Private_Helper_With_Annotated_Parameter_Does_Not_Trigger_IL2111()
+ {
+ await Assert.That(CountInstanceFields(this, typeof(TrimmedMemberRootingTests))).IsGreaterThanOrEqualTo(_field);
+ }
+
+ [Test]
+ [Arguments(7, "seven")]
+ internal async Task Internal_Method_Parameters_Resolve(int value, string text)
+ {
+ var parameters = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters;
+
+ await Assert.That(parameters[0].ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(parameters[1].ReflectionInfo.Name).IsEqualTo(nameof(text));
+ await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Method_Parameters_Resolve));
+ }
+
+ private static int CountInstanceFields(object owner,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type ownerType)
+ {
+ return ownerType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Length + (owner is null ? -1 : 0);
+ }
+}
From 09c3b013ef091e871fbea1755883a017b1889186 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:44:52 +0100
Subject: [PATCH 3/7] fix(source-gen): root non-public generic test methods by
name and match overloads by signature
Review follow-up for the IL2111 fix:
- Non-public generic test methods used GetMethods(Public | NonPublic) plus a name filter, which
keeps every method of the class and brings IL2111 back. All non-public methods now go through
ForMethodLookup with a generated typeof(T).GetMethod(name, flags) call, a trimming intrinsic that
keeps only methods with that name.
- When overloads make that call ambiguous, the resolver chooses among them by static-ness, generic
arity and parameter shape instead of taking the first name and count match. Public generic methods
get the same matching through a new ForGenericMethod(type, name, isStatic, genericParameterCount,
...) overload; the existing overload is kept.
- ForMethod and ForGenericMethod fall back to non-public methods for direct callers when no public
method matches (untrimmed apps only).
- Tests: internal generic test method, an internal generic test overloaded by a non-generic method
of the same shape, overload selection through ForGenericMethod, the non-public fallback, and an
internal generic test in the AOT NuGet tester next to the annotated private helper.
---
.../Utilities/MetadataGenerationHelper.cs | 30 +--
src/TUnit.Core/ParameterMetadataFactory.cs | 173 +++++++++++++++---
.../GenericMethodTests.Test.verified.txt | 2 +-
...aSource_Should_Generate_Tests.verified.txt | 4 +-
.../Tests2136.Test.verified.txt | 2 +-
...Has_No_API_Changes.DotNet10_0.verified.txt | 3 +-
..._Has_No_API_Changes.DotNet8_0.verified.txt | 3 +-
..._Has_No_API_Changes.DotNet9_0.verified.txt | 3 +-
...ary_Has_No_API_Changes.Net4_7.verified.txt | 3 +-
.../ParameterReflectionInfoTests.cs | 67 +++++++
.../TrimmedMemberRootingTests.cs | 12 ++
11 files changed, 249 insertions(+), 53 deletions(-)
diff --git a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index c2323dfcbe..42b2991663 100644
--- a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -239,13 +239,18 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
else if (method.DeclaredAccessibility != Accessibility.Public)
{
// The factory's declaring-type annotation only keeps public methods, so trimming stays limited to what
- // ClassMetadata.Type already keeps. A non-public method is rooted with its own intrinsic lookup instead
- // of annotating the type with NonPublicMethods, which would keep (and warn about) every private helper.
- writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForMethodLookup(\"{method.Name}\", static () => {GenerateNonPublicMethodLookup(method, containingType, usesTypeParameters)}, ");
+ // ClassMetadata.Type already keeps. A non-public method is rooted by its own GetMethod(name, flags) call,
+ // an intrinsic that keeps only methods with that name, instead of annotating the type with
+ // NonPublicMethods, which would keep (and report IL2111 for) every private helper.
+ var isStatic = method.IsStatic.ToString().ToLowerInvariant();
+ var bindingFlags = method.IsStatic
+ ? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
+ : "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
+ writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForMethodLookup(typeof({containingType}), \"{method.Name}\", {isStatic}, {method.TypeParameters.Length}, static () => typeof({containingType}).GetMethod(\"{method.Name}\", {bindingFlags}), ");
}
else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
- writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof({containingType}), \"{method.Name}\", ");
+ writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof({containingType}), \"{method.Name}\", {method.IsStatic.ToString().ToLowerInvariant()}, {method.TypeParameters.Length}, ");
}
else
{
@@ -253,23 +258,6 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
}
}
- private static string GenerateNonPublicMethodLookup(IMethodSymbol method, string containingType, bool usesTypeParameters)
- {
- if (method.TypeParameters.Length > 0 || usesTypeParameters)
- {
- return $"global::System.Linq.Enumerable.FirstOrDefault(typeof({containingType}).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Static), m => m.Name == \"{method.Name}\" && m.GetParameters().Length == {method.Parameters.Length})";
- }
-
- var bindingFlags = method.IsStatic
- ? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
- : "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
- var parameterTypes = method.Parameters.Length == 0
- ? "global::System.Type.EmptyTypes"
- : $"new global::System.Type[] {{ {string.Join(", ", method.Parameters.Select(p => $"typeof({p.Type.GloballyQualified()})"))} }}";
-
- return $"typeof({containingType}).GetMethod(\"{method.Name}\", {bindingFlags}, null, {parameterTypes}, null)";
- }
-
///
/// Generates code for creating a PropertyMetadata instance
///
diff --git a/src/TUnit.Core/ParameterMetadataFactory.cs b/src/TUnit.Core/ParameterMetadataFactory.cs
index 2b353e9aa6..6715cca18f 100644
--- a/src/TUnit.Core/ParameterMetadataFactory.cs
+++ b/src/TUnit.Core/ParameterMetadataFactory.cs
@@ -16,7 +16,7 @@ public static class ParameterMetadataFactory
/// Deliberately limited to what already keeps for a test class. Requesting
/// non-public methods here would keep every private helper of the class and report IL2111 for any helper
/// with parameters. Non-public test methods go through
- /// , whose generated lookup roots only that method.
+ /// , whose generated lookup keeps only the methods with that name.
///
internal const DynamicallyAccessedMemberTypes DeclaringTypeMembers =
DynamicallyAccessedMemberTypes.PublicConstructors
@@ -45,7 +45,7 @@ public static ParameterMetadata Create(
///
/// Attaches lazy resolution to the parameters of a
- /// non-generic method. The method is looked up once, on first access, by name and the parameters' types.
+ /// non-generic public method. The method is looked up once, on first access, by name and the parameters' types.
///
public static ParameterMetadata[] ForMethod(
[DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType,
@@ -59,8 +59,8 @@ public static ParameterMetadata[] ForMethod(
///
/// Attaches lazy resolution to the parameters of a
- /// generic method (or one whose parameters use type parameters). The method is looked up once, on first
- /// access, by name and parameter count; with same-arity overloads the first match reflection returns wins.
+ /// generic public method (or one whose parameters use type parameters). The method is looked up once, on first
+ /// access, by name and parameter shape; with overloads of the same shape the first match reflection returns wins.
///
public static ParameterMetadata[] ForGenericMethod(
[DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType,
@@ -68,20 +68,41 @@ public static ParameterMetadata[] ForGenericMethod(
params ParameterMetadata[] parameters)
{
return Attach(parameters, new ParameterInfoResolver(declaringType, methodName,
- ParameterInfoResolver.Kind.MethodByParameterCount, parameters));
+ ParameterInfoResolver.Kind.MethodByShape, parameters));
+ }
+
+ ///
+ /// Attaches lazy resolution to the parameters of a
+ /// generic public method (or one whose parameters use type parameters). The method is looked up once, on first
+ /// access, by name, static-ness, generic arity and parameter shape.
+ ///
+ public static ParameterMetadata[] ForGenericMethod(
+ [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType,
+ string methodName,
+ bool isStatic,
+ int genericParameterCount,
+ params ParameterMetadata[] parameters)
+ {
+ return Attach(parameters, new ParameterInfoResolver(declaringType, methodName,
+ ParameterInfoResolver.Kind.MethodByShape, parameters, isStatic, genericParameterCount));
}
///
/// Attaches lazy resolution to the parameters of a non-public
- /// method. runs once, on first access; the generator emits it as an intrinsic
- /// reflection call so trimming keeps exactly that method.
+ /// method. runs once, on first access. The generator emits it as
+ /// typeof(T).GetMethod(name, flags), which trimming treats as an intrinsic that keeps only the methods
+ /// with that name. When overloads make that call ambiguous, the method is chosen among them by static-ness,
+ /// generic arity and parameter shape.
///
public static ParameterMetadata[] ForMethodLookup(
+ Type declaringType,
string methodName,
- Func lookup,
+ bool isStatic,
+ int genericParameterCount,
+ Func lookup,
params ParameterMetadata[] parameters)
{
- return Attach(parameters, new ParameterInfoResolver(lookup, methodName, parameters));
+ return Attach(parameters, new ParameterInfoResolver(declaringType, methodName, isStatic, genericParameterCount, lookup, parameters));
}
///
@@ -125,19 +146,26 @@ internal enum Kind
{
InstanceMethod,
StaticMethod,
- MethodByParameterCount,
+ MethodByShape,
Constructor,
ConstructorByParameterCount,
Lookup,
}
private const BindingFlags PublicMethods = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
+ private const BindingFlags NonPublicMethods = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
[DynamicallyAccessedMembers(ParameterMetadataFactory.DeclaringTypeMembers)]
private readonly Type? _declaringType;
- private readonly Func? _lookup;
+
+ // Declaring type of a Kind.Lookup resolver. It carries no annotation: the generated lookup delegate is what
+ // keeps the method under trimming.
+ private readonly Type? _lookupType;
+ private readonly Func? _lookup;
private readonly string? _methodName;
private readonly Kind _kind;
+ private readonly bool? _isStatic;
+ private readonly int _genericParameterCount;
private readonly ParameterMetadata[] _parameters;
private ParameterInfo[]? _resolved;
@@ -145,19 +173,27 @@ public ParameterInfoResolver(
[DynamicallyAccessedMembers(ParameterMetadataFactory.DeclaringTypeMembers)] Type declaringType,
string? methodName,
Kind kind,
- ParameterMetadata[] parameters)
+ ParameterMetadata[] parameters,
+ bool? isStatic = null,
+ int genericParameterCount = -1)
{
_declaringType = declaringType;
_methodName = methodName;
_kind = kind;
_parameters = parameters;
+ _isStatic = isStatic;
+ _genericParameterCount = genericParameterCount;
}
- public ParameterInfoResolver(Func lookup, string methodName, ParameterMetadata[] parameters)
+ public ParameterInfoResolver(Type declaringType, string methodName, bool isStatic, int genericParameterCount,
+ Func lookup, ParameterMetadata[] parameters)
{
+ _lookupType = declaringType;
_lookup = lookup;
_methodName = methodName;
_kind = Kind.Lookup;
+ _isStatic = isStatic;
+ _genericParameterCount = genericParameterCount;
_parameters = parameters;
}
@@ -175,10 +211,10 @@ internal string Describe()
{
Kind.InstanceMethod => $"instance method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
Kind.StaticMethod => $"static method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter types",
- Kind.MethodByParameterCount => $"method '{_declaringType!.FullName}.{_methodName}' matched by parameter count ({_parameters.Length})",
+ Kind.MethodByShape => $"method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter shape",
Kind.Constructor => $"constructor of '{_declaringType!.FullName}' with {_parameters.Length} parameter(s) matched by parameter types",
Kind.ConstructorByParameterCount => $"constructor of '{_declaringType!.FullName}' matched by parameter count ({_parameters.Length})",
- _ => $"non-public method '{_methodName}' with {_parameters.Length} parameter(s)",
+ _ => $"{(_isStatic == true ? "static" : "instance")} method '{_lookupType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s)",
};
}
@@ -186,29 +222,118 @@ private ParameterInfo[] Resolve()
{
MethodBase? member = _kind switch
{
- Kind.InstanceMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Instance, null, GetParameterTypes(), null),
- Kind.StaticMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Static, null, GetParameterTypes(), null),
- Kind.MethodByParameterCount => FindByParameterCount(_declaringType!.GetMethods(PublicMethods)),
+ Kind.InstanceMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Instance, null, GetParameterTypes(), null)
+ ?? FindNonPublicMethod(),
+ Kind.StaticMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.Public | BindingFlags.Static, null, GetParameterTypes(), null)
+ ?? FindNonPublicMethod(),
+ Kind.MethodByShape => FindBestMatch(_declaringType!.GetMethods(PublicMethods)) ?? FindNonPublicMethod(),
Kind.Constructor => _declaringType!.GetConstructor(GetParameterTypes()),
- Kind.ConstructorByParameterCount => FindByParameterCount(_declaringType!.GetConstructors()),
- _ => _lookup!(),
+ Kind.ConstructorByParameterCount => FindBestMatch(_declaringType!.GetConstructors()),
+ _ => ResolveLookup(),
};
return member?.GetParameters() ?? [];
}
- private MethodBase? FindByParameterCount(MethodBase[] candidates)
+ private MethodBase? ResolveLookup()
+ {
+ try
+ {
+ return _lookup!();
+ }
+ catch (AmbiguousMatchException)
+ {
+ return FindAmongOverloads();
+ }
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2080",
+ Justification = "Only reached when the generated typeof(T).GetMethod(name, flags) lookup is ambiguous. That " +
+ "intrinsic call keeps every method with this name, and those are the only candidates matched here.")]
+ private MethodBase? FindAmongOverloads()
{
+ var flags = BindingFlags.Public | BindingFlags.NonPublic | (_isStatic == true ? BindingFlags.Static : BindingFlags.Instance);
+ return FindBestMatch(_lookupType!.GetMethods(flags));
+ }
+
+ ///
+ /// Generated code routes non-public methods through ,
+ /// so this only serves direct callers of the public-method factories. Trimming does not keep non-public
+ /// methods for them, so in a trimmed app this finds nothing and ReflectionInfo reports the failed lookup.
+ ///
+ [UnconditionalSuppressMessage("Trimming", "IL2080",
+ Justification = "Best-effort fallback for direct callers; trimmed apps get a descriptive lookup failure instead.")]
+ private MethodBase? FindNonPublicMethod()
+ {
+ return _kind switch
+ {
+ Kind.InstanceMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.NonPublic | BindingFlags.Instance, null, GetParameterTypes(), null),
+ Kind.StaticMethod => _declaringType!.GetMethod(_methodName!, BindingFlags.NonPublic | BindingFlags.Static, null, GetParameterTypes(), null),
+ _ => FindBestMatch(_declaringType!.GetMethods(NonPublicMethods)),
+ };
+ }
+
+ ///
+ /// Prefers a candidate whose static-ness, generic arity and parameter shape match. Otherwise falls back to the
+ /// first candidate with the right name and parameter count, which was the only rule before shape matching.
+ ///
+ private MethodBase? FindBestMatch(MethodBase[] candidates)
+ {
+ MethodBase? byCount = null;
+
foreach (var candidate in candidates)
{
- if ((_methodName is null || candidate.Name == _methodName)
- && candidate.GetParameters().Length == _parameters.Length)
+ if (_methodName is not null && candidate.Name != _methodName)
+ {
+ continue;
+ }
+
+ var candidateParameters = candidate.GetParameters();
+ if (candidateParameters.Length != _parameters.Length)
+ {
+ continue;
+ }
+
+ if (MatchesSignature(candidate, candidateParameters))
{
return candidate;
}
+
+ byCount ??= candidate;
+ }
+
+ return byCount;
+ }
+
+ private bool MatchesSignature(MethodBase candidate, ParameterInfo[] candidateParameters)
+ {
+ if (_isStatic is { } isStatic && candidate.IsStatic != isStatic)
+ {
+ return false;
+ }
+
+ if (_genericParameterCount >= 0
+ && (candidate.IsGenericMethodDefinition ? candidate.GetGenericArguments().Length : 0) != _genericParameterCount)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < candidateParameters.Length; i++)
+ {
+ var parameterType = candidateParameters[i].ParameterType;
+ if (parameterType.IsByRef)
+ {
+ parameterType = parameterType.GetElementType()!;
+ }
+
+ // Parameters that use type parameters are emitted as object, so only concrete types can be compared.
+ if (!parameterType.ContainsGenericParameters && parameterType != _parameters[i].Type)
+ {
+ return false;
+ }
}
- return null;
+ return true;
}
private Type[] GetParameterTypes()
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodTests.Test.verified.txt
index 21de9b3cc5..faf49ed8b3 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodTests.Test.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodTests.Test.verified.txt
@@ -19,7 +19,7 @@ internal static class TUnit_TestProject_GenericMethodTests_AggregateBy_HasExpect
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.GenericMethodTests);
- private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("AggregateBy_HasExpectedOutput", __classType, typeof(void), __classMetadata, genericTypeCount: 3, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.GenericMethodTests), "AggregateBy_HasExpectedOutput", new global::TUnit.Core.ParameterMetadata[]
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("AggregateBy_HasExpectedOutput", __classType, typeof(void), __classMetadata, genericTypeCount: 3, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.GenericMethodTests), "AggregateBy_HasExpectedOutput", false, 3, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "source", new global::TUnit.Core.ConstructedGeneric(typeof(global::System.Collections.Generic.IEnumerable<>), [new global::TUnit.Core.GenericParameter(0, true, "TSource")]), false),
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "keySelector", new global::TUnit.Core.ConstructedGeneric(typeof(global::System.Func<,>), [new global::TUnit.Core.GenericParameter(0, true, "TSource"), new global::TUnit.Core.GenericParameter(1, true, "TKey")]), false),
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodWithDataSourceTests.Generic_Method_With_MethodDataSource_Should_Generate_Tests.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodWithDataSourceTests.Generic_Method_With_MethodDataSource_Should_Generate_Tests.verified.txt
index e3a05db254..32489318e3 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodWithDataSourceTests.Generic_Method_With_MethodDataSource_Should_Generate_Tests.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/GenericMethodWithDataSourceTests.Generic_Method_With_MethodDataSource_Should_Generate_Tests.verified.txt
@@ -419,7 +419,7 @@ internal static class TUnit_TestProject_Bugs__4431_NonGenericClassWithGenericMet
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._4431.NonGenericClassWithGenericMethodAndDataSource);
- private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("GenericMethod_With_DataSource", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._4431.NonGenericClassWithGenericMethodAndDataSource), "GenericMethod_With_DataSource", new global::TUnit.Core.ParameterMetadata[]
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("GenericMethod_With_DataSource", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._4431.NonGenericClassWithGenericMethodAndDataSource), "GenericMethod_With_DataSource", false, 1, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "input", new global::TUnit.Core.ConcreteType(typeof(string)), false)
}));
@@ -1253,7 +1253,7 @@ internal static class TUnit_TestProject_Bugs__4431_GenericClassGenericMethodWith
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._4431.GenericClassGenericMethodWithDataSources<>);
- private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("FullyGeneric_With_DataSources", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._4431.GenericClassGenericMethodWithDataSources<>), "FullyGeneric_With_DataSources", new global::TUnit.Core.ParameterMetadata[]
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("FullyGeneric_With_DataSources", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._4431.GenericClassGenericMethodWithDataSources<>), "FullyGeneric_With_DataSources", false, 1, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(bool), "flag", new global::TUnit.Core.ConcreteType(typeof(bool)), false)
}));
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests2136.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests2136.Test.verified.txt
index 696699f70e..5abfef368d 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/Tests2136.Test.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests2136.Test.verified.txt
@@ -19,7 +19,7 @@ internal static class TUnit_TestProject_Bugs__2136_Tests_GenericArgumentsTest__T
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._2136.Tests);
- private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("GenericArgumentsTest", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._2136.Tests), "GenericArgumentsTest", new global::TUnit.Core.ParameterMetadata[]
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("GenericArgumentsTest", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof(global::TUnit.TestProject.Bugs._2136.Tests), "GenericArgumentsTest", false, 1, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new global::TUnit.Core.GenericParameter(0, true, "T"), false),
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "expected", new global::TUnit.Core.ConcreteType(typeof(string)), false)
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index fc421d104e..a8bcec2789 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1142,8 +1142,9 @@ namespace
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 4cd9a171cc..6ff521dd8c 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1142,8 +1142,9 @@ namespace
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index b24080d8b6..b5080cbb59 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1142,8 +1142,9 @@ namespace
public static .ParameterMetadata Create([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
public static .ParameterMetadata[] ForConstructor([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index 25a3b5b00c..9968666a19 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1102,8 +1102,9 @@ namespace
public static .ParameterMetadata Create( type, string name, .TypeInfo typeInfo, bool isNullable, <.ParameterInfo>? reflectionInfoFactory = null) { }
public static .ParameterMetadata[] ForConstructor( declaringType, bool matchByParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod( declaringType, string methodName, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForGenericMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod( declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup(string methodName, <.MethodBase?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
index f2b22304b4..94069d310d 100644
--- a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
+++ b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
@@ -85,6 +85,28 @@ public async Task Static_Method_Via_Factory()
await Assert.That(parameters[1].ReflectionInfo.Member.Name).IsEqualTo(nameof(Helpers.StaticHelper));
}
+ [Test]
+ public async Task Generic_Method_Overload_Selected_By_Arity_And_Shape()
+ {
+ var parameters = ParameterMetadataFactory.ForGenericMethod(typeof(Helpers), nameof(Helpers.Pick), true, 1,
+ ParameterMetadataFactory.Create(typeof(object), "item", new ConcreteType(typeof(object)), false));
+
+ await Assert.That(((MethodInfo) parameters[0].ReflectionInfo.Member).IsGenericMethodDefinition).IsTrue();
+ }
+
+ [Test]
+ public async Task Non_Public_Method_Via_Public_Factory_Falls_Back()
+ {
+ var parameters = ParameterMetadataFactory.ForMethod(typeof(ParameterReflectionInfoTests), nameof(PrivateStaticHelper), true,
+ ParameterMetadataFactory.Create(typeof(int), "value", new ConcreteType(typeof(int)), false));
+
+ await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(PrivateStaticHelper));
+ }
+
+ private static void PrivateStaticHelper(int value)
+ {
+ }
+
[Test]
public async Task Open_Generic_Constructor_Matched_By_Parameter_Count()
{
@@ -129,6 +151,15 @@ public static void StaticHelper(int value, string text)
public static void GenericHelper(T item)
{
}
+
+ // Declared before the generic overload so the first same-count match reflection returns is the wrong one.
+ public static void Pick(object item)
+ {
+ }
+
+ public static void Pick(T item)
+ {
+ }
}
public sealed class GenericHolder
@@ -146,6 +177,42 @@ internal static async Task AssertParameter(ParameterMetadata parameter, string n
await Assert.That(info.ParameterType).IsEqualTo(type);
}
+ ///
+ /// Non-public generic test methods. Kept apart from the outer class, whose constructor arguments are not
+ /// supported together with generic test methods.
+ ///
+ [EngineTest(ExpectedResult.Pass)]
+ public class NonPublicGeneric
+ {
+ [Test]
+ [Arguments(5)]
+ internal async Task Internal_Generic_Method(T value)
+ {
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(parameter.ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Generic_Method));
+ await Assert.That(((MethodInfo) parameter.ReflectionInfo.Member).IsGenericMethodDefinition).IsTrue();
+ }
+
+ // Same name, parameter count and (erased) parameter shape as the generic test below, declared first, so
+ // only generic arity can tell them apart.
+ private void Internal_Overloaded(object value)
+ {
+ }
+
+ [Test]
+ [Arguments(5)]
+ internal async Task Internal_Overloaded(T value)
+ {
+ // The non-generic overload above makes the generated GetMethod(name, flags) lookup ambiguous, so the
+ // resolver must pick this method among the same-name overloads by generic arity.
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(((MethodInfo) parameter.ReflectionInfo.Member).IsGenericMethodDefinition).IsTrue();
+ }
+ }
+
[EngineTest(ExpectedResult.Pass)]
public class Nested
{
diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
index f67f52b0e6..7918059304 100644
--- a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
+++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
@@ -30,6 +30,18 @@ internal async Task Internal_Method_Parameters_Resolve(int value, string text)
await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Method_Parameters_Resolve));
}
+ [Test]
+ [Arguments(3)]
+ internal async Task Internal_Generic_Method_Parameters_Resolve(T value)
+ {
+ // Generic non-public methods must also be rooted by name only; enumerating the class's methods would keep
+ // CountInstanceFields and bring IL2111 back.
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(parameter.ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Generic_Method_Parameters_Resolve));
+ }
+
private static int CountInstanceFields(object owner,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type ownerType)
{
From 61b5f04f767916e8edcdc647ecbf6705efe09a1e Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:04:52 +0100
Subject: [PATCH 4/7] fix(source-gen): root non-public test methods by exact
signature with DynamicDependency
Every GetMethod overload is narrowed by trimming on the name only (the Type[] argument is
ignored), so the generated GetMethod(name, flags) root still kept same-name private overloads and
reported IL2111 for one with a [DynamicallyAccessedMembers] parameter.
Non-public test methods now go through ParameterMetadataFactory.ForNonPublicMethod (replacing the
unreleased ForMethodLookup) with a no-op delegate carrying [DynamicDependency("",
typeof(T))], emitted under #if NET5_0_OR_GREATER. That keeps exactly the test method. The resolver
looks it up itself and matches overloads by static-ness, generic arity and parameter shape.
Tests: generator snapshot for the emitted roots (non-generic, generic, and a name shared with an
annotated private overload), TestProject runtime coverage, and a same-name overload case in the
AOT NuGet tester. A trimmed publish of the tester shows no IL2111 for either helper, and the trimmed
app passes all four rooting tests.
---
.../Utilities/MetadataGenerationHelper.cs | 36 ++-
src/TUnit.Core/ParameterMetadataFactory.cs | 60 ++---
...icTestMethodRootingTests.Test.verified.txt | 223 ++++++++++++++++++
.../NonPublicTestMethodRootingTests.cs | 19 ++
...Has_No_API_Changes.DotNet10_0.verified.txt | 2 +-
..._Has_No_API_Changes.DotNet8_0.verified.txt | 2 +-
..._Has_No_API_Changes.DotNet9_0.verified.txt | 2 +-
...ary_Has_No_API_Changes.Net4_7.verified.txt | 2 +-
.../NonPublicTestMethodRootingTests.cs | 52 ++++
.../TrimmedMemberRootingTests.cs | 18 ++
10 files changed, 368 insertions(+), 48 deletions(-)
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
create mode 100644 tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
diff --git a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index 42b2991663..5cb33fbec6 100644
--- a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -239,14 +239,16 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
else if (method.DeclaredAccessibility != Accessibility.Public)
{
// The factory's declaring-type annotation only keeps public methods, so trimming stays limited to what
- // ClassMetadata.Type already keeps. A non-public method is rooted by its own GetMethod(name, flags) call,
- // an intrinsic that keeps only methods with that name, instead of annotating the type with
- // NonPublicMethods, which would keep (and report IL2111 for) every private helper.
- var isStatic = method.IsStatic.ToString().ToLowerInvariant();
- var bindingFlags = method.IsStatic
- ? "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static"
- : "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance";
- writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForMethodLookup(typeof({containingType}), \"{method.Name}\", {isStatic}, {method.TypeParameters.Length}, static () => typeof({containingType}).GetMethod(\"{method.Name}\", {bindingFlags}), ");
+ // ClassMetadata.Type already keeps. A non-public method is kept instead by a no-op delegate carrying
+ // [DynamicDependency] with its exact signature. Annotating the type with NonPublicMethods would keep
+ // every private helper, and a GetMethod(name, ...) intrinsic would keep every same-name overload; both
+ // report IL2111 for helpers with [DynamicallyAccessedMembers] parameters. Trimming only matters on
+ // .NET 5+, where the attribute exists.
+ writer.AppendLine($"global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof({containingType}), \"{method.Name}\", {method.IsStatic.ToString().ToLowerInvariant()}, {method.TypeParameters.Length},");
+ writer.AppendLine("#if NET5_0_OR_GREATER");
+ writer.AppendLine($"[global::System.Diagnostics.CodeAnalysis.DynamicDependency(\"{GetDynamicDependencySignature(method)}\", typeof({containingType}))]");
+ writer.AppendLine("#endif");
+ writer.Append("static () => { }, ");
}
else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
@@ -258,6 +260,24 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
}
}
+ ///
+ /// Returns the method's signature in the documentation-comment ID form that
+ /// expects, e.g.
+ /// Run``1(``0,System.Int32): the member's doc ID without its "M:" prefix and declaring type.
+ ///
+ private static string GetDynamicDependencySignature(IMethodSymbol method)
+ {
+ var methodId = method.OriginalDefinition.GetDocumentationCommentId();
+ var typeId = method.ContainingType.OriginalDefinition.GetDocumentationCommentId();
+
+ if (methodId is null || typeId is null || !methodId.StartsWith("M:" + typeId.Substring(2) + ".", StringComparison.Ordinal))
+ {
+ return method.Name;
+ }
+
+ return methodId.Substring(typeId.Length + 1);
+ }
+
///
/// Generates code for creating a PropertyMetadata instance
///
diff --git a/src/TUnit.Core/ParameterMetadataFactory.cs b/src/TUnit.Core/ParameterMetadataFactory.cs
index 6715cca18f..70f9f943ca 100644
--- a/src/TUnit.Core/ParameterMetadataFactory.cs
+++ b/src/TUnit.Core/ParameterMetadataFactory.cs
@@ -16,7 +16,7 @@ public static class ParameterMetadataFactory
/// Deliberately limited to what already keeps for a test class. Requesting
/// non-public methods here would keep every private helper of the class and report IL2111 for any helper
/// with parameters. Non-public test methods go through
- /// , whose generated lookup keeps only the methods with that name.
+ /// , whose generated root keeps only that method.
///
internal const DynamicallyAccessedMemberTypes DeclaringTypeMembers =
DynamicallyAccessedMemberTypes.PublicConstructors
@@ -89,20 +89,22 @@ public static ParameterMetadata[] ForGenericMethod(
///
/// Attaches lazy resolution to the parameters of a non-public
- /// method. runs once, on first access. The generator emits it as
- /// typeof(T).GetMethod(name, flags), which trimming treats as an intrinsic that keeps only the methods
- /// with that name. When overloads make that call ambiguous, the method is chosen among them by static-ness,
- /// generic arity and parameter shape.
+ /// method. The method is looked up once, on first access, by name, static-ness, generic arity and parameter shape.
///
- public static ParameterMetadata[] ForMethodLookup(
+ ///
+ /// Never invoked. Generated code passes a no-op delegate carrying
+ /// with the method's exact signature, which is what keeps that one
+ /// method (and no other member) available to reflection under trimming.
+ ///
+ public static ParameterMetadata[] ForNonPublicMethod(
Type declaringType,
string methodName,
bool isStatic,
int genericParameterCount,
- Func lookup,
+ Action rootMethod,
params ParameterMetadata[] parameters)
{
- return Attach(parameters, new ParameterInfoResolver(declaringType, methodName, isStatic, genericParameterCount, lookup, parameters));
+ return Attach(parameters, new ParameterInfoResolver(declaringType, methodName, isStatic, genericParameterCount, parameters));
}
///
@@ -149,7 +151,7 @@ internal enum Kind
MethodByShape,
Constructor,
ConstructorByParameterCount,
- Lookup,
+ NonPublicMethod,
}
private const BindingFlags PublicMethods = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
@@ -158,10 +160,9 @@ internal enum Kind
[DynamicallyAccessedMembers(ParameterMetadataFactory.DeclaringTypeMembers)]
private readonly Type? _declaringType;
- // Declaring type of a Kind.Lookup resolver. It carries no annotation: the generated lookup delegate is what
- // keeps the method under trimming.
- private readonly Type? _lookupType;
- private readonly Func? _lookup;
+ // Declaring type of a Kind.NonPublicMethod resolver. It carries no annotation: a generated
+ // [DynamicDependency] keeps the method under trimming.
+ private readonly Type? _nonPublicDeclaringType;
private readonly string? _methodName;
private readonly Kind _kind;
private readonly bool? _isStatic;
@@ -186,12 +187,11 @@ public ParameterInfoResolver(
}
public ParameterInfoResolver(Type declaringType, string methodName, bool isStatic, int genericParameterCount,
- Func lookup, ParameterMetadata[] parameters)
+ ParameterMetadata[] parameters)
{
- _lookupType = declaringType;
- _lookup = lookup;
+ _nonPublicDeclaringType = declaringType;
_methodName = methodName;
- _kind = Kind.Lookup;
+ _kind = Kind.NonPublicMethod;
_isStatic = isStatic;
_genericParameterCount = genericParameterCount;
_parameters = parameters;
@@ -214,7 +214,7 @@ internal string Describe()
Kind.MethodByShape => $"method '{_declaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s) matched by parameter shape",
Kind.Constructor => $"constructor of '{_declaringType!.FullName}' with {_parameters.Length} parameter(s) matched by parameter types",
Kind.ConstructorByParameterCount => $"constructor of '{_declaringType!.FullName}' matched by parameter count ({_parameters.Length})",
- _ => $"{(_isStatic == true ? "static" : "instance")} method '{_lookupType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s)",
+ _ => $"{(_isStatic == true ? "static" : "instance")} method '{_nonPublicDeclaringType!.FullName}.{_methodName}' with {_parameters.Length} parameter(s)",
};
}
@@ -229,35 +229,23 @@ private ParameterInfo[] Resolve()
Kind.MethodByShape => FindBestMatch(_declaringType!.GetMethods(PublicMethods)) ?? FindNonPublicMethod(),
Kind.Constructor => _declaringType!.GetConstructor(GetParameterTypes()),
Kind.ConstructorByParameterCount => FindBestMatch(_declaringType!.GetConstructors()),
- _ => ResolveLookup(),
+ _ => FindDeclaredNonPublicMethod(),
};
return member?.GetParameters() ?? [];
}
- private MethodBase? ResolveLookup()
- {
- try
- {
- return _lookup!();
- }
- catch (AmbiguousMatchException)
- {
- return FindAmongOverloads();
- }
- }
-
[UnconditionalSuppressMessage("Trimming", "IL2080",
- Justification = "Only reached when the generated typeof(T).GetMethod(name, flags) lookup is ambiguous. That " +
- "intrinsic call keeps every method with this name, and those are the only candidates matched here.")]
- private MethodBase? FindAmongOverloads()
+ Justification = "Generated code roots the method with [DynamicDependency] on the delegate passed to " +
+ "ForNonPublicMethod. Methods trimming removed are simply not candidates.")]
+ private MethodBase? FindDeclaredNonPublicMethod()
{
var flags = BindingFlags.Public | BindingFlags.NonPublic | (_isStatic == true ? BindingFlags.Static : BindingFlags.Instance);
- return FindBestMatch(_lookupType!.GetMethods(flags));
+ return FindBestMatch(_nonPublicDeclaringType!.GetMethods(flags));
}
///
- /// Generated code routes non-public methods through ,
+ /// Generated code routes non-public methods through ,
/// so this only serves direct callers of the public-method factories. Trimming does not keep non-public
/// methods for them, so in a trimmed app this finds nothing and ReflectionInfo reports the failed lookup.
///
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
new file mode 100644
index 0000000000..8652b13201
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
@@ -0,0 +1,223 @@
+//
+#pragma warning disable
+
+#nullable enable
+namespace TUnit.Generated;
+[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]
+[global::System.CodeDom.Compiler.GeneratedCode("TUnit", "VERSION_SCRUBBED")]
+internal static class TUnit_TestProject_NonPublicTestMethodRootingTests_Internal_Generic__T_TestSource
+{
+ private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.NonPublicTestMethodRootingTests", new global::TUnit.Core.ClassMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests),
+ TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests)),
+ Name = "NonPublicTestMethodRootingTests",
+ Namespace = "TUnit.TestProject",
+ Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"),
+ Parameters = global::System.Array.Empty(),
+ Properties = global::System.Array.Empty(),
+ Parent = null
+ });
+ private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests);
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Internal_Generic", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Generic", false, 1,
+#if NET5_0_OR_GREATER
+[global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Generic``1(``0)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
+#endif
+static () => { }, new global::TUnit.Core.ParameterMetadata[]
+{
+global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new global::TUnit.Core.GenericParameter(0, true, "T"), false)
+}));
+ private static global::TUnit.TestProject.NonPublicTestMethodRootingTests __CreateInstance_0(global::System.Type[] typeArgs, object?[] args)
+ {
+ return new global::TUnit.TestProject.NonPublicTestMethodRootingTests();
+ }
+ private static global::System.Threading.Tasks.ValueTask __Invoke_0(global::TUnit.TestProject.NonPublicTestMethodRootingTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
+ {
+ try
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Internal_Generic(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ catch (global::System.Exception ex)
+ {
+ return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
+ }
+ }
+ private static global::System.Attribute[] __Attributes_0(int groupIndex)
+ {
+ return new global::System.Attribute[]
+ {
+ new global::TUnit.Core.TestAttribute(),
+ new global::TUnit.Core.ArgumentsAttribute(2),
+ new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
+ };
+ }
+ public static readonly global::TUnit.Core.TestEntry[] Entries_0 = new global::TUnit.Core.TestEntry[]
+ {
+ global::TUnit.Core.TestEntryFactory.Create(
+ methodName: "Internal_Generic",
+ fullyQualifiedName: "TUnit.TestProject.NonPublicTestMethodRootingTests.Internal_Generic",
+ filePath: "",
+ lineNumber: 26,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.ArgumentsAttribute(2),
+},
+ methodMetadata: __mm_0,
+ createInstance: __CreateInstance_0,
+ invokeBody: __Invoke_0,
+ methodIndex: 0,
+ createAttributes: __Attributes_0,
+ attributeGroupIndex: 0),
+ };
+}
+internal static partial class TUnit_TestRegistration
+{
+ static readonly int _r_TUnit_TestProject_NonPublicTestMethodRootingTests_Internal_Generic__T_TestSource_0 = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_NonPublicTestMethodRootingTests_Internal_Generic__T_TestSource.Entries_0);
+}
+
+
+// ===== FILE SEPARATOR =====
+
+//
+#pragma warning disable
+
+#nullable enable
+namespace TUnit.Generated;
+[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]
+[global::System.CodeDom.Compiler.GeneratedCode("TUnit", "VERSION_SCRUBBED")]
+internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSource
+{
+ private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.NonPublicTestMethodRootingTests", new global::TUnit.Core.ClassMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests),
+ TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests)),
+ Name = "NonPublicTestMethodRootingTests",
+ Namespace = "TUnit.TestProject",
+ Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"),
+ Parameters = global::System.Array.Empty(),
+ Properties = global::System.Array.Empty(),
+ Parent = null
+ });
+ private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests);
+ private static global::TUnit.TestProject.NonPublicTestMethodRootingTests __CreateInstance(global::System.Type[] typeArgs, object?[] args)
+ {
+ return new global::TUnit.TestProject.NonPublicTestMethodRootingTests();
+ }
+ private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.NonPublicTestMethodRootingTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken)
+ {
+ try
+ {
+ switch (methodIndex)
+ {
+ case 0:
+ {
+ switch (args.Length)
+ {
+ case 2:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Internal_Method(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::TUnit.Core.Helpers.CastHelper.Cast(args[1])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}");
+ }
+ }
+ case 1:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Internal_Shares_Name(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
+ default:
+ throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex));
+ }
+ }
+ catch (global::System.Exception ex)
+ {
+ return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex));
+ }
+ }
+ private static global::System.Attribute[] __Attributes(int groupIndex)
+ {
+ switch (groupIndex)
+ {
+ case 0:
+ {
+ return
+ [
+ new global::TUnit.Core.TestAttribute(),
+ new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
+ ];
+ }
+ default:
+ throw new global::System.ArgumentOutOfRangeException(nameof(groupIndex));
+ }
+ }
+ private static readonly global::System.Func __createInstance = __CreateInstance;
+ private static readonly global::System.Func __invoke = __Invoke;
+ private static readonly global::System.Func __attributes = __Attributes;
+ public static readonly global::TUnit.Core.TestEntry[] Entries = new global::TUnit.Core.TestEntry[]
+ {
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Internal_Method",
+ fullyQualifiedName: "TUnit.TestProject.NonPublicTestMethodRootingTests.Internal_Method",
+ filePath: "",
+ lineNumber: 15,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.ArgumentsAttribute(1, "one"),
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Method", false, 0,
+ #if NET5_0_OR_GREATER
+ [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Method(System.Int32,System.String)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
+ #endif
+ static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false),
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "text", new global::TUnit.Core.ConcreteType(typeof(string)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 0,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Internal_Shares_Name",
+ fullyQualifiedName: "TUnit.TestProject.NonPublicTestMethodRootingTests.Internal_Shares_Name",
+ filePath: "",
+ lineNumber: 36,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.ArgumentsAttribute(3),
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Shares_Name", false, 0,
+ #if NET5_0_OR_GREATER
+ [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Shares_Name(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
+ #endif
+ static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 1,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
+ };
+}
+internal static partial class TUnit_TestRegistration
+{
+ static readonly int _r_TUnit_TestProject_NonPublicTestMethodRootingTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_NonPublicTestMethodRootingTests__TestSource.Entries);
+}
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
new file mode 100644
index 0000000000..ccdf893751
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
@@ -0,0 +1,19 @@
+namespace TUnit.Core.SourceGenerator.Tests;
+
+internal class NonPublicTestMethodRootingTests : TestsBase
+{
+ [Test]
+ public Task Test() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
+ "TUnit.TestProject",
+ "NonPublicTestMethodRootingTests.cs"),
+ async generatedFiles =>
+ {
+ var generated = string.Join(Environment.NewLine, generatedFiles);
+
+ // Each non-public test method is rooted by its exact signature, never by name or by type annotation.
+ await Assert.That(generated).Contains("DynamicDependency(\"Internal_Method(System.Int32,System.String)\"");
+ await Assert.That(generated).Contains("DynamicDependency(\"Internal_Generic``1(``0)\"");
+ await Assert.That(generated).Contains("DynamicDependency(\"Internal_Shares_Name(System.Int32)\"");
+ await Assert.That(generated).DoesNotContain(".GetMethod(\"Internal_");
+ });
+}
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index a8bcec2789..949cff5cd6 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1144,7 +1144,7 @@ namespace
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForNonPublicMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, rootMethod, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 6ff521dd8c..51b2ad3ffe 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1144,7 +1144,7 @@ namespace
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForNonPublicMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, rootMethod, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index b5080cbb59..20af9adc5f 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1144,7 +1144,7 @@ namespace
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods)] declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForNonPublicMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, rootMethod, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicProperties)] T> : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
index 9968666a19..bbffd7c223 100644
--- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1104,7 +1104,7 @@ namespace
public static .ParameterMetadata[] ForGenericMethod( declaringType, string methodName, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForGenericMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, params .ParameterMetadata[] parameters) { }
public static .ParameterMetadata[] ForMethod( declaringType, string methodName, bool isStatic, params .ParameterMetadata[] parameters) { }
- public static .ParameterMetadata[] ForMethodLookup( declaringType, string methodName, bool isStatic, int genericParameterCount, <.MethodInfo?> lookup, params .ParameterMetadata[] parameters) { }
+ public static .ParameterMetadata[] ForNonPublicMethod( declaringType, string methodName, bool isStatic, int genericParameterCount, rootMethod, params .ParameterMetadata[] parameters) { }
}
[.DebuggerDisplay("{Type} {Name}")]
public class ParameterMetadata : .ParameterMetadata, <.ParameterMetadata>
diff --git a/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs b/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
new file mode 100644
index 0000000000..cd935b5c04
--- /dev/null
+++ b/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
@@ -0,0 +1,52 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject;
+
+///
+/// Non-public test methods whose parameters resolve through ParameterMetadataFactory.ForNonPublicMethod.
+/// The generator roots each one with [DynamicDependency] and its exact signature, so a private overload with the
+/// same name (here one with a [DynamicallyAccessedMembers] parameter) is not kept by trimming.
+///
+[EngineTest(ExpectedResult.Pass)]
+public class NonPublicTestMethodRootingTests
+{
+ [Test]
+ [Arguments(1, "one")]
+ internal async Task Internal_Method(int value, string text)
+ {
+ var parameters = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters;
+
+ await Assert.That(parameters[0].ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(parameters[1].ReflectionInfo.Name).IsEqualTo(nameof(text));
+ await Assert.That(parameters[0].ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Method));
+ }
+
+ [Test]
+ [Arguments(2)]
+ internal async Task Internal_Generic(T value)
+ {
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(((MethodInfo) parameter.ReflectionInfo.Member).IsGenericMethodDefinition).IsTrue();
+ }
+
+ [Test]
+ [Arguments(3)]
+ internal async Task Internal_Shares_Name(int value)
+ {
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(parameter.ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Shares_Name));
+ await Assert.That(Internal_Shares_Name(typeof(NonPublicTestMethodRootingTests))).IsGreaterThanOrEqualTo(0);
+ }
+
+ private int Internal_Shares_Name(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type type)
+ {
+ return type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Length;
+ }
+}
diff --git a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
index 7918059304..a23838d997 100644
--- a/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
+++ b/tools/tunit-nuget-tester/TUnit.NugetTester/TUnit.NugetTester/TrimmedMemberRootingTests.cs
@@ -42,6 +42,24 @@ internal async Task Internal_Generic_Method_Parameters_Resolve(T value)
await Assert.That(parameter.ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Generic_Method_Parameters_Resolve));
}
+ [Test]
+ [Arguments(4)]
+ internal async Task Shares_Name_With_Annotated_Helper(int value)
+ {
+ // A private overload with the same name has a [DynamicallyAccessedMembers] parameter. Rooting by name
+ // (any GetMethod(name, ...) intrinsic) would keep it too and report IL2111.
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.Name).IsEqualTo(nameof(value));
+ await Assert.That(Shares_Name_With_Annotated_Helper(this, typeof(TrimmedMemberRootingTests))).IsGreaterThanOrEqualTo(_field);
+ }
+
+ private int Shares_Name_With_Annotated_Helper(object owner,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type ownerType)
+ {
+ return CountInstanceFields(owner, ownerType);
+ }
+
private static int CountInstanceFields(object owner,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type ownerType)
{
From be2a683c6fd59c18ff7f9ba39c0a8b22edc6548a Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:36:25 +0100
Subject: [PATCH 5/7] test: keep the private helper in the non-public fallback
test under Native AOT
The public-method factories deliberately do not keep non-public methods under trimming, so in
the Native AOT TestProject run the fallback found nothing and the test failed on all three OSes.
[DynamicDependency] keeps the helper, so the test covers the fallback lookup itself in every mode.
---
tests/TUnit.TestProject/ParameterReflectionInfoTests.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
index 94069d310d..31527065a2 100644
--- a/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
+++ b/tests/TUnit.TestProject/ParameterReflectionInfoTests.cs
@@ -95,6 +95,9 @@ public async Task Generic_Method_Overload_Selected_By_Arity_And_Shape()
}
[Test]
+ // Trimming does not keep non-public methods for the public-method factories (by design), so this keeps the
+ // helper explicitly; the test covers the fallback lookup itself, which then works in JIT and Native AOT alike.
+ [DynamicDependency(nameof(PrivateStaticHelper))]
public async Task Non_Public_Method_Via_Public_Factory_Falls_Back()
{
var parameters = ParameterMetadataFactory.ForMethod(typeof(ParameterReflectionInfoTests), nameof(PrivateStaticHelper), true,
From 8fc39670c02f7aefa752637b0efb3fbf41cc844d Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 21:03:39 +0100
Subject: [PATCH 6/7] fix: never treat a by-ref overload as an exact
parameter-shape match
Stripping IsByRef made Test(int) and a same-name Test(ref int) helper look identical, so the
resolver could bind ReflectionInfo to the helper when reflection listed it first. Generated
metadata never describes by-ref parameters, so a by-ref candidate now only qualifies through the
parameter-count fallback. The new Internal_Ref_Overload test fails without this change.
---
src/TUnit.Core/ParameterMetadataFactory.cs | 5 ++-
...icTestMethodRootingTests.Test.verified.txt | 37 +++++++++++++++++++
.../NonPublicTestMethodRootingTests.cs | 1 +
.../NonPublicTestMethodRootingTests.cs | 17 +++++++++
4 files changed, 59 insertions(+), 1 deletion(-)
diff --git a/src/TUnit.Core/ParameterMetadataFactory.cs b/src/TUnit.Core/ParameterMetadataFactory.cs
index 70f9f943ca..ccb571472d 100644
--- a/src/TUnit.Core/ParameterMetadataFactory.cs
+++ b/src/TUnit.Core/ParameterMetadataFactory.cs
@@ -309,9 +309,12 @@ private bool MatchesSignature(MethodBase candidate, ParameterInfo[] candidatePar
for (var i = 0; i < candidateParameters.Length; i++)
{
var parameterType = candidateParameters[i].ParameterType;
+
+ // Generated metadata never describes a by-ref parameter, so a ref/in/out overload of a test method is
+ // never an exact match (it can still be picked by the parameter-count fallback if nothing else fits).
if (parameterType.IsByRef)
{
- parameterType = parameterType.GetElementType()!;
+ return false;
}
// Parameters that use type parameters are emitted as object, so only concrete types can be compared.
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
index 8652b13201..1a5478560e 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
@@ -134,6 +134,18 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSou
throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
}
}
+ case 2:
+ {
+ switch (args.Length)
+ {
+ case 1:
+ {
+ return new global::System.Threading.Tasks.ValueTask(instance.Internal_Ref_Overload(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])));
+ }
+ default:
+ throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}");
+ }
+ }
default:
throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex));
}
@@ -215,6 +227,31 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSou
methodIndex: 1,
createAttributes: __attributes,
attributeGroupIndex: 0),
+ global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata(
+ methodName: "Internal_Ref_Overload",
+ fullyQualifiedName: "TUnit.TestProject.NonPublicTestMethodRootingTests.Internal_Ref_Overload",
+ filePath: "",
+ lineNumber: 54,
+ hasDataSource: true,
+ testDataSources: new global::TUnit.Core.IDataSourceAttribute[]
+{
+ new global::TUnit.Core.ArgumentsAttribute(4),
+},
+ returnType: typeof(global::System.Threading.Tasks.Task),
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Ref_Overload", false, 0,
+ #if NET5_0_OR_GREATER
+ [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Ref_Overload(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
+ #endif
+ static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ {
+ global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
+ }),
+ classMetadata: __classMetadata,
+ createInstance: __createInstance,
+ invokeBody: __invoke,
+ methodIndex: 2,
+ createAttributes: __attributes,
+ attributeGroupIndex: 0),
};
}
internal static partial class TUnit_TestRegistration
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
index ccdf893751..c41f2d04c0 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
+++ b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.cs
@@ -14,6 +14,7 @@ public Task Test() => RunTest(Path.Combine(Git.TestsDirectory.FullName,
await Assert.That(generated).Contains("DynamicDependency(\"Internal_Method(System.Int32,System.String)\"");
await Assert.That(generated).Contains("DynamicDependency(\"Internal_Generic``1(``0)\"");
await Assert.That(generated).Contains("DynamicDependency(\"Internal_Shares_Name(System.Int32)\"");
+ await Assert.That(generated).Contains("DynamicDependency(\"Internal_Ref_Overload(System.Int32)\"");
await Assert.That(generated).DoesNotContain(".GetMethod(\"Internal_");
});
}
diff --git a/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs b/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
index cd935b5c04..f6646e16a4 100644
--- a/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
+++ b/tests/TUnit.TestProject/NonPublicTestMethodRootingTests.cs
@@ -44,6 +44,23 @@ internal async Task Internal_Shares_Name(int value)
await Assert.That(Internal_Shares_Name(typeof(NonPublicTestMethodRootingTests))).IsGreaterThanOrEqualTo(0);
}
+ // Declared before the test below so reflection enumerates it first: the by-ref overload must not be mistaken
+ // for the test method just because its element type matches.
+ private void Internal_Ref_Overload(ref int value)
+ {
+ value++;
+ }
+
+ [Test]
+ [Arguments(4)]
+ internal async Task Internal_Ref_Overload(int value)
+ {
+ var parameter = TestContext.Current!.Metadata.TestDetails.MethodMetadata.Parameters[0];
+
+ await Assert.That(parameter.ReflectionInfo.ParameterType).IsEqualTo(typeof(int));
+ await Assert.That(parameter.ReflectionInfo.Member.Name).IsEqualTo(nameof(Internal_Ref_Overload));
+ }
+
private int Internal_Shares_Name(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] Type type)
{
From 4a32d8c4bd8a2c5ad95301efc67147478d9bb29d Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 29 Sep 2026 21:31:23 +0100
Subject: [PATCH 7/7] fix(source-gen): keep the DynamicDependency root in .NET
Standard test libraries
The root was emitted under #if NET5_0_OR_GREATER, which is evaluated when the test library
compiles. A netstandard2.0 test library consumed by a trimmed or Native AOT .NET 5+ app lost the
root, so ReflectionInfo for its non-public test methods failed at runtime.
The attribute is now always emitted. A new DynamicDependencyPolyfillGenerator declares an internal
System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute when the compilation has no single usable
one. The trimmer matches the attribute by name in any assembly. The detection logic (missing or
ambiguous attribute, [Embedded] polyfills, PolySharp, EnableTUnitPolyfills opt-out) moves from
ModuleInitializerPolyfillGenerator into a shared AttributePolyfillGenerator base.
Checked with a scratch netstandard2.0 library that declares the polyfill, consumed by a trimmed
.NET 10 app: with the root, only the targeted overload survives; without it, neither does.
---
.../Generators/AttributePolyfillGenerator.cs | 158 ++++++++++++++++++
.../DynamicDependencyPolyfillGenerator.cs | 63 +++++++
.../ModuleInitializerPolyfillGenerator.cs | 144 +---------------
.../Utilities/MetadataGenerationHelper.cs | 11 +-
...DynamicDependencyPolyfillGeneratorTests.cs | 117 +++++++++++++
...icTestMethodRootingTests.Test.verified.txt | 24 +--
6 files changed, 351 insertions(+), 166 deletions(-)
create mode 100644 src/TUnit.Core.SourceGenerator/Generators/AttributePolyfillGenerator.cs
create mode 100644 src/TUnit.Core.SourceGenerator/Generators/DynamicDependencyPolyfillGenerator.cs
create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/DynamicDependencyPolyfillGeneratorTests.cs
diff --git a/src/TUnit.Core.SourceGenerator/Generators/AttributePolyfillGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/AttributePolyfillGenerator.cs
new file mode 100644
index 0000000000..e795574e99
--- /dev/null
+++ b/src/TUnit.Core.SourceGenerator/Generators/AttributePolyfillGenerator.cs
@@ -0,0 +1,158 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace TUnit.Core.SourceGenerator.Generators;
+
+///
+/// Declares an attribute that generated code relies on as an internal type, but only when the project has no
+/// single usable declaration of it: it is missing (.NET Framework, .NET Standard) or declared by several
+/// references so that it is ambiguous.
+///
+///
+/// The existence check needs the compilation, so this cannot use RegisterPostInitializationOutput.
+/// Types from other source generators are invisible here, so PolySharp (the common generator-based
+/// polyfill) is detected through the build properties it makes visible to the compiler instead.
+/// Set EnableTUnitPolyfills to false to turn the fallback off.
+///
+public abstract class AttributePolyfillGenerator : IIncrementalGenerator
+{
+ /// The attribute's metadata name, e.g. System.Runtime.CompilerServices.ModuleInitializerAttribute.
+ protected abstract string AttributeMetadataName { get; }
+
+ protected abstract string HintName { get; }
+
+ protected abstract string Source { get; }
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var metadataName = AttributeMetadataName;
+ var hintName = HintName;
+ var source = Source;
+
+ var allowedProvider = context.AnalyzerConfigOptionsProvider
+ .Select((options, _) => IsAllowed(options.GlobalOptions, metadataName));
+
+ var attributeMissingProvider = context.CompilationProvider
+ .Select((compilation, _) => !HasAccessibleAttribute(compilation, metadataName));
+
+ context.RegisterSourceOutput(attributeMissingProvider.Combine(allowedProvider), (spc, data) =>
+ {
+ if (data.Left && data.Right)
+ {
+ spc.AddSource(hintName, SourceText.From(source, System.Text.Encoding.UTF8));
+ }
+ });
+ }
+
+ private static bool IsAllowed(AnalyzerConfigOptions options, string metadataName)
+ {
+ if (IsFalse(options, "build_property.EnableTUnitSourceGeneration")
+ || IsFalse(options, "build_property.EnableTUnitPolyfills"))
+ {
+ return false;
+ }
+
+ return !PolySharpProvidesAttribute(options, metadataName);
+ }
+
+ private static bool IsFalse(AnalyzerConfigOptions options, string key)
+ {
+ return options.TryGetValue(key, out var value)
+ && string.Equals(value.Trim(), "false", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // PolySharp marks its settings as compiler-visible, so the keys exist (possibly empty) whenever it is installed.
+ // It generates every polyfill that is missing unless the type is filtered out by these two lists.
+ private static bool PolySharpProvidesAttribute(AnalyzerConfigOptions options, string metadataName)
+ {
+ if (!options.TryGetValue("build_property.PolySharpIncludeGeneratedTypes", out var included))
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(included) && !ListContainsAttribute(included, metadataName))
+ {
+ return false;
+ }
+
+ return !options.TryGetValue("build_property.PolySharpExcludeGeneratedTypes", out var excluded)
+ || !ListContainsAttribute(excluded, metadataName);
+ }
+
+ private static bool ListContainsAttribute(string list, string metadataName)
+ {
+ foreach (var entry in list.Split([';', ','], StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (string.Equals(entry.Trim(), metadataName, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool HasAccessibleAttribute(Compilation compilation, string metadataName)
+ {
+ // GetTypeByMetadataName returns null when several references declare the type (common with
+ // polyfill packages), so inspect every candidate. The compiler ignores [Embedded] types from other
+ // assemblies, which is how Polyfill declares them, even when InternalsVisibleTo makes them accessible.
+ INamedTypeSymbol? accessible = null;
+ var accessibleCount = 0;
+
+ foreach (var type in compilation.GetTypesByMetadataName(metadataName))
+ {
+ if (compilation.IsSymbolAccessibleWithin(type, compilation.Assembly)
+ && !IsEmbeddedFromReference(type, compilation))
+ {
+ accessible = type;
+ accessibleCount++;
+ }
+ }
+
+ if (accessibleCount <= 1)
+ {
+ return accessible != null;
+ }
+
+ // Several accessible definitions are not always ambiguous (a declaration in source wins, for example).
+ // Only the compiler's own lookup gives the right answer, so bind the name the generated code uses and
+ // declare our own attribute only when that fails (CS0433). Ours is then in source, so it takes precedence
+ // over the referenced ones.
+ var tree = compilation.SyntaxTrees.FirstOrDefault();
+
+ if (tree is null)
+ {
+ // Speculative binding needs a tree, and other generators may still emit code that uses the attribute.
+ tree = CSharpSyntaxTree.ParseText(string.Empty);
+ compilation = compilation.AddSyntaxTrees(tree);
+ }
+
+ var typeInfo = compilation.GetSemanticModel(tree).GetSpeculativeTypeInfo(
+ 0,
+ SyntaxFactory.ParseTypeName("global::" + metadataName),
+ SpeculativeBindingOption.BindAsTypeOrNamespace);
+
+ return typeInfo.Type is { TypeKind: not TypeKind.Error };
+ }
+
+ private static bool IsEmbeddedFromReference(INamedTypeSymbol type, Compilation compilation)
+ {
+ if (SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly))
+ {
+ return false;
+ }
+
+ foreach (var attribute in type.GetAttributes())
+ {
+ if (attribute.AttributeClass is { Name: "EmbeddedAttribute", ContainingNamespace: { Name: "CodeAnalysis", ContainingNamespace: { Name: "Microsoft", ContainingNamespace.IsGlobalNamespace: true } } })
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/TUnit.Core.SourceGenerator/Generators/DynamicDependencyPolyfillGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/DynamicDependencyPolyfillGenerator.cs
new file mode 100644
index 0000000000..50e8acbdda
--- /dev/null
+++ b/src/TUnit.Core.SourceGenerator/Generators/DynamicDependencyPolyfillGenerator.cs
@@ -0,0 +1,63 @@
+using Microsoft.CodeAnalysis;
+
+namespace TUnit.Core.SourceGenerator.Generators;
+
+///
+/// Generated code keeps each non-public test method available to reflection under trimming with
+/// [DynamicDependency], but System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute only ships
+/// in .NET 5+. A .NET Standard test library can still end up in a trimmed or Native AOT application, and the
+/// trimmer recognises the attribute by name in any assembly, so this generator declares it as an internal type
+/// when nothing else provides it.
+///
+[Generator]
+public sealed class DynamicDependencyPolyfillGenerator : AttributePolyfillGenerator
+{
+ protected override string AttributeMetadataName => "System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute";
+
+ protected override string HintName => "TUnit.DynamicDependencyAttribute.g.cs";
+
+ protected override string Source =>
+ """
+ //
+ // Declared by TUnit because the project has no single usable DynamicDependencyAttribute: it is missing
+ // (.NET Framework, .NET Standard) or declared by several references. The trimmer matches the attribute
+ // by name, so this declaration works for trimmed and Native AOT applications. Set the
+ // EnableTUnitPolyfills MSBuild property to false to opt out.
+ #nullable enable
+ namespace System.Diagnostics.CodeAnalysis
+ {
+ [global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Field | global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal sealed class DynamicDependencyAttribute : global::System.Attribute
+ {
+ public DynamicDependencyAttribute(string memberSignature)
+ {
+ MemberSignature = memberSignature;
+ }
+
+ public DynamicDependencyAttribute(string memberSignature, global::System.Type type)
+ {
+ MemberSignature = memberSignature;
+ Type = type;
+ }
+
+ public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName)
+ {
+ MemberSignature = memberSignature;
+ TypeName = typeName;
+ AssemblyName = assemblyName;
+ }
+
+ public string? MemberSignature { get; }
+
+ public global::System.Type? Type { get; }
+
+ public string? TypeName { get; }
+
+ public string? AssemblyName { get; }
+
+ public string? Condition { get; set; }
+ }
+ }
+ """;
+}
diff --git a/src/TUnit.Core.SourceGenerator/Generators/ModuleInitializerPolyfillGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/ModuleInitializerPolyfillGenerator.cs
index 412dc1eb3a..06f53257ba 100644
--- a/src/TUnit.Core.SourceGenerator/Generators/ModuleInitializerPolyfillGenerator.cs
+++ b/src/TUnit.Core.SourceGenerator/Generators/ModuleInitializerPolyfillGenerator.cs
@@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis.Text;
namespace TUnit.Core.SourceGenerator.Generators;
@@ -12,18 +9,14 @@ namespace TUnit.Core.SourceGenerator.Generators;
/// or several references declare it so that it is ambiguous, this generator declares it as an internal
/// type so the generated code compiles without the project having to reference a polyfill package.
///
-///
-/// The existence check needs the compilation, so this cannot use RegisterPostInitializationOutput.
-/// Types from other source generators are invisible here, so PolySharp (the common generator-based
-/// polyfill) is detected through the build properties it makes visible to the compiler instead.
-/// Set EnableTUnitPolyfills to false to turn the fallback off.
-///
[Generator]
-public sealed class ModuleInitializerPolyfillGenerator : IIncrementalGenerator
+public sealed class ModuleInitializerPolyfillGenerator : AttributePolyfillGenerator
{
- private const string AttributeMetadataName = "System.Runtime.CompilerServices.ModuleInitializerAttribute";
+ protected override string AttributeMetadataName => "System.Runtime.CompilerServices.ModuleInitializerAttribute";
- private const string Source =
+ protected override string HintName => "TUnit.ModuleInitializerAttribute.g.cs";
+
+ protected override string Source =>
"""
//
// Declared by TUnit because the project has no single usable ModuleInitializerAttribute: it is missing
@@ -38,131 +31,4 @@ internal sealed class ModuleInitializerAttribute : global::System.Attribute
}
}
""";
-
- public void Initialize(IncrementalGeneratorInitializationContext context)
- {
- var allowedProvider = context.AnalyzerConfigOptionsProvider
- .Select(static (options, _) => IsAllowed(options.GlobalOptions));
-
- var attributeMissingProvider = context.CompilationProvider
- .Select(static (compilation, _) => !HasAccessibleAttribute(compilation));
-
- context.RegisterSourceOutput(attributeMissingProvider.Combine(allowedProvider), static (spc, data) =>
- {
- if (data.Left && data.Right)
- {
- spc.AddSource("TUnit.ModuleInitializerAttribute.g.cs", SourceText.From(Source, System.Text.Encoding.UTF8));
- }
- });
- }
-
- private static bool IsAllowed(AnalyzerConfigOptions options)
- {
- if (IsFalse(options, "build_property.EnableTUnitSourceGeneration")
- || IsFalse(options, "build_property.EnableTUnitPolyfills"))
- {
- return false;
- }
-
- return !PolySharpProvidesAttribute(options);
- }
-
- private static bool IsFalse(AnalyzerConfigOptions options, string key)
- {
- return options.TryGetValue(key, out var value)
- && string.Equals(value.Trim(), "false", StringComparison.OrdinalIgnoreCase);
- }
-
- // PolySharp marks its settings as compiler-visible, so the keys exist (possibly empty) whenever it is installed.
- // It generates every polyfill that is missing unless the type is filtered out by these two lists.
- private static bool PolySharpProvidesAttribute(AnalyzerConfigOptions options)
- {
- if (!options.TryGetValue("build_property.PolySharpIncludeGeneratedTypes", out var included))
- {
- return false;
- }
-
- if (!string.IsNullOrWhiteSpace(included) && !ListContainsAttribute(included))
- {
- return false;
- }
-
- return !options.TryGetValue("build_property.PolySharpExcludeGeneratedTypes", out var excluded)
- || !ListContainsAttribute(excluded);
- }
-
- private static bool ListContainsAttribute(string list)
- {
- foreach (var entry in list.Split([';', ','], StringSplitOptions.RemoveEmptyEntries))
- {
- if (string.Equals(entry.Trim(), AttributeMetadataName, StringComparison.Ordinal))
- {
- return true;
- }
- }
-
- return false;
- }
-
- private static bool HasAccessibleAttribute(Compilation compilation)
- {
- // GetTypeByMetadataName returns null when several references declare the type (common with
- // polyfill packages), so inspect every candidate. The compiler ignores [Embedded] types from other
- // assemblies, which is how Polyfill declares them, even when InternalsVisibleTo makes them accessible.
- INamedTypeSymbol? accessible = null;
- var accessibleCount = 0;
-
- foreach (var type in compilation.GetTypesByMetadataName(AttributeMetadataName))
- {
- if (compilation.IsSymbolAccessibleWithin(type, compilation.Assembly)
- && !IsEmbeddedFromReference(type, compilation))
- {
- accessible = type;
- accessibleCount++;
- }
- }
-
- if (accessibleCount <= 1)
- {
- return accessible != null;
- }
-
- // Several accessible definitions are not always ambiguous (a declaration in source wins, for example).
- // Only the compiler's own lookup gives the right answer, so bind the name the generated code uses and
- // declare our own attribute only when that fails (CS0433). Ours is then in source, so it takes precedence
- // over the referenced ones.
- var tree = compilation.SyntaxTrees.FirstOrDefault();
-
- if (tree is null)
- {
- // Speculative binding needs a tree, and other generators may still emit a module initializer.
- tree = CSharpSyntaxTree.ParseText(string.Empty);
- compilation = compilation.AddSyntaxTrees(tree);
- }
-
- var typeInfo = compilation.GetSemanticModel(tree).GetSpeculativeTypeInfo(
- 0,
- SyntaxFactory.ParseTypeName("global::" + AttributeMetadataName),
- SpeculativeBindingOption.BindAsTypeOrNamespace);
-
- return typeInfo.Type is { TypeKind: not TypeKind.Error };
- }
-
- private static bool IsEmbeddedFromReference(INamedTypeSymbol type, Compilation compilation)
- {
- if (SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly))
- {
- return false;
- }
-
- foreach (var attribute in type.GetAttributes())
- {
- if (attribute.AttributeClass is { Name: "EmbeddedAttribute", ContainingNamespace: { Name: "CodeAnalysis", ContainingNamespace: { Name: "Microsoft", ContainingNamespace.IsGlobalNamespace: true } } })
- {
- return true;
- }
- }
-
- return false;
- }
}
diff --git a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index 5cb33fbec6..4f68feb416 100644
--- a/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/src/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -242,13 +242,10 @@ private static void WriteReflectionInfoAttachStart(ICodeWriter writer, IMethodSy
// ClassMetadata.Type already keeps. A non-public method is kept instead by a no-op delegate carrying
// [DynamicDependency] with its exact signature. Annotating the type with NonPublicMethods would keep
// every private helper, and a GetMethod(name, ...) intrinsic would keep every same-name overload; both
- // report IL2111 for helpers with [DynamicallyAccessedMembers] parameters. Trimming only matters on
- // .NET 5+, where the attribute exists.
- writer.AppendLine($"global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof({containingType}), \"{method.Name}\", {method.IsStatic.ToString().ToLowerInvariant()}, {method.TypeParameters.Length},");
- writer.AppendLine("#if NET5_0_OR_GREATER");
- writer.AppendLine($"[global::System.Diagnostics.CodeAnalysis.DynamicDependency(\"{GetDynamicDependencySignature(method)}\", typeof({containingType}))]");
- writer.AppendLine("#endif");
- writer.Append("static () => { }, ");
+ // report IL2111 for helpers with [DynamicallyAccessedMembers] parameters. The attribute is emitted for every
+ // target: a .NET Standard test library can end up in a trimmed app, and DynamicDependencyPolyfillGenerator
+ // declares the attribute where the framework lacks it.
+ writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof({containingType}), \"{method.Name}\", {method.IsStatic.ToString().ToLowerInvariant()}, {method.TypeParameters.Length}, [global::System.Diagnostics.CodeAnalysis.DynamicDependency(\"{GetDynamicDependencySignature(method)}\", typeof({containingType}))] static () => {{ }}, ");
}
else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/DynamicDependencyPolyfillGeneratorTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/DynamicDependencyPolyfillGeneratorTests.cs
new file mode 100644
index 0000000000..8b6f5aa343
--- /dev/null
+++ b/tests/TUnit.Core.SourceGenerator.Tests/DynamicDependencyPolyfillGeneratorTests.cs
@@ -0,0 +1,117 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using TUnit.Core.SourceGenerator.CodeGenerators;
+using TUnit.Core.SourceGenerator.Generators;
+
+namespace TUnit.Core.SourceGenerator.Tests;
+
+///
+/// Generated code roots non-public test methods with [DynamicDependency], which .NET Framework and .NET Standard
+/// lack. A .NET Standard test library can still be trimmed as part of a .NET 5+ app, so
+/// declares the attribute when nothing else provides it.
+///
+internal class DynamicDependencyPolyfillGeneratorTests
+{
+ // The shape generated code uses: the attribute on a no-op lambda passed as an argument.
+ private const string DynamicDependencyUsage =
+ """
+ namespace MyTests;
+
+ internal class Tests
+ {
+ internal void Test(int value) { }
+
+ internal static System.Action Root() =>
+ [System.Diagnostics.CodeAnalysis.DynamicDependency("Test(System.Int32)", typeof(Tests))] static () => { };
+ }
+ """;
+
+ private const string UserDeclaredAttribute =
+ """
+ namespace System.Diagnostics.CodeAnalysis
+ {
+ internal sealed class DynamicDependencyAttribute : Attribute
+ {
+ public DynamicDependencyAttribute(string memberSignature, Type type) { }
+ }
+ }
+ """;
+
+ [Test]
+ public async Task Declares_attribute_when_compilation_lacks_it()
+ {
+ var generated = RunGenerator(CreateCompilation(references: []), buildProperties: null);
+
+ await Assert.That(generated).HasSingleItem();
+ await Assert.That(generated[0]).Contains("internal sealed class DynamicDependencyAttribute");
+ }
+
+ [Test]
+ [Arguments("EnableTUnitPolyfills", "false")]
+ [Arguments("EnableTUnitSourceGeneration", "false")]
+ [Arguments("PolySharpIncludeGeneratedTypes", "")]
+ public async Task Skipped_when_opted_out_or_PolySharp_provides_it(string property, string value)
+ {
+ var generated = RunGenerator(
+ CreateCompilation(references: []),
+ new Dictionary { [$"build_property.{property}"] = value });
+
+ await Assert.That(generated).IsEmpty();
+ }
+
+ [Test]
+ public async Task Skipped_when_project_declares_attribute()
+ {
+ var generated = RunGenerator(CreateCompilation(references: [], UserDeclaredAttribute), buildProperties: null);
+
+ await Assert.That(generated).IsEmpty();
+ }
+
+ [Test]
+ public async Task Generated_usage_compiles_on_current_target_framework()
+ {
+ // On .NET Framework the attribute is missing, so the generator must supply it; on .NET it exists and a
+ // second declaration would be ambiguous. Either way the generated usage must compile.
+ var parseOptions = new CSharpParseOptions(LanguageVersion.Preview);
+ var compilation = CSharpCompilation.Create(
+ "DynamicDependencyPolyfill",
+ [CSharpSyntaxTree.ParseText(DynamicDependencyUsage, parseOptions)],
+ ReferencesHelper.References,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(
+ [new DynamicDependencyPolyfillGenerator().AsSourceGenerator()], parseOptions: parseOptions);
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out _);
+
+ var errors = output.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Select(d => d.ToString());
+
+ await Assert.That(errors).IsEmpty();
+ }
+
+ private static CSharpCompilation CreateCompilation(MetadataReference[] references, params string[] sources)
+ {
+ return CSharpCompilation.Create(
+ "DynamicDependencyPolyfill",
+ sources.Select(s => CSharpSyntaxTree.ParseText(s)),
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+
+ private static string[] RunGenerator(Compilation compilation, Dictionary? buildProperties)
+ {
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new DynamicDependencyPolyfillGenerator());
+
+ if (buildProperties != null)
+ {
+ driver = driver.WithUpdatedAnalyzerConfigOptions(
+ new TestAnalyzerConfigOptionsProvider(buildProperties.ToImmutableDictionary()));
+ }
+
+ return driver.RunGenerators(compilation)
+ .GetRunResult()
+ .GeneratedTrees
+ .Select(t => t.GetText().ToString())
+ .ToArray();
+ }
+}
diff --git a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
index 1a5478560e..13dc51dece 100644
--- a/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
+++ b/tests/TUnit.Core.SourceGenerator.Tests/NonPublicTestMethodRootingTests.Test.verified.txt
@@ -19,11 +19,7 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests_Internal
Parent = null
});
private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests);
- private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Internal_Generic", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Generic", false, 1,
-#if NET5_0_OR_GREATER
-[global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Generic``1(``0)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
-#endif
-static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ private static readonly global::TUnit.Core.MethodMetadata __mm_0 = global::TUnit.Core.MethodMetadataFactory.Create("Internal_Generic", __classType, typeof(global::System.Threading.Tasks.Task), __classMetadata, genericTypeCount: 1, parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Generic", false, 1, [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Generic``1(``0)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))] static () => { }, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new global::TUnit.Core.GenericParameter(0, true, "T"), false)
}));
@@ -187,11 +183,7 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSou
new global::TUnit.Core.ArgumentsAttribute(1, "one"),
},
returnType: typeof(global::System.Threading.Tasks.Task),
- parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Method", false, 0,
- #if NET5_0_OR_GREATER
- [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Method(System.Int32,System.String)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
- #endif
- static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Method", false, 0, [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Method(System.Int32,System.String)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))] static () => { }, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false),
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(string), "text", new global::TUnit.Core.ConcreteType(typeof(string)), false)
@@ -213,11 +205,7 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSou
new global::TUnit.Core.ArgumentsAttribute(3),
},
returnType: typeof(global::System.Threading.Tasks.Task),
- parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Shares_Name", false, 0,
- #if NET5_0_OR_GREATER
- [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Shares_Name(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
- #endif
- static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Shares_Name", false, 0, [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Shares_Name(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))] static () => { }, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
}),
@@ -238,11 +226,7 @@ internal static class TUnit_TestProject_NonPublicTestMethodRootingTests__TestSou
new global::TUnit.Core.ArgumentsAttribute(4),
},
returnType: typeof(global::System.Threading.Tasks.Task),
- parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Ref_Overload", false, 0,
- #if NET5_0_OR_GREATER
- [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Ref_Overload(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))]
- #endif
- static () => { }, new global::TUnit.Core.ParameterMetadata[]
+ parameters: global::TUnit.Core.ParameterMetadataFactory.ForNonPublicMethod(typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests), "Internal_Ref_Overload", false, 0, [global::System.Diagnostics.CodeAnalysis.DynamicDependency("Internal_Ref_Overload(System.Int32)", typeof(global::TUnit.TestProject.NonPublicTestMethodRootingTests))] static () => { }, new global::TUnit.Core.ParameterMetadata[]
{
global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "value", new global::TUnit.Core.ConcreteType(typeof(int)), false)
}),