Browse Source

Fix #2728: render the MSVC C++/CLI ID string dialect

MSVC documents C++/CLI members with ECMA-372-style ID strings that
differ from Roslyn's in signatures: custom modifiers are rendered after
the modified type (a 'const int' parameter becomes
System.Int32!System.Runtime.CompilerServices.IsConst), arity markers
stay on generic instantiations (List`1{System.Int32}.Enumerator), and
the default indexed property is called 'default'. Roslyn ignores
modifiers entirely, so one generated string cannot match both
compilers' xml files: GetIdString keeps producing the C#/Roslyn form,
and the new GetIdStringCandidates additionally yields the C++/CLI form,
most specific first, for lookup code to try in order.

The dialect is pinned by IdStringProbe.il/.xml, the trimmed disassembly
of an MSVC-compiled probe assembly together with the unmodified xml MSVC
generated for it. Notable observed deviations from MSVC's documented
format: modreq is generated, but only modreq(IsVolatile) uses the
documented '|'; modreq(IsByValue) on conversion operator operands is
rendered with '!'.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3941/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
75e7c61e93
  1. 2
      ICSharpCode.Decompiler.Tests/Documentation/.gitignore
  2. 81
      ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.cpp
  3. 351
      ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.il
  4. 68
      ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.xml
  5. 117
      ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
  6. 108
      ICSharpCode.Decompiler/Documentation/IdStringProvider.cs

2
ICSharpCode.Decompiler.Tests/Documentation/.gitignore vendored

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
/IdStringProbe.dll
/IdStringProbe.pdb

81
ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.cpp

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
// Probe assembly for MSVC's xml doc ID string generator (the ECMA-372-style
// dialect ILSpy's IdStringProvider must reproduce for C++/CLI assemblies).
// Every documented member below lands in the generated .xml; the members are
// independent, so if one fails to compile, delete it and rebuild.
//
// Build (VS Developer Command Prompt):
// msbuild IdStringProbe.vcxproj -p:Configuration=Debug -p:Platform=x64
// Return: x64\Debug\IdStringProbe.dll and x64\Debug\IdStringProbe.xml
using namespace System;
using namespace System::Collections::Generic;
/// <summary>Probe class.</summary>
public ref class IdProbe
{
public:
/// <summary>const int parameter; known baseline: System.Int32!System.Runtime.CompilerServices.IsConst</summary>
static int ConstValue(const int x) { return x; }
/// <summary>long parameter; modopt(IsLong) on Int32</summary>
static void TakesLong(long x) { (void)x; }
/// <summary>unsigned long parameter; modopt(IsLong) on UInt32</summary>
static void TakesULong(unsigned long x) { (void)x; }
/// <summary>char pointer parameter; modopt(IsSignUnspecifiedByte) under a pointer</summary>
static void TakesCharPtr(char* p) { (void)p; }
/// <summary>const char pointer parameter; IsConst and IsSignUnspecifiedByte together (modifier ordering)</summary>
static void TakesConstCharPtr(const char* p) { (void)p; }
/// <summary>volatile int pointer parameter; modreq(IsVolatile): the open question is whether MSVC renders '|', '!', or omits it</summary>
static void TakesVolatilePtr(volatile int* p) { (void)p; }
/// <summary>const volatile int pointer parameter; modreq and modopt mixed on one type</summary>
static void TakesConstVolatilePtr(const volatile int* p) { (void)p; }
/// <summary>tracking reference parameter; expected System.Int32@</summary>
static void TakesTrackingRef(int% r) { r = 0; }
/// <summary>const tracking reference parameter; modifier placement relative to the '@'</summary>
static void TakesConstTrackingRef(const int% r) { (void)r; }
/// <summary>native reference parameter under /clr; representation evidence</summary>
static void TakesNativeRef(int& r) { r = 0; }
/// <summary>two-dimensional managed array parameter; expected System.Int32[0:,0:]</summary>
static void TakesArray2(array<int, 2>^ a) { (void)a; }
/// <summary>nested type of a generic instantiation; how does MSVC distribute the type arguments?</summary>
static void TakesEnumerator(List<int>::Enumerator e) { (void)e; }
/// <summary>generic method; expected arity marker and grave-accent parameter encoding</summary>
generic<typename T> static void Gen(T t) { (void)t; }
/// <summary>conversion operator with a by-value ref-class parameter; the MSVC docs show IdProbe!IsByValue and a '~' return type</summary>
static explicit operator int(IdProbe x) { (void)x; return 0; }
/// <summary>indexed property with a long parameter; modopt inside the indexer parentheses</summary>
property int default[long]
{
int get(long i) { return (int)i; }
void set(long i, int value) { (void)i; (void)value; }
}
};
/// <summary>Generic ref class; arity in the type ID.</summary>
generic<typename T> public ref class GBox
{
public:
/// <summary>method on a generic type taking T; expected grave-accent zero</summary>
void Hold(T item) { (void)item; }
};
/// <summary>Consumer of an instantiated generic type.</summary>
public ref class GBoxUser
{
public:
/// <summary>generic instantiation in a signature; expected GBox{System.Int32}</summary>
static void Use(GBox<int>^ b) { (void)b; }
};

351
ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.il

@ -0,0 +1,351 @@ @@ -0,0 +1,351 @@
// Trimmed disassembly of an assembly compiled with MSVC from IdStringProbe.cpp
// (next to this file) with /clr /doc. The signatures are verbatim from the
// MSVC-produced metadata; the accompanying IdStringProbe.xml is the unmodified
// output of MSVC's xml doc generator for the same compilation and serves as the
// reference for the C++/CLI (ECMA-372-style) ID string dialect.
.assembly extern mscorlib
{
.ver 4:0:0:0
}
.assembly IdStringProbe
{
.ver 0:0:0:0
}
.module IdStringProbe.dll
.class public auto ansi beforefieldinit IdProbe
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Reflection.DefaultMemberAttribute::.ctor(string) = (
01 00 04 49 74 65 6d 00 00
)
// Methods
.method public hidebysig static
int32 ConstValue (
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsConst) x
) cil managed
{
// Method begins at RVA 0x10e0
// Header size: 12
// Code size: 4 (0x4)
.maxstack 1
.locals (
[0] int32
)
IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ret
} // end of method IdProbe::ConstValue
.method public hidebysig static
void TakesLong (
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) x
) cil managed
{
// Method begins at RVA 0x10f0
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesLong
.method public hidebysig static
void TakesULong (
uint32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) x
) cil managed
{
// Method begins at RVA 0x1100
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesULong
.method public hidebysig static
void TakesCharPtr (
int8 modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte)* p
) cil managed
{
// Method begins at RVA 0x1110
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesCharPtr
.method public hidebysig static
void TakesConstCharPtr (
int8 modopt([mscorlib]System.Runtime.CompilerServices.IsSignUnspecifiedByte) modopt([mscorlib]System.Runtime.CompilerServices.IsConst)* p
) cil managed
{
// Method begins at RVA 0x1120
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesConstCharPtr
.method public hidebysig static
void TakesVolatilePtr (
int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)* p
) cil managed
{
// Method begins at RVA 0x1130
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesVolatilePtr
.method public hidebysig static
void TakesConstVolatilePtr (
int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile) modopt([mscorlib]System.Runtime.CompilerServices.IsConst)* p
) cil managed
{
// Method begins at RVA 0x1140
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesConstVolatilePtr
.method public hidebysig static
void TakesTrackingRef (
int32& r
) cil managed
{
// Method begins at RVA 0x1150
// Header size: 12
// Code size: 4 (0x4)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: stind.i4
IL_0003: ret
} // end of method IdProbe::TakesTrackingRef
.method public hidebysig static
void TakesConstTrackingRef (
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsConst)& r
) cil managed
{
// Method begins at RVA 0x1160
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesConstTrackingRef
.method public hidebysig static
void TakesNativeRef (
int32* modopt([mscorlib]System.Runtime.CompilerServices.IsImplicitlyDereferenced) r
) cil managed
{
// Method begins at RVA 0x1170
// Header size: 12
// Code size: 4 (0x4)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: stind.i4
IL_0003: ret
} // end of method IdProbe::TakesNativeRef
.method public hidebysig static
void TakesArray2 (
int32[0..., 0...] a
) cil managed
{
// Method begins at RVA 0x1180
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesArray2
.method public hidebysig static
void TakesEnumerator (
valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> e
) cil managed
{
// Method begins at RVA 0x1190
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::TakesEnumerator
.method public hidebysig static
void Gen<T> (
!!T t
) cil managed
{
// Method begins at RVA 0x11a0
// Header size: 12
// Code size: 3 (0x3)
.maxstack 1
.locals (
[0] !!T
)
IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ret
} // end of method IdProbe::Gen
.method public hidebysig specialname static
int32 op_Explicit (
class IdProbe modreq([mscorlib]System.Runtime.CompilerServices.IsByValue) x
) cil managed
{
// Method begins at RVA 0x11b0
// Header size: 12
// Code size: 4 (0x4)
.maxstack 1
.locals (
[0] int32
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ret
} // end of method IdProbe::op_Explicit
.method public hidebysig specialname
instance int32 get_Item (
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) i
) cil managed
{
// Method begins at RVA 0x11c0
// Header size: 12
// Code size: 4 (0x4)
.maxstack 1
.locals (
[0] int32
)
IL_0000: ldarg.1
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ret
} // end of method IdProbe::get_Item
.method public hidebysig specialname
instance void set_Item (
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) i,
int32 'value'
) cil managed
{
// Method begins at RVA 0x11d0
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method IdProbe::set_Item
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x11e0
// Header size: 12
// Code size: 7 (0x7)
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method IdProbe::.ctor
// Properties
.property instance int32 Item(
int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong) i
)
{
.get instance int32 IdProbe::get_Item(int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong))
.set instance void IdProbe::set_Item(int32 modopt([mscorlib]System.Runtime.CompilerServices.IsLong), int32)
}
} // end of class IdProbe
.class public auto ansi beforefieldinit GBox`1<T>
extends [mscorlib]System.Object
{
// Methods
.method public hidebysig
instance void Hold (
!T item
) cil managed
{
// Method begins at RVA 0x11f4
// Header size: 12
// Code size: 3 (0x3)
.maxstack 1
.locals (
[0] !T
)
IL_0000: ldarg.1
IL_0001: stloc.0
IL_0002: ret
} // end of method GBox`1::Hold
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x1204
// Header size: 12
// Code size: 7 (0x7)
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method GBox`1::.ctor
} // end of class GBox`1
.class public auto ansi beforefieldinit GBoxUser
extends [mscorlib]System.Object
{
// Methods
.method public hidebysig static
void Use (
class GBox`1<int32> b
) cil managed
{
// Method begins at RVA 0x1218
// Header size: 12
// Code size: 1 (0x1)
.maxstack 0
IL_0000: ret
} // end of method GBoxUser::Use
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x1228
// Header size: 12
// Code size: 7 (0x7)
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method GBoxUser::.ctor
} // end of class GBoxUser

68
ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.xml

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
<?xml version="1.0"?>
<doc>
<assembly>
"IdStringProbe"
</assembly>
<members>
<member name="M:GBoxUser.Use(GBox`1{System.Int32})">
<summary>generic instantiation in a signature; expected GBox{System.Int32}</summary>
</member>
<member name="T:GBoxUser">
<summary>Consumer of an instantiated generic type.</summary>
</member>
<member name="M:GBox`1.Hold(`0)">
<summary>method on a generic type taking T; expected grave-accent zero</summary>
</member>
<member name="T:GBox`1">
<summary>Generic ref class; arity in the type ID.</summary>
</member>
<member name="P:IdProbe.default(System.Int32!System.Runtime.CompilerServices.IsLong)">
<summary>indexed property with a long parameter; modopt inside the indexer parentheses</summary>
</member>
<member name="M:IdProbe.op_Explicit(IdProbe!System.Runtime.CompilerServices.IsByValue)~System.Int32">
<summary>conversion operator with a by-value ref-class parameter; the MSVC docs show IdProbe!IsByValue and a '~' return type</summary>
</member>
<member name="M:IdProbe.Gen``1(``0)">
<summary>generic method; expected arity marker and grave-accent parameter encoding</summary>
</member>
<member name="M:IdProbe.TakesEnumerator(System.Collections.Generic.List`1{System.Int32}.Enumerator)">
<summary>nested type of a generic instantiation; how does MSVC distribute the type arguments?</summary>
</member>
<member name="M:IdProbe.TakesArray2(System.Int32[0:,0:])">
<summary>two-dimensional managed array parameter; expected System.Int32[0:,0:]</summary>
</member>
<member name="M:IdProbe.TakesNativeRef(System.Int32*!System.Runtime.CompilerServices.IsImplicitlyDereferenced)">
<summary>native reference parameter under /clr; representation evidence</summary>
</member>
<member name="M:IdProbe.TakesConstTrackingRef(System.Int32!System.Runtime.CompilerServices.IsConst@)">
<summary>const tracking reference parameter; modifier placement relative to the '@'</summary>
</member>
<member name="M:IdProbe.TakesTrackingRef(System.Int32@)">
<summary>tracking reference parameter; expected System.Int32@</summary>
</member>
<member name="M:IdProbe.TakesConstVolatilePtr(System.Int32|System.Runtime.CompilerServices.IsVolatile!System.Runtime.CompilerServices.IsConst*)">
<summary>const volatile int pointer parameter; modreq and modopt mixed on one type</summary>
</member>
<member name="M:IdProbe.TakesVolatilePtr(System.Int32|System.Runtime.CompilerServices.IsVolatile*)">
<summary>volatile int pointer parameter; modreq(IsVolatile): the open question is whether MSVC renders '|', '!', or omits it</summary>
</member>
<member name="M:IdProbe.TakesConstCharPtr(System.SByte!System.Runtime.CompilerServices.IsSignUnspecifiedByte!System.Runtime.CompilerServices.IsConst*)">
<summary>const char pointer parameter; IsConst and IsSignUnspecifiedByte together (modifier ordering)</summary>
</member>
<member name="M:IdProbe.TakesCharPtr(System.SByte!System.Runtime.CompilerServices.IsSignUnspecifiedByte*)">
<summary>char pointer parameter; modopt(IsSignUnspecifiedByte) under a pointer</summary>
</member>
<member name="M:IdProbe.TakesULong(System.UInt32!System.Runtime.CompilerServices.IsLong)">
<summary>unsigned long parameter; modopt(IsLong) on UInt32</summary>
</member>
<member name="M:IdProbe.TakesLong(System.Int32!System.Runtime.CompilerServices.IsLong)">
<summary>long parameter; modopt(IsLong) on Int32</summary>
</member>
<member name="M:IdProbe.ConstValue(System.Int32!System.Runtime.CompilerServices.IsConst)">
<summary>const int parameter; known baseline: System.Int32!System.Runtime.CompilerServices.IsConst</summary>
</member>
<member name="T:IdProbe">
<summary>Probe class.</summary>
</member>
</members>
</doc>

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

@ -2041,6 +2041,63 @@ namespace ModreqParams @@ -2041,6 +2041,63 @@ namespace ModreqParams
Assert.That(idString, Is.EqualTo("M:Host.M(System.Int32[1:5,3:])"));
}
[Test]
public void Modifier_Optional()
{
// The C#/Roslyn form ignores custom modifiers; the MSVC C++/CLI form renders
// modopt as '!' + modifier following the modified type, as C++/CLI 'const int'
// parameters show (see https://github.com/icsharpcode/ILSpy/issues/2728).
var pe = BuildAssemblyWithMethodSignature((metadata, parameter) => {
parameter.CustomModifiers().AddModifier(
AddCompilerServicesTypeRef(metadata, "IsConst"), isOptional: true);
parameter.Type().Int32();
});
Assert.That(pe.GetIdString(MetadataTokens.MethodDefinitionHandle(1)),
Is.EqualTo("M:Host.M(System.Int32)"));
Assert.That(pe.GetIdStringCandidates(MetadataTokens.MethodDefinitionHandle(1)),
Is.EqualTo(new[] {
"M:Host.M(System.Int32!System.Runtime.CompilerServices.IsConst)",
"M:Host.M(System.Int32)",
}));
}
[Test]
public void Modifier_Required()
{
// The C#/Roslyn form ignores modreq (e.g. modreq(InAttribute) on virtual 'in'
// parameters is documented as T@); the MSVC form renders modreq(IsVolatile)
// with '|' as observed in MSVC-generated xml doc files.
var pe = BuildAssemblyWithMethodSignature((metadata, parameter) => {
parameter.CustomModifiers().AddModifier(
AddCompilerServicesTypeRef(metadata, "IsVolatile"), isOptional: false);
parameter.Type().Int32();
});
Assert.That(pe.GetIdString(MetadataTokens.MethodDefinitionHandle(1)),
Is.EqualTo("M:Host.M(System.Int32)"));
Assert.That(pe.GetIdStringCandidates(MetadataTokens.MethodDefinitionHandle(1)),
Is.EqualTo(new[] {
"M:Host.M(System.Int32|System.Runtime.CompilerServices.IsVolatile)",
"M:Host.M(System.Int32)",
}));
}
[Test]
public void Modifier_OptionalUnderPointer()
{
// C++/CLI 'char*' emits int8 modopt(IsSignUnspecifiedByte)*, rendered by MSVC as
// System.SByte!System.Runtime.CompilerServices.IsSignUnspecifiedByte*
var pe = BuildAssemblyWithMethodSignature((metadata, parameter) => {
var pointee = parameter.Type().Pointer();
pointee.CustomModifiers().AddModifier(
AddCompilerServicesTypeRef(metadata, "IsSignUnspecifiedByte"), isOptional: true);
pointee.SByte();
});
Assert.That(pe.GetIdString(MetadataTokens.MethodDefinitionHandle(1)),
Is.EqualTo("M:Host.M(System.SByte*)"));
Assert.That(pe.GetIdStringCandidates(MetadataTokens.MethodDefinitionHandle(1)),
Does.Contain("M:Host.M(System.SByte!System.Runtime.CompilerServices.IsSignUnspecifiedByte*)"));
}
[Test]
public void Pinned_Suffix()
{
@ -2057,8 +2114,68 @@ namespace ModreqParams @@ -2057,8 +2114,68 @@ namespace ModreqParams
#endregion
#region MSVC C++/CLI dialect fixture
// IdStringProbe.il is the trimmed disassembly of an MSVC-compiled C++/CLI assembly
// and IdStringProbe.xml the unmodified xml doc file MSVC generated for it; every
// member key MSVC wrote must be reachable through the ID string candidates.
static async Task<PEFile> AssembleIdStringProbe()
{
string dir = Path.Combine(Tester.TesterPath, "../../../../Documentation");
string dll = await Tester.AssembleIL(Path.Combine(dir, "IdStringProbe.il"), AssemblerOptions.Library);
return new PEFile(dll);
}
static HashSet<string> CollectIdStringCandidates(PEFile pe)
{
var md = pe.Metadata;
var candidates = new HashSet<string>();
foreach (var th in md.TypeDefinitions)
{
var td = md.GetTypeDefinition(th);
if (md.GetString(td.Name) == "<Module>")
continue;
candidates.UnionWith(pe.GetIdStringCandidates(th));
foreach (var h in td.GetMethods())
candidates.UnionWith(pe.GetIdStringCandidates(h));
foreach (var h in td.GetProperties())
candidates.UnionWith(pe.GetIdStringCandidates(h));
foreach (var h in td.GetEvents())
candidates.UnionWith(pe.GetIdStringCandidates(h));
foreach (var h in td.GetFields())
candidates.UnionWith(pe.GetIdStringCandidates(h));
}
return candidates;
}
[Test]
public async Task MsvcCppCliXml_AllMemberIdsCovered()
{
var pe = await AssembleIdStringProbe();
var candidates = CollectIdStringCandidates(pe);
string xmlPath = Path.Combine(Tester.TesterPath, "../../../../Documentation/IdStringProbe.xml");
Assert.Multiple(() => {
foreach (var member in System.Xml.Linq.XDocument.Load(xmlPath).Descendants("member"))
{
string name = member.Attribute("name").Value;
Assert.That(candidates, Does.Contain(name), name);
}
});
}
#endregion
#region Hand-built metadata helpers
static TypeReferenceHandle AddCompilerServicesTypeRef(MetadataBuilder metadata, string name)
{
var mscorlib = metadata.AddAssemblyReference(metadata.GetOrAddString("mscorlib"),
new Version(4, 0, 0, 0), default, default, 0, default);
return metadata.AddTypeReference(mscorlib,
metadata.GetOrAddString("System.Runtime.CompilerServices"), metadata.GetOrAddString(name));
}
static PEFile BuildAssemblyWithMethodSignature(Action<MetadataBuilder, ParameterTypeEncoder> encodeParameter)
{
return BuildAssemblyWithMethods(encodeParameter);

108
ICSharpCode.Decompiler/Documentation/IdStringProvider.cs

@ -36,9 +36,38 @@ namespace ICSharpCode.Decompiler.Documentation @@ -36,9 +36,38 @@ namespace ICSharpCode.Decompiler.Documentation
{
#region GetIdString
/// <summary>
/// Gets the ID string (C# 4.0 spec, §A.3.1) for the specified entity.
/// Gets the ID string (C# 4.0 spec, §A.3.1) for the specified entity,
/// in the form the C# compiler writes into xml documentation files.
/// </summary>
public static string GetIdString(this MetadataFile module, EntityHandle handle)
{
return GetIdString(module, handle, cppCliDialect: false);
}
/// <summary>
/// Gets the ID string candidates for the entity, most specific first: the MSVC
/// C++/CLI (ECMA-372-style) form when it differs from the C#/Roslyn form, then the
/// C#/Roslyn form. Documentation lookup should try the candidates in order, so that
/// xml doc files written by either compiler can be matched. The dialects differ in
/// signatures only: Roslyn ignores custom modifiers and strips the arity marker of
/// instantiated generic types, while MSVC renders modifiers ('!' or '|' followed by
/// the modifier type), keeps arity markers, and refers to a default indexed
/// property as 'default'. The C++/CLI form comes first because wherever it differs
/// 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).
/// </summary>
public static IEnumerable<string> 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;
}
static string GetIdString(MetadataFile module, EntityHandle handle, bool cppCliDialect)
{
if (handle.IsNil)
throw new ArgumentException("The handle must not be nil.", nameof(handle));
@ -60,12 +89,12 @@ namespace ICSharpCode.Decompiler.Documentation @@ -60,12 +89,12 @@ namespace ICSharpCode.Decompiler.Documentation
case HandleKind.MethodDefinition:
b.Append("M:");
AppendMethodIdString(b, metadata, (MethodDefinitionHandle)handle);
AppendMethodIdString(b, metadata, (MethodDefinitionHandle)handle, cppCliDialect);
break;
case HandleKind.PropertyDefinition:
b.Append("P:");
AppendPropertyIdString(b, metadata, (PropertyDefinitionHandle)handle);
AppendPropertyIdString(b, metadata, (PropertyDefinitionHandle)handle, cppCliDialect);
break;
case HandleKind.EventDefinition:
@ -154,7 +183,7 @@ namespace ICSharpCode.Decompiler.Documentation @@ -154,7 +183,7 @@ namespace ICSharpCode.Decompiler.Documentation
b.Append(metadata.GetString(fieldDef.Name));
}
static void AppendMethodIdString(StringBuilder b, MetadataReader metadata, MethodDefinitionHandle handle)
static void AppendMethodIdString(StringBuilder b, MetadataReader metadata, MethodDefinitionHandle handle, bool cppCliDialect)
{
var methodDef = metadata.GetMethodDefinition(handle);
var declaringType = methodDef.GetDeclaringType();
@ -175,7 +204,7 @@ namespace ICSharpCode.Decompiler.Documentation @@ -175,7 +204,7 @@ namespace ICSharpCode.Decompiler.Documentation
// Parameters
var signature = methodDef.DecodeSignature(
new IdStringSignatureTypeProvider(),
new IdStringSignatureTypeProvider(cppCliDialect),
new MetadataGenericContext(handle, metadata));
AppendParameterList(b, signature.ParameterTypes);
@ -202,7 +231,7 @@ namespace ICSharpCode.Decompiler.Documentation @@ -202,7 +231,7 @@ namespace ICSharpCode.Decompiler.Documentation
}
}
static void AppendPropertyIdString(StringBuilder b, MetadataReader metadata, PropertyDefinitionHandle handle)
static void AppendPropertyIdString(StringBuilder b, MetadataReader metadata, PropertyDefinitionHandle handle, bool cppCliDialect)
{
var propertyDef = metadata.GetPropertyDefinition(handle);
@ -211,15 +240,49 @@ namespace ICSharpCode.Decompiler.Documentation @@ -211,15 +240,49 @@ namespace ICSharpCode.Decompiler.Documentation
b.Append('.');
var signature = propertyDef.DecodeSignature(
new IdStringSignatureTypeProvider(),
new IdStringSignatureTypeProvider(cppCliDialect),
new MetadataGenericContext(declaringType, metadata));
b.Append(metadata.GetString(propertyDef.Name).Replace('.', '#').Replace('<', '{').Replace('>', '}'));
string name = metadata.GetString(propertyDef.Name);
// The MSVC xml doc generator refers to a type's default indexed property by the
// C++/CLI keyword 'default' instead of the property's metadata name.
if (cppCliDialect && signature.ParameterTypes.Length > 0
&& name == GetDefaultMemberName(metadata, declaringType))
{
b.Append("default");
}
else
{
b.Append(name.Replace('.', '#').Replace('<', '{').Replace('>', '}'));
}
// Indexers have parameters
AppendParameterList(b, signature.ParameterTypes);
}
static string GetDefaultMemberName(MetadataReader metadata, TypeDefinitionHandle declaringType)
{
foreach (var h in metadata.GetTypeDefinition(declaringType).GetCustomAttributes())
{
var customAttribute = metadata.GetCustomAttribute(h);
if (!customAttribute.IsKnownAttribute(metadata, KnownAttribute.DefaultMember))
continue;
try
{
var value = customAttribute.DecodeValue(Metadata.MetadataExtensions.MinimalAttributeTypeProvider);
if (value.FixedArguments.Length == 1 && value.FixedArguments[0].Value is string name)
return name;
}
catch (BadImageFormatException)
{
}
catch (Metadata.EnumUnderlyingTypeResolveException)
{
}
}
return null;
}
static TypeDefinitionHandle FindDeclaringTypeOfProperty(MetadataReader metadata, PropertyDefinitionHandle propertyHandle)
{
var accessors = metadata.GetPropertyDefinition(propertyHandle).GetAccessors();
@ -278,10 +341,15 @@ namespace ICSharpCode.Decompiler.Documentation @@ -278,10 +341,15 @@ namespace ICSharpCode.Decompiler.Documentation
static bool IsAsciiDigit(char c) => c >= '0' && c <= '9';
/// <summary>
/// Signature type provider that produces ID string fragments.
/// Signature type provider that produces ID string fragments. With
/// <paramref name="cppCliDialect"/> set, produces the MSVC C++/CLI form
/// (custom modifiers rendered, arity markers kept on generic instantiations)
/// instead of the C#/Roslyn form.
/// </summary>
readonly struct IdStringSignatureTypeProvider : ISignatureTypeProvider<string, MetadataGenericContext>
readonly struct IdStringSignatureTypeProvider(bool cppCliDialect) : ISignatureTypeProvider<string, MetadataGenericContext>
{
readonly bool cppCliDialect = cppCliDialect;
public string GetPrimitiveType(PrimitiveTypeCode typeCode)
{
return typeCode switch {
@ -370,6 +438,10 @@ namespace ICSharpCode.Decompiler.Documentation @@ -370,6 +438,10 @@ namespace ICSharpCode.Decompiler.Documentation
}
else
{
// MSVC keeps the arity marker in front of the argument list
// (List`1{System.Int32}); Roslyn strips it (List{System.Int32}).
if (cppCliDialect)
sb.Append(genericType, i, markerEnd - i);
sb.Append('{');
for (int k = 0; k < arity; k++)
{
@ -459,10 +531,18 @@ namespace ICSharpCode.Decompiler.Documentation @@ -459,10 +531,18 @@ namespace ICSharpCode.Decompiler.Documentation
public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired)
{
// Custom modifiers are not part of the ID string: Roslyn ignores them (e.g. a
// virtual method's 'in' parameter carries modreq(InAttribute) but is
// documented as T@).
return unmodifiedType;
// Roslyn ignores custom modifiers entirely (e.g. a virtual method's 'in'
// parameter carries modreq(InAttribute) but is documented as T@).
if (!cppCliDialect)
return unmodifiedType;
// The MSVC xml doc generator renders a modifier after the modified type,
// e.g. System.Int32!System.Runtime.CompilerServices.IsConst for a C++/CLI
// 'const int' parameter. Its documented mapping is '!' for modopt and '|'
// for modreq, but observed output uses '|' only for modreq(IsVolatile);
// modreq(IsByValue) on conversion operator operands is rendered with '!'.
char prefix = isRequired && modifier == "System.Runtime.CompilerServices.IsVolatile"
? '|' : '!';
return unmodifiedType + prefix + modifier;
}
public string GetPinnedType(string elementType)

Loading…
Cancel
Save