From 862cecdc0eb9f1bfbd06e052a7d06213da7e2e4b Mon Sep 17 00:00:00 2001 From: Ifeanyi Shadrach Odom Date: Tue, 28 Jul 2026 11:05:11 +0100 Subject: [PATCH 1/2] #3202 Disambiguate type display names in benchmark selection Added GetDisambiguatedDisplayNames to qualify ambiguous type names with namespaces. Updated UserInteraction to use these names for clearer benchmark selection and filtering. Added unit tests and supporting fixture classes to verify correct behavior. --- .../Extensions/ReflectionExtensions.cs | 28 ++++++++++++++ .../Running/UserInteraction.cs | 15 ++++---- .../BenchmarkDotNet.Tests/ReflectionTests.cs | 38 +++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index 4f2eaf0dc4..806e65eb35 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -112,6 +112,34 @@ private static IEnumerable GetNestedTypeNames(Type type, bool includeGen /// internal static string GetDisplayName(this Type type) => GetDisplayName(type.GetTypeInfo()); + /// + /// Returns a display name per type, aligned with . When more than one + /// type shares the same simple (e.g. same class name in + /// different namespaces), the colliding names are qualified with their namespace so they can be + /// told apart; unambiguous names are left as-is. + /// + internal static string[] GetDisambiguatedDisplayNames(this IReadOnlyList types) + { + var simpleNames = new string[types.Count]; + for (int i = 0; i < types.Count; i++) + simpleNames[i] = types[i].GetDisplayName(); + + var ambiguousNames = new HashSet( + simpleNames.GroupBy(name => name).Where(group => group.Count() > 1).Select(group => group.Key), + StringComparer.Ordinal); + + var displayNames = new string[types.Count]; + for (int i = 0; i < types.Count; i++) + { + string? @namespace = types[i].Namespace; + displayNames[i] = ambiguousNames.Contains(simpleNames[i]) && !string.IsNullOrEmpty(@namespace) + ? $"{@namespace}.{simpleNames[i]}" + : simpleNames[i]; + } + + return displayNames; + } + /// /// returns simple, human friendly display name /// diff --git a/src/BenchmarkDotNet/Running/UserInteraction.cs b/src/BenchmarkDotNet/Running/UserInteraction.cs index d706110142..0bdc99314c 100644 --- a/src/BenchmarkDotNet/Running/UserInteraction.cs +++ b/src/BenchmarkDotNet/Running/UserInteraction.cs @@ -21,11 +21,12 @@ public void PrintNoBenchmarksError(ILogger logger) public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) { var selectedTypes = new List(); - string benchmarkCaptionExample = allTypes.First().GetDisplayName(); + var displayNames = allTypes.GetDisambiguatedDisplayNames(); + string benchmarkCaptionExample = displayNames[0]; while (selectedTypes.Count == 0 && !consoleCancelKeyPressed) { - PrintAvailable(allTypes, logger); + PrintAvailable(allTypes, displayNames, logger); if (consoleCancelKeyPressed) break; @@ -42,7 +43,7 @@ public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) break; } - selectedTypes.AddRange(GetMatching(allTypes, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries))); + selectedTypes.AddRange(GetMatching(allTypes, displayNames, userInput.Split([' '], StringSplitOptions.RemoveEmptyEntries))); logger.WriteLine(); } @@ -81,7 +82,7 @@ public void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, s logger.WriteLineInfo("To learn more about filtering use `--help`."); } - private static IEnumerable GetMatching(IReadOnlyList allTypes, string[] userInput) + private static IEnumerable GetMatching(IReadOnlyList allTypes, string[] displayNames, string[] userInput) { if (userInput.IsEmpty()) yield break; @@ -93,7 +94,7 @@ private static IEnumerable GetMatching(IReadOnlyList allTypes, strin { var type = allTypes[i]; - if (stringInput.Any(arg => type.GetDisplayName().ContainsWithIgnoreCase(arg)) + if (stringInput.Any(arg => displayNames[i].ContainsWithIgnoreCase(arg)) || stringInput.Contains($"#{i}") || integerInput.Contains($"{i}") || stringInput.Contains("*")) @@ -105,13 +106,13 @@ private static IEnumerable GetMatching(IReadOnlyList allTypes, strin static bool IsInteger(string str) => int.TryParse(str, out _); } - private static void PrintAvailable(IReadOnlyList allTypes, ILogger logger) + private static void PrintAvailable(IReadOnlyList allTypes, string[] displayNames, ILogger logger) { logger.WriteLineHelp($"Available Benchmark{(allTypes.Count > 1 ? "s" : "")}:"); int numberWidth = allTypes.Count.ToString().Length; for (int i = 0; i < allTypes.Count && !consoleCancelKeyPressed; i++) - logger.WriteLineHelp(string.Format(CultureInfo.InvariantCulture, " #{0} {1}", i.ToString().PadRight(numberWidth), allTypes[i].GetDisplayName())); + logger.WriteLineHelp(string.Format(CultureInfo.InvariantCulture, " #{0} {1}", i.ToString().PadRight(numberWidth), displayNames[i])); if (!consoleCancelKeyPressed) { diff --git a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs index 17b6a78d9a..e39c71af82 100644 --- a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs +++ b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs @@ -125,6 +125,34 @@ private static void CheckCorrectDisplayName(string expectedName, Type type) Assert.Equal(expectedName, type.GetDisplayName()); } + [Fact] + public void GetDisambiguatedDisplayNamesQualifiesCollidingNamesWithNamespace() + { + var types = new[] + { + typeof(Foo.Fixture), + typeof(Bar.Fixture), + typeof(ReflectionTests), + }; + + var displayNames = types.GetDisambiguatedDisplayNames(); + + Assert.Equal("BenchmarkDotNet.Tests.Foo.Fixture", displayNames[0]); + Assert.Equal("BenchmarkDotNet.Tests.Bar.Fixture", displayNames[1]); + Assert.Equal("ReflectionTests", displayNames[2]); + } + + [Fact] + public void GetDisambiguatedDisplayNamesLeavesUniqueNamesUnqualified() + { + var types = new[] { typeof(Foo.Fixture), typeof(ReflectionTests) }; + + var displayNames = types.GetDisambiguatedDisplayNames(); + + Assert.Equal("Fixture", displayNames[0]); + Assert.Equal("ReflectionTests", displayNames[1]); + } + [Fact] public void OnlyClosedGenericsWithPublicParameterlessCtorsAreSupported() { @@ -175,4 +203,14 @@ public static implicit operator StackOnlyStruct(WithImplicitCastToStackOnlySt => new StackOnlyStruct { Span = instance.Array }; } } +} + +namespace BenchmarkDotNet.Tests.Foo +{ + public class Fixture { } +} + +namespace BenchmarkDotNet.Tests.Bar +{ + public class Fixture { } } \ No newline at end of file From e90b701ed71a3b3e571ab40a9192715d0e453a0b Mon Sep 17 00:00:00 2001 From: Ifeanyi Shadrach Odom Date: Wed, 29 Jul 2026 07:07:25 +0100 Subject: [PATCH 2/2] Use FullName for ambiguous type display names Updated logic to use the type's FullName property for display names when type names are ambiguous, instead of combining namespace and simple name. This ensures more accurate and consistent type identification. --- src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index 806e65eb35..1bfaf346ce 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -131,9 +131,9 @@ internal static string[] GetDisambiguatedDisplayNames(this IReadOnlyList t var displayNames = new string[types.Count]; for (int i = 0; i < types.Count; i++) { - string? @namespace = types[i].Namespace; - displayNames[i] = ambiguousNames.Contains(simpleNames[i]) && !string.IsNullOrEmpty(@namespace) - ? $"{@namespace}.{simpleNames[i]}" + string? fullName = types[i].FullName; + displayNames[i] = ambiguousNames.Contains(simpleNames[i]) && !string.IsNullOrEmpty(fullName) + ? fullName : simpleNames[i]; }