From 19782f25044adf2ca951bf24baa0d3ed35f002b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:26:56 +0000 Subject: [PATCH] [patch] Resolve overloads in TryFindMethod and search inherited interfaces TryFindMethod threw AmbiguousMatchException for any overloaded name, and returned false for methods an interface inherits from its base interfaces. It now searches one hierarchy level at a time, takes the overload with the fewest parameters (non-generic first, then ordinal signature order), and for an interface also searches its inherited interfaces, most-derived first. Fixes #155 Fixes #158 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P2fpLkFHYU6nd3JVKufVtK --- Extensions.Test/ReflectionExtensionsTests.cs | 112 ++++++++++++++++++- Extensions/ReflectionExtensions.cs | 60 +++++++++- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/Extensions.Test/ReflectionExtensionsTests.cs b/Extensions.Test/ReflectionExtensionsTests.cs index 1697e88..77d7dd0 100644 --- a/Extensions.Test/ReflectionExtensionsTests.cs +++ b/Extensions.Test/ReflectionExtensionsTests.cs @@ -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(() => 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).TryFindMethod(nameof(IList<>.Add), bindingFlags, out MethodInfo? add)); + Assert.AreEqual(typeof(ICollection), add?.DeclaringType); + + Assert.IsTrue(typeof(IList).TryFindMethod(nameof(IList<>.GetEnumerator), bindingFlags, out MethodInfo? getEnumerator)); + Assert.AreEqual(typeof(IEnumerable), getEnumerator?.DeclaringType); + + Assert.IsTrue(typeof(ICollection).TryFindMethod(nameof(ICollection<>.Add), bindingFlags, out MethodInfo? collectionAdd)); + Assert.AreEqual(typeof(ICollection), collectionAdd?.DeclaringType); } [TestMethod] @@ -194,4 +271,35 @@ public void GenericMethod(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 + { + } } diff --git a/Extensions/ReflectionExtensions.cs b/Extensions/ReflectionExtensions.cs index ff1b6c4..2d60135 100644 --- a/Extensions/ReflectionExtensions.cs +++ b/Extensions/ReflectionExtensions.cs @@ -12,6 +12,18 @@ public static class ReflectionExtensions /// /// Walks up the inheritance tree to find a method with the given name and binding flags. /// + /// + /// + /// The search starts at and moves to each base class in turn, stopping at the first + /// level that declares a method with the given name. When is an interface, the + /// interfaces it inherits are searched after it, more-derived interfaces first. + /// + /// + /// 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. + /// + /// /// The type to search. /// The name of the method to find. /// The binding flags to use when searching. @@ -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; + } + + /// + /// 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. + /// + private static IEnumerable SearchLevels(Type type) + { + if (type.IsInterface) + { + yield return [type]; + + IEnumerable 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]; + } } }