mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The Documentation namespace (XmlDocumentationProvider, IdStringProvider, XmlDocLoader) had no test coverage at all, even though XmlDoc is listed as an implemented C# 1 feature on the wiki Language-Feature-Roadmap. These tests pin ID-string generation and FindEntity round-trips for the intricate encodings (nested generic types, `0/``0 type-parameter references, ref/out @ suffixes, pointer and jagged/multi-dimensional array forms incl. [0:,0:], conversion operators with the ~ReturnType suffix, indexers, #ctor) plus XmlDocumentationProvider lookup by ID string and by entity against a generated doc file. All green. Assisted-by: Claude:claude-fable-5:Claude Codepull/3857/head
1 changed files with 291 additions and 0 deletions
@ -0,0 +1,291 @@
@@ -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 |
||||
{ |
||||
/// <summary>Documentation for the sample class.</summary>
|
||||
public class Sample |
||||
{ |
||||
/// <summary>Documentation for the field.</summary>
|
||||
public int Field; |
||||
|
||||
/// <summary>Documentation for the event.</summary>
|
||||
public event EventHandler Event; |
||||
|
||||
/// <summary>Documentation for the constructor.</summary>
|
||||
public Sample(int x) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>Documentation for the indexer.</summary>
|
||||
public string this[int index] => null; |
||||
|
||||
/// <summary>Documentation for ref/out parameters.</summary>
|
||||
public void MethodWithRefOutParams(ref int x, out string y) |
||||
{ |
||||
y = null; |
||||
} |
||||
|
||||
/// <summary>Documentation for params.</summary>
|
||||
public void MethodWithParamsArray(params object[] args) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>Documentation for pointers.</summary>
|
||||
public unsafe void MethodWithPointer(int* p) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>Documentation for arrays.</summary>
|
||||
public void MethodWithArrays(int[] a, string[,] b, int[][] c) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>Documentation for the addition operator.</summary>
|
||||
public static Sample operator +(Sample a, Sample b) |
||||
{ |
||||
return a; |
||||
} |
||||
|
||||
/// <summary>Documentation for the implicit conversion.</summary>
|
||||
public static implicit operator int(Sample a) |
||||
{ |
||||
return 0; |
||||
} |
||||
|
||||
/// <summary>Documentation for the explicit conversion.</summary>
|
||||
public static explicit operator Sample(int a) |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/// <summary>Documentation for the generic sample class.</summary>
|
||||
public class GenericSample<A, B> |
||||
{ |
||||
/// <summary>Documentation for the nested generic class.</summary>
|
||||
public class Nested<C> |
||||
{ |
||||
/// <summary>Documentation for the nested method.</summary>
|
||||
public void NestedMethod(A a, C c) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
/// <summary>Documentation for the method using class type parameters.</summary>
|
||||
public void Method(A a, B b) |
||||
{ |
||||
} |
||||
|
||||
/// <summary>Documentation for the generic method.</summary>
|
||||
public C GenericMethod<C>(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."); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue