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