// 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."); } } }