From 75baa4991f179b8ee24f0b417e7ce2beb87fe998 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 28 Jul 2026 17:29:37 +0200 Subject: [PATCH] Do not fall back to a sibling's documentation key The C#/Roslyn-form ID of a member whose C++/CLI form differs can equal the only key of a same-named sibling overload (char* vs signed char*). 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: falling back to the Roslyn form would show the sibling's documentation for an undocumented member. GetIdStringCandidates now omits the Roslyn form when a same-named sibling's C++/CLI form owns it, so a lookup miss stays a miss. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Documentation/IdStringProviderTests.cs | 54 +++++++++++++++ .../Documentation/IdStringProvider.cs | 69 +++++++++++++++++-- 2 files changed, 119 insertions(+), 4 deletions(-) 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)