diff --git a/ICSharpCode.Decompiler.Tests/Documentation/.gitignore b/ICSharpCode.Decompiler.Tests/Documentation/.gitignore
new file mode 100644
index 000000000..f0f1af068
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Documentation/.gitignore
@@ -0,0 +1,2 @@
+/IdStringProbe.dll
+/IdStringProbe.pdb
diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.cpp b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.cpp
new file mode 100644
index 000000000..dfc719a9e
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.cpp
@@ -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;
+
+/// Probe class.
+public ref class IdProbe
+{
+public:
+ /// const int parameter; known baseline: System.Int32!System.Runtime.CompilerServices.IsConst
+ static int ConstValue(const int x) { return x; }
+
+ /// long parameter; modopt(IsLong) on Int32
+ static void TakesLong(long x) { (void)x; }
+
+ /// unsigned long parameter; modopt(IsLong) on UInt32
+ static void TakesULong(unsigned long x) { (void)x; }
+
+ /// char pointer parameter; modopt(IsSignUnspecifiedByte) under a pointer
+ static void TakesCharPtr(char* p) { (void)p; }
+
+ /// const char pointer parameter; IsConst and IsSignUnspecifiedByte together (modifier ordering)
+ static void TakesConstCharPtr(const char* p) { (void)p; }
+
+ /// volatile int pointer parameter; modreq(IsVolatile): the open question is whether MSVC renders '|', '!', or omits it
+ static void TakesVolatilePtr(volatile int* p) { (void)p; }
+
+ /// const volatile int pointer parameter; modreq and modopt mixed on one type
+ static void TakesConstVolatilePtr(const volatile int* p) { (void)p; }
+
+ /// tracking reference parameter; expected System.Int32@
+ static void TakesTrackingRef(int% r) { r = 0; }
+
+ /// const tracking reference parameter; modifier placement relative to the '@'
+ static void TakesConstTrackingRef(const int% r) { (void)r; }
+
+ /// native reference parameter under /clr; representation evidence
+ static void TakesNativeRef(int& r) { r = 0; }
+
+ /// two-dimensional managed array parameter; expected System.Int32[0:,0:]
+ static void TakesArray2(array^ a) { (void)a; }
+
+ /// nested type of a generic instantiation; how does MSVC distribute the type arguments?
+ static void TakesEnumerator(List::Enumerator e) { (void)e; }
+
+ /// generic method; expected arity marker and grave-accent parameter encoding
+ generic static void Gen(T t) { (void)t; }
+
+ /// conversion operator with a by-value ref-class parameter; the MSVC docs show IdProbe!IsByValue and a '~' return type
+ static explicit operator int(IdProbe x) { (void)x; return 0; }
+
+ /// indexed property with a long parameter; modopt inside the indexer parentheses
+ property int default[long]
+ {
+ int get(long i) { return (int)i; }
+ void set(long i, int value) { (void)i; (void)value; }
+ }
+};
+
+/// Generic ref class; arity in the type ID.
+generic public ref class GBox
+{
+public:
+ /// method on a generic type taking T; expected grave-accent zero
+ void Hold(T item) { (void)item; }
+};
+
+/// Consumer of an instantiated generic type.
+public ref class GBoxUser
+{
+public:
+ /// generic instantiation in a signature; expected GBox{System.Int32}
+ static void Use(GBox^ b) { (void)b; }
+};
diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.il b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.il
new file mode 100644
index 000000000..19b14bde2
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.il
@@ -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 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
+ ) 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
+ 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 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
diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.xml b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.xml
new file mode 100644
index 000000000..07f746190
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProbe.xml
@@ -0,0 +1,68 @@
+
+
+
+ "IdStringProbe"
+
+
+
+ generic instantiation in a signature; expected GBox{System.Int32}
+
+
+ Consumer of an instantiated generic type.
+
+
+ method on a generic type taking T; expected grave-accent zero
+
+
+ Generic ref class; arity in the type ID.
+
+
+ indexed property with a long parameter; modopt inside the indexer parentheses
+
+
+ conversion operator with a by-value ref-class parameter; the MSVC docs show IdProbe!IsByValue and a '~' return type
+
+
+ generic method; expected arity marker and grave-accent parameter encoding
+
+
+ nested type of a generic instantiation; how does MSVC distribute the type arguments?
+
+
+ two-dimensional managed array parameter; expected System.Int32[0:,0:]
+
+
+ native reference parameter under /clr; representation evidence
+
+
+ const tracking reference parameter; modifier placement relative to the '@'
+
+
+ tracking reference parameter; expected System.Int32@
+
+
+ const volatile int pointer parameter; modreq and modopt mixed on one type
+
+
+ volatile int pointer parameter; modreq(IsVolatile): the open question is whether MSVC renders '|', '!', or omits it
+
+
+ const char pointer parameter; IsConst and IsSignUnspecifiedByte together (modifier ordering)
+
+
+ char pointer parameter; modopt(IsSignUnspecifiedByte) under a pointer
+
+
+ unsigned long parameter; modopt(IsLong) on UInt32
+
+
+ long parameter; modopt(IsLong) on Int32
+
+
+ const int parameter; known baseline: System.Int32!System.Runtime.CompilerServices.IsConst
+
+
+ Probe class.
+
+
+
\ No newline at end of file
diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
index 2b167fa69..e065904ad 100644
--- a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
+++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs
@@ -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
#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 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 CollectIdStringCandidates(PEFile pe)
+ {
+ var md = pe.Metadata;
+ var candidates = new HashSet();
+ foreach (var th in md.TypeDefinitions)
+ {
+ var td = md.GetTypeDefinition(th);
+ if (md.GetString(td.Name) == "")
+ 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 encodeParameter)
{
return BuildAssemblyWithMethods(encodeParameter);
diff --git a/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs b/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
index 881762a07..df073a459 100644
--- a/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
+++ b/ICSharpCode.Decompiler/Documentation/IdStringProvider.cs
@@ -36,9 +36,38 @@ namespace ICSharpCode.Decompiler.Documentation
{
#region GetIdString
///
- /// 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.
///
public static string GetIdString(this MetadataFile module, EntityHandle handle)
+ {
+ return GetIdString(module, handle, cppCliDialect: false);
+ }
+
+ ///
+ /// 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).
+ ///
+ 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;
+ }
+
+ 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
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
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
// 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
}
}
- 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
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
static bool IsAsciiDigit(char c) => c >= '0' && c <= '9';
///
- /// Signature type provider that produces ID string fragments.
+ /// Signature type provider that produces ID string fragments. With
+ /// set, produces the MSVC C++/CLI form
+ /// (custom modifiers rendered, arity markers kept on generic instantiations)
+ /// instead of the C#/Roslyn form.
///
- readonly struct IdStringSignatureTypeProvider : ISignatureTypeProvider
+ readonly struct IdStringSignatureTypeProvider(bool cppCliDialect) : ISignatureTypeProvider
{
+ readonly bool cppCliDialect = cppCliDialect;
+
public string GetPrimitiveType(PrimitiveTypeCode typeCode)
{
return typeCode switch {
@@ -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
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)