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)