Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion kampose.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"stopOnIssues": true
},
"assemblies": [
"src/bin/Release/netstandard2.1/Kampute.DocToolkit.dll"
"src/bin/Release/**/Kampute.DocToolkit.dll"
],
"topics": [
"docs/**/*.md"
Expand Down
2 changes: 1 addition & 1 deletion src/Kampute.DocToolkit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<Title>Kampute.DocDotLib</Title>
<Description>Provides extensible pipeline for generating .NET API documentation by transforming assembly metadata and XML documentation into structured models, with automatic cross-reference resolution, support for multiple output formats (HTML, Markdown), and integration of conceptual topics.</Description>
<Version>2.0.0</Version>
<Version>2.0.1</Version>
<Company>Kampute</Company>
<Authors>Kambiz Khojasteh</Authors>
<Copyright>Copyright (c) 2025 Kampute</Copyright>
Expand Down
96 changes: 83 additions & 13 deletions src/Metadata/Adapters/CompositeTypeAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Kampute.DocToolkit.Metadata.Adapters
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

/// <summary>
/// An abstract adapter that wraps a composite <see cref="Type"/> such as a class or struct and provides metadata access.
Expand Down Expand Up @@ -311,26 +312,95 @@ protected virtual bool IsExplicitMember(MemberInfo member)
/// Determines whether an explicit interface implementation is a compiler-generated bridge method.
/// </summary>
/// <param name="method">The reflection information of the method to check.</param>
/// <returns><see langword="true"/> if the method is is a compiler-generated bridge method; otherwise, <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if the method is a compiler-generated bridge method; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="method"/> is <see langword="null"/>.</exception>
/// <remarks>
/// A compiler-generated bridge method is created by the C# compiler to handle explicit interface implementations that
/// involve 'in' parameters. These methods are not part of the original source code and should be excluded from metadata
/// representation.
/// <para>
/// This method checks for the presence of <c>in</c> parameters and attempts to find a corresponding public method for
/// the explicit interface implementation. If such a method exists, it indicates that the explicit interface method is
/// a compiler-generated bridge method.
/// </para>
/// </remarks>
protected virtual bool IsCompilerGeneratedBridgeMethod(MethodInfo method)
{
if (method is null)
throw new ArgumentNullException(nameof(method));

var parameters = method.GetParameters();

// Compiler-generated bridge methods are created for explicit interface implementations
// that have by-ref parameters (in, ref, out). These methods must have a corresponding
// public method with the same signature.
return parameters.Any(static p => p.ParameterType.IsByRef) && Reflection.GetMethod
(
name: method.Name.SubstringAfterLast('.'),
bindingAttr: BindingFlags.DeclaredOnly | BindingFlags.Public | (method.IsStatic ? BindingFlags.Static : BindingFlags.Instance),
binder: null,
types: [.. parameters.Select(static p => p.ParameterType)],
modifiers: null
) is not null;
if (!ContainsInParameter(parameters))
return false;

var methodName = method.Name.SubstringAfterLastOrSelf('.'); // Exclude interface name
var bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Public | (method.IsStatic ? BindingFlags.Static : BindingFlags.Instance);

return method.IsGenericMethod
? HasMatchingPublicGenericMethod()
: HasMatchingPublicNonGenericMethod();


[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool ContainsInParameter(ParameterInfo[] parameters)
{
for (var i = 0; i < parameters.Length; ++i)
{
if (parameters[i].IsIn)
return true;
}
return false;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool HasMatchingPublicNonGenericMethod()
{
return Reflection.GetMethod
(
name: methodName,
bindingAttr: bindingFlags,
binder: null,
types: [.. parameters.Select(static p => p.ParameterType)],
modifiers: null
) is not null;
}

bool HasMatchingPublicGenericMethod()
{
var genericArity = method.GetGenericArguments().Length;
foreach (var candidate in Reflection.GetMember(methodName, bindingFlags))
{
if (candidate is not MethodInfo { IsGenericMethod: true } candidateMethod)
continue;

if (candidateMethod.GetGenericArguments().Length != genericArity)
continue;

var candidateParameters = candidateMethod.GetParameters();
if (candidateParameters.Length != parameters.Length)
continue;

var match = true;
for (var i = 0; i < parameters.Length && match; ++i)
match = ParametersMatch(parameters[i], candidateParameters[i]);

if (match)
return true;
}

return false;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool ParametersMatch(ParameterInfo parameter1, ParameterInfo parameter2)
{
if (parameter1.ParameterType.IsGenericMethodParameter != parameter2.ParameterType.IsGenericMethodParameter)
return false;

return parameter1.ParameterType.IsGenericMethodParameter
? parameter1.ParameterType.GenericParameterPosition == parameter2.ParameterType.GenericParameterPosition
: AdapterHelper.HaveSameDeclarationScope(parameter1.ParameterType, parameter2.ParameterType);
}
}
}
}
22 changes: 20 additions & 2 deletions src/Metadata/Adapters/TypeParameterAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,26 @@ private static bool HasDefaultConstructor(IType type)
if (type.IsInterface || type.IsEnum)
return false;

return type is IWithConstructors { HasConstructors: true } withConstructors
&& withConstructors.Constructors.Any(static c => c.IsDefaultConstructor);
return IsAccessibleType(type)
&& type is IWithConstructors { HasConstructors: true } typeWithConstructors
&& typeWithConstructors.Constructors.Any(static c => c.IsPublic && c.IsDefaultConstructor);
}

/// <summary>
/// Determines whether the specified type and its declaring types are accessible.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><see langword="true"/> if the type and its declaring types are accessible; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAccessibleType(IType type)
{
for (var current = type; current is not null; current = current.DeclaringType)
{
if (!current.IsPublic || current is IClassType { IsAbstract: true })
return false;
}

return true;
}
}
}
10 changes: 7 additions & 3 deletions src/Metadata/IConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ namespace Kampute.DocToolkit.Metadata
public interface IConstructor : ITypeMember, IWithParameters, IWithOverloads
{
/// <summary>
/// Gets a value indicating whether this constructor is the default public constructor of a type.
/// Gets a value indicating whether this constructor is the default constructor of a type.
/// </summary>
/// <value>
/// <see langword="true"/> if this constructor is a default public constructor; otherwise, <see langword="false"/>.
/// <see langword="true"/> if this constructor is a default constructor; otherwise, <see langword="false"/>.
/// </value>
bool IsDefaultConstructor => !IsStatic && Visibility == MemberVisibility.Public && Parameters.Count == 0;
/// <remarks>
/// A constructor is considered a default constructor if it is not static and has no parameters.
/// The visibility of the constructor is not considered.
/// </remarks>
bool IsDefaultConstructor => !IsStatic && Parameters.Count == 0;

/// <summary>
/// Gets the constructor in the base class that has the same signature as this constructor, if any.
Expand Down
24 changes: 21 additions & 3 deletions src/Models/CompositeTypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,22 @@ public abstract class CompositeTypeModel : TypeModel<ICompositeType>
protected CompositeTypeModel(object declaringEntity, ICompositeType type)
: base(declaringEntity, type)
{
constructors = new(() =>
{
if (Metadata.Constructors.Count == 0)
return [];

var all = new List<ConstructorModel>(Metadata.Constructors.Count);
all.AddRange(Metadata.Constructors.Select(c => new ConstructorModel(this, c)));

// Omit the default constructor if it's the only one and has no documentation.
if (all.Count == 1 && all[0].Metadata.IsDefaultConstructor && all[0].Doc.IsEmpty)
return [];

return all;
});

fields = new(() => [.. Metadata.Fields.Select(f => new FieldModel(this, f))]);
constructors = new(() => [.. Metadata.Constructors.Select(c => new ConstructorModel(this, c))]);
properties = new(() => [.. Metadata.Properties.Select(p => new PropertyModel(this, p))]);
methods = new(() => [.. Metadata.Methods.Select(m => new MethodModel(this, m))]);
events = new(() => [.. Metadata.Events.Select(e => new EventModel(this, e))]);
Expand Down Expand Up @@ -84,8 +98,12 @@ protected CompositeTypeModel(object declaringEntity, ICompositeType type)
/// The constructors in the collection are ordered by their number of parameters.
/// </value>
/// <remarks>
/// If the type only has a default public constructor without any documentation, the constructor is considered implicit
/// and the collection will be empty.
/// If a type defines only a default constructor and that constructor has no documentation, it is treated as
/// compiler-generated and omitted from the collection. This reduces clutter in the generated documentation by
/// hiding trivial constructors that add no useful information.
/// <note type="hint" title="Hint">
/// The <see cref="Metadata"/> property can still be used to access the default constructor’s metadata if needed.
/// </note>
/// </remarks>
public IReadOnlyList<ConstructorModel> Constructors => constructors.Value;

Expand Down
2 changes: 1 addition & 1 deletion src/XmlDoc/XmlDocInspectionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public enum XmlDocInspectionOptions
Overloads = 0x1000,

/// <summary>
/// Omit reporting missing <c>summary</c> tags for default public constructors that have no overloads.
/// Omit reporting missing <c>summary</c> tags for default constructors that have no overloads.
/// </summary>
OmitImplicitlyCreatedConstructors = 0x2000,

Expand Down
93 changes: 90 additions & 3 deletions tests/Acme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,28 @@ void InterfaceMethod() { }
/// <returns>The value.</returns>
T InterfaceGenericMethod<T>(T value) where T : struct => value;

/// <summary>
/// A generic method with an in parameter in the interface.
/// </summary>
/// <typeparam name="T1">The type parameter.</typeparam>
/// <param name="value">The in parameter.</param>
/// <returns>The value.</returns>
T1 InterfaceGenericMethodWithInParam<T1>(in T1 value) where T1 : struct => value;

/// <summary>
/// An overload of the generic method with an in parameter in the interface.
/// </summary>
/// <typeparam name="T2">The type parameter.</typeparam>
/// <param name="value">The in parameter.</param>
/// <param name="dummy">A dummy parameter to differentiate the overload.</param>
/// <returns>The value.</returns>
T2 InterfaceGenericMethodWithInParam<T2>(in T2 value, bool dummy) where T2 : struct => value;

/// <summary>
/// A method with an in parameter.
/// </summary>
/// <param name="i">The in parameter.</param>
void InterfaceMethodWithInParam(in int i) { }
/// <param name="dec">The in parameter.</param>
void InterfaceMethodWithInParam(in decimal dec) { }

/// <summary>
/// A method with a ref parameter.
Expand Down Expand Up @@ -232,6 +249,22 @@ public class SampleOperators : ISampleInterface
/// </summary>
public void operator --() { }

/// <summary>
/// Xor operator.
/// </summary>
/// <param name="x">The left operand.</param>
/// <param name="y">The right operand.</param>
/// <returns>The result.</returns>
public static SampleOperators operator ^(SampleOperators x, SampleOperators y) => x;

/// <summary>
/// Xor operator with integer.
/// </summary>
/// <param name="x">The left operand.</param>
/// <param name="y">The right operand.</param>
/// <returns>The result.</returns>
public static SampleOperators operator ^(SampleOperators x, int y) => x;

/// <summary>
/// Implicit conversion to string.
/// </summary>
Expand Down Expand Up @@ -370,11 +403,23 @@ void ISampleInterface.InterfaceMethod() { }
/// <inheritdoc/>
public T InterfaceGenericMethod<T>(T value) where T : struct => value;

/// <summary>
/// Implements the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
public T1 InterfaceGenericMethodWithInParam<T1>(in T1 value) where T1 : struct => value;

/// <summary>
/// Implements an overload of the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
public T2 InterfaceGenericMethodWithInParam<T2>(in T2 value, bool dummy) where T2 : struct => value;

/// <summary>
/// Explicitly implements the interface method with in parameter.
/// </summary>
/// <inheritdoc/>
void ISampleInterface.InterfaceMethodWithInParam(in int i) { }
void ISampleInterface.InterfaceMethodWithInParam(in decimal dec) { }

/// <summary>
/// Explicitly implements the interface method with out parameter.
Expand Down Expand Up @@ -667,6 +712,24 @@ public DeepInnerGenericClass() { }
/// <inheritdoc/>
void ISampleInterface.InterfaceMethod() { }

/// <summary>
/// Implements the interface method with in parameter.
/// </summary>
/// <inheritdoc/>
public void InterfaceMethodWithInParam(in decimal dec) { }

/// <summary>
/// Implements the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
public T1 InterfaceGenericMethodWithInParam<T1>(in T1 value) where T1 : struct => value;

/// <summary>
/// Implements an overload of the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
public T2 InterfaceGenericMethodWithInParam<T2>(in T2 value, bool dummy) where T2 : struct => value;

/// <summary>
/// Explicitly implements the static interface method.
/// </summary>
Expand Down Expand Up @@ -881,6 +944,24 @@ readonly void ISampleInterface.InterfaceMethod() { }
/// <inheritdoc/>
static void ISampleInterface.InterfaceStaticMethod() { }

/// <summary>
/// Explicitly implements the interface method with in parameter.
/// </summary>
/// <inheritdoc/>
readonly void ISampleInterface.InterfaceMethodWithInParam(in decimal dec) { }

/// <summary>
/// Explicitly implements the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
readonly T1 ISampleInterface.InterfaceGenericMethodWithInParam<T1>(in T1 value) where T1 : struct => value;

/// <summary>
/// Explicitly implements an overload of the generic interface method with in parameter.
/// </summary>
/// <inheritdoc/>
readonly T2 ISampleInterface.InterfaceGenericMethodWithInParam<T2>(in T2 value, bool dummy) where T2 : struct => value;

/// <summary>
/// Explicitly implements the GetEnumerator method.
/// </summary>
Expand Down Expand Up @@ -1025,6 +1106,12 @@ public interface ISampleExtendedGenericInterface<T, in U, out V> : ISampleGeneri
/// </summary>
public interface ISampleExtendedConstructedGenericInterface : ISampleExtendedGenericInterface<object, int, string>
{
/// <summary>
/// An indexer.
/// </summary>
/// <param name="i">The index.</param>
/// <value>The indexed value.</value>
int this[int i] { get; }
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion tests/Languages/CSharpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ public string FormatDefinition_ForConstructors_ReturnsExpectedString(Type type,
[TestCase(typeof(Acme.SampleMethods), nameof(Acme.SampleMethods.VirtualMethod), ExpectedResult = "protected virtual void VirtualMethod(int i)")]
[TestCase(typeof(Acme.SampleMethods), nameof(Acme.SampleMethods.ToString), ExpectedResult = "public sealed override string ToString()")]
[TestCase(typeof(Acme.SampleMethods), "Acme.ISampleInterface.InterfaceMethod", ExpectedResult = "void ISampleInterface.InterfaceMethod()")]
[TestCase(typeof(Acme.SampleMethods), "Acme.ISampleInterface.InterfaceMethodWithInParam", ExpectedResult = "void ISampleInterface.InterfaceMethodWithInParam(in int i)")]
[TestCase(typeof(Acme.SampleMethods), "Acme.ISampleInterface.InterfaceMethodWithInParam", ExpectedResult = "void ISampleInterface.InterfaceMethodWithInParam(in decimal dec)")]
[TestCase(typeof(Acme.SampleMethods), "Acme.ISampleInterface.InterfaceMethodWithOutParam", ExpectedResult = "void ISampleInterface.InterfaceMethodWithOutParam(out double d)")]
Comment thread
kampute marked this conversation as resolved.
[TestCase(typeof(Acme.SampleMethods), nameof(Acme.SampleMethods.InterfaceMethodWithRefParam), ExpectedResult = "public void InterfaceMethodWithRefParam(ref string s)")]
[TestCase(typeof(Acme.SampleGenericClass<>.InnerGenericClass<,>.DeepInnerGenericClass), nameof(Acme.SampleGenericClass<>.InnerGenericClass<,>.DeepInnerGenericClass.Method), ExpectedResult = "public abstract object Method(T t, U u, V v)")]
Expand Down
Loading