Browse Source

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
pull/3941/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
75baa4991f
  1. 54
      ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
  2. 69
      ICSharpCode.Decompiler/Documentation/IdStringProvider.cs

54
ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs

@ -2119,6 +2119,60 @@ namespace ModreqParams
Assert.That(modifiedHandle, Is.EqualTo((EntityHandle)MetadataTokens.MethodDefinitionHandle(1))); 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, """
<?xml version="1.0"?>
<doc>
<assembly><name>test</name></assembly>
<members>
<member name="M:Host.M(System.SByte*)">
<summary>plain overload</summary>
</member>
</members>
</doc>
""");
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 #endregion
#region MSVC C++/CLI dialect fixture #region MSVC C++/CLI dialect fixture

69
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 /// 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 /// 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 /// 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.
/// </summary> /// </summary>
public static IEnumerable<string> GetIdStringCandidates(this MetadataFile module, EntityHandle handle) public static IEnumerable<string> GetIdStringCandidates(this MetadataFile module, EntityHandle handle)
{ {
string primary = GetIdString(module, handle, cppCliDialect: false); string primary = GetIdString(module, handle, cppCliDialect: false);
string cppCli = GetIdString(module, handle, cppCliDialect: true); string cppCli = GetIdString(module, handle, cppCliDialect: true);
if (cppCli != primary) if (cppCli == primary)
yield return cppCli; {
yield return primary; yield return primary;
yield break;
}
yield return cppCli;
if (!RoslynFormBelongsToSibling(module, handle, primary))
yield return primary;
}
/// <summary>
/// True when the C#/Roslyn-form ID of <paramref name="handle"/> 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.
/// </summary>
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) static string GetIdString(MetadataFile module, EntityHandle handle, bool cppCliDialect)

Loading…
Cancel
Save