diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
index 468f8ddb4..7e233f431 100644
--- a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
+++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
@@ -2119,6 +2119,60 @@ namespace ModreqParams
Assert.That(modifiedHandle, Is.EqualTo((EntityHandle)MetadataTokens.MethodDefinitionHandle(1)));
}
+ [Test]
+ public void DocumentationLookup_DoesNotFallBackToSiblingKey()
+ {
+ // The Roslyn form of the modified overload equals the only key of the
+ // unmodified overload. Such an assembly cannot come from the C# compiler, so
+ // its xml file uses the C++/CLI dialect, where that key documents the
+ // unmodified overload: the modified overload's candidates must omit it, and
+ // its documentation lookup must miss instead of showing the sibling's text.
+ var pe = BuildAssemblyWithMethods(
+ (metadata, parameter) => {
+ var pointee = parameter.Type().Pointer();
+ pointee.CustomModifiers().AddModifier(
+ AddCompilerServicesTypeRef(metadata, "IsSignUnspecifiedByte"), isOptional: true);
+ pointee.SByte();
+ },
+ (metadata, parameter) => parameter.Type().Pointer().SByte());
+
+ Assert.That(pe.GetIdStringCandidates(MetadataTokens.MethodDefinitionHandle(1)),
+ Is.EqualTo(new[] { "M:Host.M(System.SByte!System.Runtime.CompilerServices.IsSignUnspecifiedByte*)" }));
+ Assert.That(pe.GetIdStringCandidates(MetadataTokens.MethodDefinitionHandle(2)),
+ Is.EqualTo(new[] { "M:Host.M(System.SByte*)" }));
+
+ string xmlPath = Path.Combine(Path.GetTempPath(),
+ "IdStringSiblingGuard_" + Guid.NewGuid().ToString("N") + ".xml");
+ File.WriteAllText(xmlPath, """
+
+
+ test
+
+
+ plain overload
+
+
+
+ """);
+ try
+ {
+ var provider = new XmlDocumentationProvider(xmlPath);
+ var compilation = new SimpleCompilation(pe, MinimalCorlib.Instance);
+ var host = compilation.MainModule.TopLevelTypeDefinitions.Single(t => t.Name == "Host");
+ var modified = host.Methods.Single(
+ m => m.MetadataToken == (EntityHandle)MetadataTokens.MethodDefinitionHandle(1));
+ var plain = host.Methods.Single(
+ m => m.MetadataToken == (EntityHandle)MetadataTokens.MethodDefinitionHandle(2));
+
+ Assert.That(provider.GetDocumentation(plain), Does.Contain("plain overload"));
+ Assert.That(provider.GetDocumentation(modified), Is.Null);
+ }
+ finally
+ {
+ File.Delete(xmlPath);
+ }
+ }
+
#endregion
#region MSVC C++/CLI dialect fixture
diff --git a/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs b/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
index 10bb769c5..96bcdbc6d 100644
--- a/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
+++ b/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
@@ -56,15 +56,76 @@ namespace ICSharpCode.Decompiler.Documentation
/// it contains character sequences Roslyn never writes, so it can only match
/// MSVC-generated keys; the stripped Roslyn form of one member can collide with the
/// key of a different member in an MSVC-generated file (e.g. overloads differing
- /// only in a custom modifier).
+ /// only in a custom modifier). When such a colliding sibling overload exists, the
+ /// Roslyn form is omitted entirely: assemblies containing such overloads cannot
+ /// come from the C# compiler, so their xml files use the C++/CLI dialect, where
+ /// that key documents the sibling.
///
public static IEnumerable GetIdStringCandidates(this MetadataFile module, EntityHandle handle)
{
string primary = GetIdString(module, handle, cppCliDialect: false);
string cppCli = GetIdString(module, handle, cppCliDialect: true);
- if (cppCli != primary)
- yield return cppCli;
- yield return primary;
+ if (cppCli == primary)
+ {
+ yield return primary;
+ yield break;
+ }
+ yield return cppCli;
+ if (!RoslynFormBelongsToSibling(module, handle, primary))
+ yield return primary;
+ }
+
+ ///
+ /// True when the C#/Roslyn-form ID of equals the
+ /// C++/CLI-form ID of a same-named sibling member of the same type (only members
+ /// with a signature portion can diverge, so only methods and properties are
+ /// checked). Keys embed the declaring type and member name, so no other member
+ /// can own the string.
+ ///
+ static bool RoslynFormBelongsToSibling(MetadataFile module, EntityHandle handle, string roslynForm)
+ {
+ var metadata = module.Metadata;
+ switch (handle.Kind)
+ {
+ case HandleKind.MethodDefinition:
+ {
+ var methodHandle = (MethodDefinitionHandle)handle;
+ var methodDef = metadata.GetMethodDefinition(methodHandle);
+ string name = metadata.GetString(methodDef.Name);
+ foreach (var sibling in metadata.GetTypeDefinition(methodDef.GetDeclaringType()).GetMethods())
+ {
+ if (sibling == methodHandle
+ || !metadata.StringComparer.Equals(metadata.GetMethodDefinition(sibling).Name, name))
+ {
+ continue;
+ }
+ if (GetIdString(module, sibling, cppCliDialect: true) == roslynForm)
+ return true;
+ }
+ return false;
+ }
+
+ case HandleKind.PropertyDefinition:
+ {
+ var propertyHandle = (PropertyDefinitionHandle)handle;
+ string name = metadata.GetString(metadata.GetPropertyDefinition(propertyHandle).Name);
+ var declaringType = FindDeclaringTypeOfProperty(metadata, propertyHandle);
+ foreach (var sibling in metadata.GetTypeDefinition(declaringType).GetProperties())
+ {
+ if (sibling == propertyHandle
+ || !metadata.StringComparer.Equals(metadata.GetPropertyDefinition(sibling).Name, name))
+ {
+ continue;
+ }
+ if (GetIdString(module, sibling, cppCliDialect: true) == roslynForm)
+ return true;
+ }
+ return false;
+ }
+
+ default:
+ return false;
+ }
}
static string GetIdString(MetadataFile module, EntityHandle handle, bool cppCliDialect)