Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions Extensions.Test/ReflectionExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,90 @@ public void TryFindMethodFindsMethodWithParameters()
}

[TestMethod]
public void TryFindMethodThrowsOnAmbiguousMatchForOverloadedMethod()
public void TryFindMethodPicksFewestParametersForOverloadedMethod()
{
Type type = typeof(DerivedClassWithAdditionalMethods);
string methodName = "OverloadedMethod";
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

Assert.ThrowsExactly<AmbiguousMatchException>(() => type.TryFindMethod(methodName, bindingFlags, out MethodInfo? methodInfo));
bool result = type.TryFindMethod(methodName, bindingFlags, out MethodInfo? methodInfo);

Assert.IsTrue(result, "TryFindMethod should return true for an overloaded method name.");
Assert.IsNotNull(methodInfo);
Assert.IsEmpty(methodInfo.GetParameters());
}

[TestMethod]
public void TryFindMethodDoesNotThrowForOverloadedFrameworkMethod()
{
bool result = typeof(string).TryFindMethod(nameof(string.Split), BindingFlags.Instance | BindingFlags.Public, out MethodInfo? methodInfo);

Assert.IsTrue(result, "TryFindMethod should return true for string.Split.");
Assert.IsNotNull(methodInfo);
Assert.AreEqual(nameof(string.Split), methodInfo.Name);
}

[TestMethod]
public void TryFindMethodFindsOverloadedPrivateMethodInBaseClass()
{
Type type = typeof(DerivedFromPrivateOverloads);
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;

bool result = type.TryFindMethod("Foo", bindingFlags, out MethodInfo? methodInfo);

Assert.IsTrue(result, "TryFindMethod should return true for an overloaded private method on a base class.");
Assert.IsNotNull(methodInfo);
Assert.AreEqual(typeof(BaseWithPrivateOverloads), methodInfo.DeclaringType);
}

[TestMethod]
public void TryFindMethodResolvesOverloadsTheSameWayEveryTime()
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

typeof(string).TryFindMethod(nameof(string.Split), bindingFlags, out MethodInfo? first);
typeof(string).TryFindMethod(nameof(string.Split), bindingFlags, out MethodInfo? second);

Assert.AreEqual(first, second);
}

[TestMethod]
public void TryFindMethodFindsMethodInheritedFromBaseInterface()
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

bool result = typeof(IDerivedInterface).TryFindMethod(nameof(IBaseInterface.BaseOp), bindingFlags, out MethodInfo? methodInfo);

Assert.IsTrue(result, "TryFindMethod should find a method declared on a base interface.");
Assert.IsNotNull(methodInfo);
Assert.AreEqual(typeof(IBaseInterface), methodInfo.DeclaringType);
}

[TestMethod]
public void TryFindMethodFindsMethodTwoInterfaceLevelsUp()
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

bool result = typeof(IGrandchildInterface).TryFindMethod(nameof(IBaseInterface.BaseOp), bindingFlags, out MethodInfo? methodInfo);

Assert.IsTrue(result, "TryFindMethod should find a method declared two interface levels up.");
Assert.IsNotNull(methodInfo);
Assert.AreEqual(typeof(IBaseInterface), methodInfo.DeclaringType);
}

[TestMethod]
public void TryFindMethodFindsMethodsOnGenericCollectionInterfaces()
{
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

Assert.IsTrue(typeof(IList<int>).TryFindMethod(nameof(IList<>.Add), bindingFlags, out MethodInfo? add));
Assert.AreEqual(typeof(ICollection<int>), add?.DeclaringType);

Assert.IsTrue(typeof(IList<int>).TryFindMethod(nameof(IList<>.GetEnumerator), bindingFlags, out MethodInfo? getEnumerator));
Assert.AreEqual(typeof(IEnumerable<int>), getEnumerator?.DeclaringType);

Assert.IsTrue(typeof(ICollection<int>).TryFindMethod(nameof(ICollection<>.Add), bindingFlags, out MethodInfo? collectionAdd));
Assert.AreEqual(typeof(ICollection<int>), collectionAdd?.DeclaringType);
}

[TestMethod]
Expand Down Expand Up @@ -194,4 +271,35 @@ public void GenericMethod<T>(T param)
{
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Test class")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Test class")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Test class")]
public class BaseWithPrivateOverloads
{
private void Foo(int value)
{
}

private void Foo(string value)
{
}
}

public class DerivedFromPrivateOverloads : BaseWithPrivateOverloads
{
}

public interface IBaseInterface
{
public void BaseOp();
}

public interface IDerivedInterface : IBaseInterface
{
}

public interface IGrandchildInterface : IDerivedInterface
{
}
}
60 changes: 54 additions & 6 deletions Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ public static class ReflectionExtensions
/// <summary>
/// Walks up the inheritance tree to find a method with the given name and binding flags.
/// </summary>
/// <remarks>
/// <para>
/// The search starts at <paramref name="type"/> and moves to each base class in turn, stopping at the first
/// level that declares a method with the given name. When <paramref name="type"/> is an interface, the
/// interfaces it inherits are searched after it, more-derived interfaces first.
/// </para>
/// <para>
/// When the name is overloaded at that level, the overload with the fewest parameters is returned. A
/// non-generic method is preferred over a generic one with the same parameter count, and any remaining tie is
/// broken by ordinal comparison of the method signatures, so the result is always the same for the same type.
/// </para>
/// </remarks>
/// <param name="type">The type to search.</param>
/// <param name="methodName">The name of the method to find.</param>
/// <param name="bindingFlags">The binding flags to use when searching.</param>
Expand All @@ -28,16 +40,52 @@ public static bool TryFindMethod(this Type type, string methodName, BindingFlags
}

methodInfo = null;
Type? methodOwner = type;
while (methodInfo is null && methodOwner is not null)
foreach (Type[] level in SearchLevels(type))
{
methodInfo = methodOwner.GetMethod(methodName, bindingFlags);
if (methodInfo is null)
methodInfo = level
.SelectMany(t => t.GetMethods(bindingFlags | BindingFlags.DeclaredOnly))
.Where(m => m.Name.Equals(methodName, StringComparison.Ordinal))
.OrderBy(m => m.GetParameters().Length)
.ThenBy(m => m.IsGenericMethodDefinition)
.ThenBy(m => m.ToString(), StringComparer.Ordinal)
.FirstOrDefault();

if (methodInfo is not null)
{
return true;
}
}

return false;
}

/// <summary>
/// Yields the groups of types to search, nearest first: the type and its base classes one at a time, or for an
/// interface, the interface itself followed by its inherited interfaces grouped by how many interfaces they in
/// turn inherit, so that a derived interface is always searched before the interfaces it extends.
/// </summary>
private static IEnumerable<Type[]> SearchLevels(Type type)
{
if (type.IsInterface)
{
yield return [type];

IEnumerable<Type[]> inheritedLevels = type.GetInterfaces()
.GroupBy(i => i.GetInterfaces().Length)
.OrderByDescending(g => g.Key)
.Select(g => g.ToArray());

foreach (Type[] level in inheritedLevels)
{
methodOwner = methodOwner.BaseType;
yield return level;
}

yield break;
}

return methodInfo is not null;
for (Type? methodOwner = type; methodOwner is not null; methodOwner = methodOwner.BaseType)
{
yield return [methodOwner];
}
}
}
Loading