Skip to content

Commit

Permalink
Features/cref resolving (#8)
Browse files Browse the repository at this point in the history
* cref resolving concept & improve snapshot tests

* markdown link building

* cref building for methods & fields

* cref resolving constructors

* generate docs
  • Loading branch information
DavidVollmers authored May 23, 2024
1 parent 75ed57d commit 0216125
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 30 deletions.
14 changes: 14 additions & 0 deletions Doki.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.Output.Json", "src\Dok
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.Output.Json.Tests", "tests\Doki.Output.Json.Tests\Doki.Output.Json.Tests.csproj", "{6CCD9EE6-B3FC-485F-9155-553165141B20}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.TestAssembly", "tests\assemblies\Doki.TestAssembly\Doki.TestAssembly.csproj", "{0293D689-DFDC-4A78-80D8-BFC11DB0A175}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.Tests.Common", "tests\Doki.Tests.Common\Doki.Tests.Common.csproj", "{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -46,6 +50,8 @@ Global
{A89D22B2-2427-4863-A2D9-9E1BEFF37C61} = {568576F3-3D48-459E-B4D2-1790DAE80E7A}
{00BCCBC0-A719-489C-A746-559B4D055B56} = {568576F3-3D48-459E-B4D2-1790DAE80E7A}
{6CCD9EE6-B3FC-485F-9155-553165141B20} = {8C7B5305-B599-4F08-B28B-DD9F1715DD51}
{0293D689-DFDC-4A78-80D8-BFC11DB0A175} = {08041208-BE3D-4BE8-9AF7-806B73985275}
{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC} = {8C7B5305-B599-4F08-B28B-DD9F1715DD51}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F31B87A-2BD3-4FB4-8C08-7E059A338D4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -96,5 +102,13 @@ Global
{6CCD9EE6-B3FC-485F-9155-553165141B20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CCD9EE6-B3FC-485F-9155-553165141B20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CCD9EE6-B3FC-485F-9155-553165141B20}.Release|Any CPU.Build.0 = Release|Any CPU
{0293D689-DFDC-4A78-80D8-BFC11DB0A175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0293D689-DFDC-4A78-80D8-BFC11DB0A175}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0293D689-DFDC-4A78-80D8-BFC11DB0A175}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0293D689-DFDC-4A78-80D8-BFC11DB0A175}.Release|Any CPU.Build.0 = Release|Any CPU
{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ Inheritance: [System.Object](https://learn.microsoft.com/en-us/dotnet/api/System
|Constructor|A constructor in the documentation.|
|Field|A field in the documentation.|
|Property|A property in the documentation.|
|Method|A method in the documentation.|


Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Implements: [System.IEquatable<Doki.MemberDocumentation>](https://learn.mi
|Namespace|Gets the namespace of the member.|
|Assembly|Gets the assembly of the member.|
|Summary|Gets the summary of the member.|
|IsDocumented|Gets a value indicating whether the type is documented.|
|ContentType||


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ Implements: [System.IEquatable<Doki.TypeDocumentationReference>](https://l
|EqualityContract||
|IsGeneric|Gets a value indicating whether the type is generic.|
|FullName|Gets the full name of the type.|
|IsDocumented|Gets a value indicating whether the type is documented.|
|IsMicrosoft|Gets a value indicating whether the type is from Microsoft.|
|BaseType|Gets the base type of the type.|
|GenericArguments|Gets the generic arguments of the type.|
Expand Down
5 changes: 5 additions & 0 deletions src/Doki.Abstractions/DocumentationContentType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,9 @@ public enum DocumentationContentType
/// A property in the documentation.
/// </summary>
Property,

/// <summary>
/// A method in the documentation.
/// </summary>
Method,
}
5 changes: 5 additions & 0 deletions src/Doki.Abstractions/MemberDocumentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ public record MemberDocumentation : DocumentationObject
/// </summary>
public XmlDocumentation? Summary { get; set; }

/// <summary>
/// Gets a value indicating whether the type is documented.
/// </summary>
public bool IsDocumented { get; init; }

public new DocumentationContentType ContentType { get; init; }
}
5 changes: 0 additions & 5 deletions src/Doki.Abstractions/TypeDocumentationReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ public record TypeDocumentationReference : MemberDocumentation
/// </summary>
public string FullName { get; init; } = null!;

/// <summary>
/// Gets a value indicating whether the type is documented.
/// </summary>
public bool IsDocumented { get; init; }

/// <summary>
/// Gets a value indicating whether the type is from Microsoft.
/// </summary>
Expand Down
54 changes: 41 additions & 13 deletions src/Doki.Output.Markdown/InternalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public static Text BuildText(this MarkdownBuilder builder, DocumentationObject o
text.Append(builder.BuildLinkTo(typeDocumentationReference));
text.Space();
break;
case MemberDocumentation memberDocumentation:
text.Append(builder.BuildLinkTo(memberDocumentation));
text.Space();
break;
default:
throw new NotSupportedException(
$"Unsupported {nameof(DocumentationObject)} type: {content.GetType().Name}");
Expand All @@ -74,27 +78,51 @@ public static string BuildRelativePath(this MarkdownBuilder builder, Documentati

public static Element BuildLinkTo(this MarkdownBuilder builder, DocumentationObject to, string? text = null)
{
var indexFile = to.ContentType is DocumentationContentType.Root or DocumentationContentType.Assembly
or DocumentationContentType.Namespace;

var asText = false;
string? relativePath = null;
if (to is TypeDocumentationReference typeDocumentationReference)
string relativePath;
switch (to)
{
text ??= typeDocumentationReference.IsDocumented
? typeDocumentationReference.Name
: typeDocumentationReference.FullName;
case TypeDocumentationReference typeDocumentationReference:
{
text ??= typeDocumentationReference.IsDocumented
? typeDocumentationReference.Name
: typeDocumentationReference.FullName;

asText = typeDocumentationReference is { IsDocumented: false, IsMicrosoft: false };

if (typeDocumentationReference.IsMicrosoft)
{
relativePath =
$"https://learn.microsoft.com/en-us/dotnet/api/{typeDocumentationReference.FullName}";
}
else
{
relativePath = builder.BuildRelativePath(to) + ".md";
}

break;
}
case MemberDocumentation memberDocumentation:
{
text ??= memberDocumentation.Name;

asText = typeDocumentationReference is { IsDocumented: false, IsMicrosoft: false };
asText = !memberDocumentation.IsDocumented;

if (typeDocumentationReference.IsMicrosoft)
relativePath = builder.BuildRelativePath(memberDocumentation.Parent!) + ".md";
break;
}
default:
{
relativePath = $"https://learn.microsoft.com/en-us/dotnet/api/{typeDocumentationReference.FullName}";
var indexFile = to.ContentType is DocumentationContentType.Root or DocumentationContentType.Assembly
or DocumentationContentType.Namespace;

relativePath = indexFile
? builder.BuildRelativePath(to, "README.md")
: builder.BuildRelativePath(to) + ".md";
break;
}
}

relativePath ??= indexFile ? builder.BuildRelativePath(to, "README.md") : builder.BuildRelativePath(to) + ".md";

text ??= to.Id;
if (text == "root") text = "Packages";

Expand Down
125 changes: 117 additions & 8 deletions src/Doki/DocumentationGenerator.Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ private IEnumerable<MemberDocumentation> BuildFieldDocumentation(Type type, Docu
Namespace = field.DeclaringType.Namespace,
Assembly = fieldAssembly.Name,
Parent = parent,
IsDocumented = true
};

if (summary != null)
Expand Down Expand Up @@ -173,6 +174,7 @@ private IEnumerable<MemberDocumentation> BuildConstructorDocumentation(Type type
Namespace = constructor.DeclaringType.Namespace,
Assembly = constructorAssembly.Name,
Parent = parent,
IsDocumented = true
};

if (summary != null)
Expand Down Expand Up @@ -214,6 +216,7 @@ private IEnumerable<MemberDocumentation> BuildPropertyDocumentation(Type type, D
Namespace = property.DeclaringType.Namespace,
Assembly = propertyAssembly.Name,
Parent = parent,
IsDocumented = true
};

if (summary != null)
Expand Down Expand Up @@ -253,10 +256,11 @@ private IEnumerable<MemberDocumentation> BuildMethodDocumentation(Type type, Doc
{
Id = methodId,
Name = method.GetSanitizedName(),
ContentType = DocumentationContentType.Property,
ContentType = DocumentationContentType.Method,
Namespace = method.DeclaringType.Namespace,
Assembly = methodAssembly.Name,
Parent = parent,
IsDocumented = true
};

if (summary != null)
Expand Down Expand Up @@ -337,13 +341,16 @@ private XmlDocumentation BuildXmlDocumentation(XPathNavigator navigator, Documen

private DocumentationObject BuildCRefDocumentation(string cref, DocumentationObject parent)
{
if (!cref.StartsWith("T:")) throw new ArgumentOutOfRangeException(nameof(cref), cref, "Unsupported cref.");
var parts = cref.Split(':');
if (parts.Length != 2) throw new ArgumentOutOfRangeException(nameof(cref), cref, "Invalid cref format.");

var typeName = cref[2..];
if (!typeName.Contains('.'))
var memberType = parts[0];
var memberName = parts[1];
var typeName = parts[1];
if (memberType != "T")
{
var @namespace = parent.TryGetParent<NamespaceDocumentation>(DocumentationContentType.Namespace);
if (@namespace != null) typeName = $"{@namespace.Id}.{typeName}";
memberName = memberName.Split('.').Last();
typeName = typeName[..typeName.LastIndexOf('.')];
}

var type = LookupType(typeName);
Expand All @@ -352,10 +359,112 @@ private DocumentationObject BuildCRefDocumentation(string cref, DocumentationObj
{
Id = "text",
Parent = parent,
Text = typeName
Text = memberName
};

return BuildTypeDocumentationReference(type, parent);
var typeDocumentationReference = BuildTypeDocumentationReference(type, parent);

switch (memberType)
{
case "T":
return typeDocumentationReference;
case "P":
{
var property = type.GetProperty(memberName, AllMembersBindingFlags);
if (property == null)
return new TextContent
{
Id = "text",
Parent = parent,
Text = memberName
};

return new MemberDocumentation
{
Id = property.GetXmlDocumentationId(),
Name = property.Name,
ContentType = DocumentationContentType.Property,
Namespace = type.Namespace,
Assembly = type.Assembly.GetName().Name,
Parent = typeDocumentationReference,
IsDocumented = PropertyFilter.Expression?.Invoke(property) ?? PropertyFilter.Default(property)
};
}
case "F":
{
var field = type.GetField(memberName, AllMembersBindingFlags);
if (field == null)
return new TextContent
{
Id = "text",
Parent = parent,
Text = memberName
};

return new MemberDocumentation
{
Id = field.GetXmlDocumentationId(),
Name = field.Name,
ContentType = DocumentationContentType.Field,
Namespace = type.Namespace,
Assembly = type.Assembly.GetName().Name,
Parent = typeDocumentationReference,
IsDocumented = FieldFilter.Expression?.Invoke(field) ?? FieldFilter.Default(field)
};
}
case "M":
{
if (memberName.StartsWith("#ctor"))
{
var constructors = type.GetConstructors(AllMembersBindingFlags);

var constructor = constructors.FirstOrDefault(c => c.GetXmlDocumentationId() == parts[1]);

if (constructor == null)
return new TextContent
{
Id = "text",
Parent = parent,
Text = memberName
};

return new MemberDocumentation
{
Id = constructor.GetXmlDocumentationId(),
Name = constructor.GetSanitizedName(),
ContentType = DocumentationContentType.Constructor,
Namespace = type.Namespace,
Assembly = type.Assembly.GetName().Name,
Parent = typeDocumentationReference,
IsDocumented = ConstructorFilter.Expression?.Invoke(constructor) ??
ConstructorFilter.Default(constructor)
};
}

var method = type.GetMethod(memberName, AllMembersBindingFlags);

if (method == null)
return new TextContent
{
Id = "text",
Parent = parent,
Text = memberName
};

return new MemberDocumentation
{
Id = method.GetXmlDocumentationId(),
Name = method.GetSanitizedName(),
ContentType = DocumentationContentType.Method,
Namespace = type.Namespace,
Assembly = type.Assembly.GetName().Name,
Parent = typeDocumentationReference,
IsDocumented = MethodFilter.Expression?.Invoke(method) ?? MethodFilter.Default(method)
};
}
default:
throw new ArgumentOutOfRangeException(nameof(cref), cref, "Unsupported cref member type.");
}
}

private TypeDocumentationReference BuildTypeDocumentationReference(Type type, DocumentationObject parent)
Expand Down
14 changes: 14 additions & 0 deletions tests/Doki.Tests.Common/Doki.Tests.Common.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions tests/Doki.Tests.Common/TestOutputLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Doki.Tests.Common;

public sealed class TestOutputLogger(ITestOutputHelper output) : ILogger
{
public bool HadError { get; private set; }

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (logLevel == LogLevel.Error) HadError = true;

output.WriteLine(formatter(state, exception));

if (exception != null) output.WriteLine(exception.ToString());
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
}
2 changes: 2 additions & 0 deletions tests/Doki.Tests.Snapshots/Doki.Tests.Snapshots.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<ProjectReference Include="..\..\src\Doki\Doki.csproj" />
<ProjectReference Include="..\assemblies\Doki.TestAssembly.ParentRootNamespace\Doki.TestAssembly.ParentRootNamespace.csproj" />
<ProjectReference Include="..\assemblies\Doki.TestAssembly.InheritanceChain\Doki.TestAssembly.InheritanceChain.csproj" />
<ProjectReference Include="..\assemblies\Doki.TestAssembly\Doki.TestAssembly.csproj" />
<ProjectReference Include="..\Doki.Tests.Common\Doki.Tests.Common.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 0216125

Please sign in to comment.