diff --git a/ICSharpCode.Decompiler.Tests/Documentation/XmlDocTests.cs b/ICSharpCode.Decompiler.Tests/Documentation/XmlDocTests.cs
new file mode 100644
index 000000000..63df92d30
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/Documentation/XmlDocTests.cs
@@ -0,0 +1,291 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+using ICSharpCode.Decompiler.Documentation;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.Tests.Helpers;
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.Decompiler.TypeSystem.Implementation;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.Decompiler.Tests.Documentation
+{
+ [TestFixture]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable",
+ Justification = "The PEFile is disposed by the OneTimeTearDown method.")]
+ public class XmlDocTests
+ {
+ // Compiled by Tester.CompileCSharp with CompilerOptions.ProcessXmlDoc, so the member
+ // ID strings in the documentation file come from the compiler itself. Looking an
+ // entity's documentation up therefore checks IdStringProvider.GetIdString against
+ // the compiler-generated ID for the same member.
+ const string SampleCode = """
+ using System;
+
+ namespace XmlDocSamples
+ {
+ /// Documentation for the sample class.
+ public class Sample
+ {
+ /// Documentation for the field.
+ public int Field;
+
+ /// Documentation for the event.
+ public event EventHandler Event;
+
+ /// Documentation for the constructor.
+ public Sample(int x)
+ {
+ }
+
+ /// Documentation for the indexer.
+ public string this[int index] => null;
+
+ /// Documentation for ref/out parameters.
+ public void MethodWithRefOutParams(ref int x, out string y)
+ {
+ y = null;
+ }
+
+ /// Documentation for params.
+ public void MethodWithParamsArray(params object[] args)
+ {
+ }
+
+ /// Documentation for pointers.
+ public unsafe void MethodWithPointer(int* p)
+ {
+ }
+
+ /// Documentation for arrays.
+ public void MethodWithArrays(int[] a, string[,] b, int[][] c)
+ {
+ }
+
+ /// Documentation for the addition operator.
+ public static Sample operator +(Sample a, Sample b)
+ {
+ return a;
+ }
+
+ /// Documentation for the implicit conversion.
+ public static implicit operator int(Sample a)
+ {
+ return 0;
+ }
+
+ /// Documentation for the explicit conversion.
+ public static explicit operator Sample(int a)
+ {
+ return null;
+ }
+ }
+
+ /// Documentation for the generic sample class.
+ public class GenericSample
+ {
+ /// Documentation for the nested generic class.
+ public class Nested
+ {
+ /// Documentation for the nested method.
+ public void NestedMethod(A a, C c)
+ {
+ }
+ }
+
+ /// Documentation for the method using class type parameters.
+ public void Method(A a, B b)
+ {
+ }
+
+ /// Documentation for the generic method.
+ public C GenericMethod(C input, A other)
+ {
+ return input;
+ }
+ }
+ }
+ """;
+
+ const string NS = "XmlDocSamples";
+
+ string sourceFile;
+ CompilerResults results;
+ PEFile peFile;
+ ICompilation compilation;
+ XmlDocumentationProvider documentation;
+
+ [OneTimeSetUp]
+ public async Task CompileSample()
+ {
+ sourceFile = Path.Combine(Path.GetTempPath(), $"XmlDocSamples-{Guid.NewGuid():N}.cs");
+ File.WriteAllText(sourceFile, SampleCode);
+ results = await Tester.CompileCSharp(sourceFile,
+ CompilerOptions.UseRoslynLatest | CompilerOptions.Optimize | CompilerOptions.Library | CompilerOptions.ProcessXmlDoc);
+ peFile = new PEFile(results.PathToAssembly,
+ new FileStream(results.PathToAssembly, FileMode.Open, FileAccess.Read));
+ compilation = new SimpleCompilation(peFile.WithOptions(TypeSystemOptions.Default), MinimalCorlib.Instance);
+ documentation = new XmlDocumentationProvider(Path.ChangeExtension(results.PathToAssembly, ".xml"));
+ }
+
+ [OneTimeTearDown]
+ public void Cleanup()
+ {
+ peFile?.Dispose();
+ if (results != null)
+ {
+ string xmlFile = Path.ChangeExtension(results.PathToAssembly, ".xml");
+ results.DeleteTempFiles();
+ if (File.Exists(xmlFile))
+ File.Delete(xmlFile);
+ }
+ if (sourceFile != null && File.Exists(sourceFile))
+ File.Delete(sourceFile);
+ }
+
+ ITypeDefinition GetTypeDefinition(string reflectionName)
+ {
+ var typeDef = compilation.FindType(new FullTypeName(reflectionName)).GetDefinition();
+ Assert.That(typeDef, Is.Not.Null, $"Could not resolve {reflectionName}");
+ return typeDef;
+ }
+
+ void AssertIdStringRoundTrip(IEntity entity, string expectedIdString)
+ {
+ Assert.That(entity.GetIdString(), Is.EqualTo(expectedIdString));
+ var found = IdStringProvider.FindEntity(expectedIdString, new SimpleTypeResolveContext(compilation));
+ Assert.That(found, Is.EqualTo(entity), $"FindEntity did not round-trip {expectedIdString}");
+ }
+
+ void AssertDocumentation(IEntity entity, string expectedText)
+ {
+ Assert.That(documentation.GetDocumentation(entity), Does.Contain(expectedText),
+ $"Documentation not found for {entity.GetIdString()}");
+ }
+
+ [Test]
+ public void TypeIdStrings()
+ {
+ AssertIdStringRoundTrip(GetTypeDefinition($"{NS}.Sample"),
+ $"T:{NS}.Sample");
+ AssertIdStringRoundTrip(GetTypeDefinition($"{NS}.GenericSample`2"),
+ $"T:{NS}.GenericSample`2");
+ AssertIdStringRoundTrip(GetTypeDefinition($"{NS}.GenericSample`2+Nested`1"),
+ $"T:{NS}.GenericSample`2.Nested`1");
+ }
+
+ [Test]
+ public void FieldEventAndConstructorIdStrings()
+ {
+ var sample = GetTypeDefinition($"{NS}.Sample");
+ AssertIdStringRoundTrip(sample.Fields.Single(f => f.Name == "Field"),
+ $"F:{NS}.Sample.Field");
+ AssertIdStringRoundTrip(sample.Events.Single(),
+ $"E:{NS}.Sample.Event");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.IsConstructor),
+ $"M:{NS}.Sample.#ctor(System.Int32)");
+ }
+
+ [Test]
+ public void MethodIdStrings()
+ {
+ var sample = GetTypeDefinition($"{NS}.Sample");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "MethodWithRefOutParams"),
+ $"M:{NS}.Sample.MethodWithRefOutParams(System.Int32@,System.String@)");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "MethodWithParamsArray"),
+ $"M:{NS}.Sample.MethodWithParamsArray(System.Object[])");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "MethodWithPointer"),
+ $"M:{NS}.Sample.MethodWithPointer(System.Int32*)");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "MethodWithArrays"),
+ $"M:{NS}.Sample.MethodWithArrays(System.Int32[],System.String[0:,0:],System.Int32[][])");
+ }
+
+ [Test]
+ public void IndexerIdString()
+ {
+ var sample = GetTypeDefinition($"{NS}.Sample");
+ AssertIdStringRoundTrip(sample.Properties.Single(p => p.IsIndexer),
+ $"P:{NS}.Sample.Item(System.Int32)");
+ }
+
+ [Test]
+ public void OperatorIdStrings()
+ {
+ var sample = GetTypeDefinition($"{NS}.Sample");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "op_Addition"),
+ $"M:{NS}.Sample.op_Addition({NS}.Sample,{NS}.Sample)");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "op_Implicit"),
+ $"M:{NS}.Sample.op_Implicit({NS}.Sample)~System.Int32");
+ AssertIdStringRoundTrip(sample.Methods.Single(m => m.Name == "op_Explicit"),
+ $"M:{NS}.Sample.op_Explicit(System.Int32)~{NS}.Sample");
+ }
+
+ [Test]
+ public void GenericMemberIdStrings()
+ {
+ var generic = GetTypeDefinition($"{NS}.GenericSample`2");
+ AssertIdStringRoundTrip(generic.Methods.Single(m => m.Name == "Method"),
+ $"M:{NS}.GenericSample`2.Method(`0,`1)");
+ AssertIdStringRoundTrip(generic.Methods.Single(m => m.Name == "GenericMethod"),
+ $"M:{NS}.GenericSample`2.GenericMethod``1(``0,`0)");
+ var nested = GetTypeDefinition($"{NS}.GenericSample`2+Nested`1");
+ AssertIdStringRoundTrip(nested.Methods.Single(m => m.Name == "NestedMethod"),
+ $"M:{NS}.GenericSample`2.Nested`1.NestedMethod(`0,`2)");
+ }
+
+ [Test]
+ public void LookupByIdString()
+ {
+ Assert.That(documentation.GetDocumentation($"T:{NS}.Sample"),
+ Does.Contain("Documentation for the sample class."));
+ Assert.That(documentation.GetDocumentation($"T:{NS}.DoesNotExist"), Is.Null);
+ }
+
+ [Test]
+ public void LookupByEntityMatchesCompilerGeneratedIds()
+ {
+ var sample = GetTypeDefinition($"{NS}.Sample");
+ AssertDocumentation(sample, "Documentation for the sample class.");
+ AssertDocumentation(sample.Fields.Single(f => f.Name == "Field"), "Documentation for the field.");
+ AssertDocumentation(sample.Events.Single(), "Documentation for the event.");
+ AssertDocumentation(sample.Methods.Single(m => m.IsConstructor), "Documentation for the constructor.");
+ AssertDocumentation(sample.Properties.Single(p => p.IsIndexer), "Documentation for the indexer.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "MethodWithRefOutParams"), "Documentation for ref/out parameters.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "MethodWithParamsArray"), "Documentation for params.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "MethodWithPointer"), "Documentation for pointers.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "MethodWithArrays"), "Documentation for arrays.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "op_Addition"), "Documentation for the addition operator.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "op_Implicit"), "Documentation for the implicit conversion.");
+ AssertDocumentation(sample.Methods.Single(m => m.Name == "op_Explicit"), "Documentation for the explicit conversion.");
+
+ var generic = GetTypeDefinition($"{NS}.GenericSample`2");
+ AssertDocumentation(generic, "Documentation for the generic sample class.");
+ AssertDocumentation(generic.Methods.Single(m => m.Name == "Method"), "Documentation for the method using class type parameters.");
+ AssertDocumentation(generic.Methods.Single(m => m.Name == "GenericMethod"), "Documentation for the generic method.");
+ var nested = GetTypeDefinition($"{NS}.GenericSample`2+Nested`1");
+ AssertDocumentation(nested, "Documentation for the nested generic class.");
+ AssertDocumentation(nested.Methods.Single(m => m.Name == "NestedMethod"), "Documentation for the nested method.");
+ }
+ }
+}