Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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)}, ");
Comment thread
thomhurst marked this conversation as resolved.
Outdated
}
else if (method.TypeParameters.Length > 0 || usesTypeParameters)
{
writer.Append($"global::TUnit.Core.ParameterMetadataFactory.ForGenericMethod(typeof({containingType}), \"{method.Name}\", ");
Expand All @@ -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})";
Comment thread
thomhurst marked this conversation as resolved.
Outdated
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}

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)";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/// <summary>
/// Generates code for creating a PropertyMetadata instance
/// </summary>
Expand Down
58 changes: 44 additions & 14 deletions src/TUnit.Core/ParameterMetadataFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ namespace TUnit.Core;
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ParameterMetadataFactory
{
/// <remarks>
/// Deliberately limited to what <see cref="ClassMetadata.Type"/> 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 <see cref="DynamicallyAccessedMembersAttribute"/> parameters. Non-public test methods go through
/// <see cref="ForMethodLookup"/>, whose generated lookup roots only that method.
/// </remarks>
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")]
Expand Down Expand Up @@ -66,6 +71,19 @@ public static ParameterMetadata[] ForGenericMethod(
ParameterInfoResolver.Kind.MethodByParameterCount, parameters));
}

/// <summary>
/// Attaches lazy <see cref="ParameterMetadata.ReflectionInfo"/> resolution to the parameters of a non-public
/// method. <paramref name="lookup"/> runs once, on first access; the generator emits it as an intrinsic
/// reflection call so trimming keeps exactly that method.
/// </summary>
public static ParameterMetadata[] ForMethodLookup(
string methodName,
Func<MethodBase?> lookup,
params ParameterMetadata[] parameters)
{
return Attach(parameters, new ParameterInfoResolver(lookup, methodName, parameters));
}

/// <summary>
/// Attaches lazy <see cref="ParameterMetadata.ReflectionInfo"/> resolution to the parameters of a public
/// constructor. When <paramref name="matchByParameterCount"/> is true the constructor is matched by
Expand Down Expand Up @@ -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<MethodBase?>? _lookup;
private readonly string? _methodName;
private readonly Kind _kind;
private readonly ParameterMetadata[] _parameters;
Expand All @@ -133,6 +153,14 @@ public ParameterInfoResolver(
_parameters = parameters;
}

public ParameterInfoResolver(Func<MethodBase?> 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
Expand All @@ -145,23 +173,25 @@ 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)",
};
}

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)),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Kind.Constructor => _declaringType!.GetConstructor(GetParameterTypes()),
Kind.ConstructorByParameterCount => FindByParameterCount(_declaringType!.GetConstructors()),
_ => _lookup!(),
};

return member?.GetParameters() ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : .ParameterMetadata, <.ParameterMetadata<T>>
Expand Down
Loading
Loading