Skip to content

Commit

Permalink
Docs: ApiDocsGenerator for API documentation from XML comments (#5846)
Browse files Browse the repository at this point in the history
* ApiDocsGenerator

* Adding simple logger that emits logs into cs file.

* Handles default values.

* adds README.md

* Enum values are part of the comment.

* Handling multiple components in one docs page

* small edits

* Parametrize the Name -> column in table for API

* Methods for APIdocs done!

* Styling adjustments

* Fix docs api structure

* Use shared generator settings

* Move apidocs dtos to own project.

* Adds typeName for components (mainly bcs of generics).

* Generator also includes generic types (e.g. BaseTextInput<TValue>)

* Properties and method from inheritance chain. Making it work with extensions.

* inheritdocs, "should only be used internally"

* Add ApiDocsDtos to solution

* Don't show empty parameters

* checked some of the docs pages

* prepare to be checked

* xml comment improvements

* Update apidocsGenerator with Remarks and `<c>` support

* Formating

* Improve BaseComponent comments

* markup string on remarks

* Better Should only be used internally message

* Improve Field comments

* Adds Events to the apidocs.

* Replaces base component type name inside description or remarks with component type name

* Rename "description" to "summary".

* removes dead code

* typo fix

* support for <seealso (links in xml comments), fixes generic param in <see. New XmlCommentToHtmlConverter.cs (to keep the parsing stuff in one place).

* (D-V) new api docs on several components

* BreadcrumbItem comment

* format

* see instead of seealso

* Render content of see tag.

* cleanups

* removes redundant type (on enums and enum like types)

* Blazorise constants with numeric values. Fix for enums from different namespaces (e.g. Snackbar)

* Dont include DocsApiSourceGenerator to nuget

* include fody. Blazorise dll cleanup works

* ApiDocsGenerator and Dtos to existing Generators and Features projects;
Fody project structure works as it should;
Extensions.Snackbar with option to not use SGs on pack;

* namespaces refactoring, small improvements

* fixing for linux

* Format csproj files

* format md files

* Readme update

* remove IsPack, includes EnableApiDocsGenerator.

* Converts [Generator] into analyzer that generates source files; Removes reference to generator from Blazorise.csproj;Works with generic better (BaseChar<> vs BaseChart<,,,>); Removes default value in "non-string" form (wasn't needed and caused troubles); Removes references to Generator.Features from Blazorise.Docs, moves the Dtos to Blazorise.Docs; Removes weaving of of ApiDocs from BlazoriseLib;

* format csproj

* clean, format, and reorganize

* Formating

* load only current assembly

* Fix for generic in dictionary.

* fix for component inheritance (up to IComponent)

* fix from constant from complex type

* Animate fix + docs

* removes Logger.cs

* Cleanups in ComponentApiDocs.razor. - review fixes.

* Fix for retrieving xml comments from Microsoft.AspnetCore.Components namespace.

* fix for !: inside the cref type

* System.Runtime attempt; Default value to remove global::

* Extensions with new ApiDocs in place (also still with the old api for comparation)

* fixing local index

* Add missing comments

* change local var name

* More fixed comments

* Skip internal components

* Don't show enum values of enum has more than 30 values

* Remove DataGrid SG API for now

* Remove old apis

* Remove old snackbar apis

* Quick fix to not show Dispose method

* Fix comments

* support for generic type "rename".

* rm assembly name inside defualt value string (Vide.SettingsList)

* Unified place for type qualified for apidocs; support for other types then components (e.g. options)

* Format

* Cropper

---------

Co-authored-by: Mladen Macanovic <[email protected]>
  • Loading branch information
tesar-tech and stsrki authored Dec 10, 2024
1 parent 0aeb89e commit 4bba734
Show file tree
Hide file tree
Showing 177 changed files with 2,305 additions and 4,686 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,9 @@ Documentation/Blazorise.Docs.Server/wwwroot/img/blog/.DS_Store

# Source Generation directory
__SOURCEGENERATED__/

# Fody
/Source/Blazorise/FodyWeavers.xsd

# "source generator" for api docs
Documentation/Blazorise.Docs/ApiDocs
14 changes: 14 additions & 0 deletions Blazorise.sln
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazorise.Charts.Zoom", "So
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazorise.PdfViewer", "Source\Extensions\Blazorise.PdfViewer\Blazorise.PdfViewer.csproj", "{EAB7EC89-900A-4280-B24A-152B9DD2B503}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Weavers", "Source\SourceGenerators\Blazorise.Weavers\Blazorise.Weavers.csproj", "{BF5FFB8C-45AD-4875-BB01-2DA388890419}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorise.Weavers.Fody", "Source\SourceGenerators\Blazorise.Weavers.Fody\Blazorise.Weavers.Fody.csproj", "{FFC4A285-1A16-4DD4-8B8C-141521E405B0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -411,6 +415,14 @@ Global
{EAB7EC89-900A-4280-B24A-152B9DD2B503}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAB7EC89-900A-4280-B24A-152B9DD2B503}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAB7EC89-900A-4280-B24A-152B9DD2B503}.Release|Any CPU.Build.0 = Release|Any CPU
{BF5FFB8C-45AD-4875-BB01-2DA388890419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF5FFB8C-45AD-4875-BB01-2DA388890419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF5FFB8C-45AD-4875-BB01-2DA388890419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF5FFB8C-45AD-4875-BB01-2DA388890419}.Release|Any CPU.Build.0 = Release|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFC4A285-1A16-4DD4-8B8C-141521E405B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -481,6 +493,8 @@ Global
{2B4FD79A-42E2-4B81-828B-0799E4744ADA} = {9731051E-0AA7-411E-A76A-987854F034DA}
{045536EC-BD97-409D-BDF7-C148B7C5AAFC} = {9731051E-0AA7-411E-A76A-987854F034DA}
{EAB7EC89-900A-4280-B24A-152B9DD2B503} = {9731051E-0AA7-411E-A76A-987854F034DA}
{BF5FFB8C-45AD-4875-BB01-2DA388890419} = {0538DB67-B4F3-4D00-B969-D3874A52E405}
{FFC4A285-1A16-4DD4-8B8C-141521E405B0} = {0538DB67-B4F3-4D00-B969-D3874A52E405}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {205B3EA4-470F-45DA-911E-346AF7D0A9A5}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
#region Using directives
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using Blazorise.Docs.Compiler.ApiDocsGenerator.Dtos;
using Blazorise.Docs.Compiler.ApiDocsGenerator.Extensions;
using Blazorise.Docs.Compiler.ApiDocsGenerator.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
#endregion

namespace Blazorise.Docs.Compiler.ApiDocsGenerator;

public class ComponentsApiDocsGenerator
{
#region Members

private Assembly aspNetCoreComponentsAssembly;

private CSharpCompilation blazoriseCompilation;

private XmlDocumentationProvider aspnetCoreDocumentationProvider;


private Assembly systemRuntimeAssembly;
private XmlDocumentationProvider systemRuntimeDocumentationProvider;


const string ShouldOnlyBeUsedInternally = "This method is intended for internal framework use only and should not be called directly by user code";

#endregion

#region Constructors

public ComponentsApiDocsGenerator()
{
var aspnetCoreAssemblyName = typeof( Microsoft.AspNetCore.Components.ParameterAttribute ).Assembly.GetName().Name;
aspNetCoreComponentsAssembly = AppDomain.CurrentDomain
.GetAssemblies()
.FirstOrDefault( a => a.GetName().Name == aspnetCoreAssemblyName );

systemRuntimeAssembly = AppDomain.CurrentDomain
.GetAssemblies()
.FirstOrDefault( a => a.GetName().Name == "System.Runtime" );

if ( systemRuntimeAssembly is not null )
{
systemRuntimeDocumentationProvider = XmlDocumentationProvider.CreateFromFile( $"{Path.GetFullPath( "." )}/System.Runtime.xml" );
}
if ( aspNetCoreComponentsAssembly != null )
{
// Replace the .dll extension with .xml to get the documentation file path
string xmlDocumentationPath = Path.ChangeExtension( aspNetCoreComponentsAssembly.Location, ".xml" );
aspnetCoreDocumentationProvider = XmlDocumentationProvider.CreateFromFile( xmlDocumentationPath );
}
//get the blazorise compilation, it's needed for every extension.
blazoriseCompilation = GetCompilation( Paths.BlazoriseLibRoot, "Blazorise", true );
}

#endregion

#region Methods

public bool Execute()
{
if ( aspNetCoreComponentsAssembly is null )
{
Console.WriteLine( $"Error generating ApiDocs. Cannot find ASP.NET Core assembly." );
return false;
}
if ( blazoriseCompilation is null )
{
Console.WriteLine( $"Error generating ApiDocs. Cannot find Blazorise assembly." );
return false;
}
if ( !Directory.Exists( Paths.BlazoriseExtensionsRoot ) )
{
Console.WriteLine( $"Directory for extensions does not exist: {Paths.BlazoriseExtensionsRoot}" );
return false;
}

//directories where to load the source code from one by one
string[] inputLocations = [Paths.BlazoriseLibRoot, .. Directory.GetDirectories( Paths.BlazoriseExtensionsRoot )];

foreach ( var inputLocation in inputLocations )
{
string assemblyName = Path.GetFileName( inputLocation ); // Use directory name as assembly name

CSharpCompilation compilation = inputLocation.EndsWith( "Blazorise" )
? blazoriseCompilation // the case for getting components from Blazorise
: GetCompilation( inputLocation, assemblyName );

INamespaceSymbol namespaceToSearch = FindNamespace( compilation, assemblyName ); // e.g. Blazorise.Animate

IEnumerable<ComponentInfo> componentInfo = GetComponentsInfo( compilation, namespaceToSearch );
string sourceText = GenerateComponentsApiSource( compilation, [.. componentInfo], assemblyName );

if ( !Directory.Exists( Paths.ApiDocsPath ) ) // BlazoriseDocs.ApiDocs
Directory.CreateDirectory( Paths.ApiDocsPath );

string outputPath = Path.Join( Paths.ApiDocsPath, $"{assemblyName}.ApiDocs.cs" );

File.WriteAllText( outputPath, sourceText );
Console.WriteLine( $"API Docs generated for {assemblyName} at {outputPath}. {sourceText.Length} characters." );
}

return true;
}

//namespace are divided in chunks (Blazorise.Animate is under Blazorise...)
INamespaceSymbol FindNamespace( Compilation compilation, string namespaceName, INamespaceSymbol? namespaceToSearch = null )

Check warning on line 114 in Documentation/Blazorise.Docs.Compiler/ApiDocsGenerator/ComponentsApiDocsGenerator.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 114 in Documentation/Blazorise.Docs.Compiler/ApiDocsGenerator/ComponentsApiDocsGenerator.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
namespaceToSearch ??= compilation.GlobalNamespace
.GetNamespaceMembers()
.FirstOrDefault( ns => ns.Name == "Blazorise" );

if ( namespaceToSearch is null )
throw new Exception( $"Unable to find namespace {namespaceName}." );

if ( namespaceToSearch.ToDisplayString() == namespaceName )
return namespaceToSearch;

foreach ( var childNamespace in namespaceToSearch.GetNamespaceMembers() )
{
var result = FindNamespace( compilation, namespaceName, childNamespace );
if ( result != null )
return result;
}

return null;
}
private CSharpCompilation GetCompilation( string inputLocation, string assemblyName, bool isBlazoriseAssembly = false )
{
var sourceFiles = Directory.GetFiles( inputLocation, "*.cs", SearchOption.AllDirectories );

List<MetadataReference> references =
[
MetadataReference.CreateFromFile( systemRuntimeAssembly.Location, documentation:systemRuntimeDocumentationProvider ), // Microsoft.AspNetCore.Components
MetadataReference.CreateFromFile( aspNetCoreComponentsAssembly.Location, documentation:aspnetCoreDocumentationProvider ), // Microsoft.AspNetCore.Components
];
if ( !isBlazoriseAssembly ) //get Blazorise assembly as reference (for extensions)
references.Add( blazoriseCompilation.ToMetadataReference() );

var syntaxTrees = sourceFiles.Select( file => CSharpSyntaxTree.ParseText( File.ReadAllText( file ) ) );

var compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees,
references.ToImmutableArray(),
new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
);
return compilation;
}

private IEnumerable<ComponentInfo> GetComponentsInfo( Compilation compilation, INamespaceSymbol namespaceToSearch )
{
var baseComponentSymbol = compilation.GetTypeByMetadataName( "Blazorise.BaseComponent" );

foreach ( var type in namespaceToSearch.GetTypeMembers().OfType<INamedTypeSymbol>() )
{
var (qualifiesForApiDocs, inheritsFromChain, skipParamCheck) = QualifiesForApiDocs( type, baseComponentSymbol );
if ( !qualifiesForApiDocs )
continue;

// Retrieve properties
var parameterProperties = type.GetMembers()
.OfType<IPropertySymbol>()
.Where( p =>
p.DeclaredAccessibility == Accessibility.Public &&
( skipParamCheck || p.GetAttributes().Any( attr =>
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute" ) ) &&
p.OverriddenProperty == null );

// Retrieve methods
var publicMethods = type.GetMembers()
.OfType<IMethodSymbol>()
.Where( m => m.DeclaredAccessibility == Accessibility.Public &&
!m.IsImplicitlyDeclared &&
m.MethodKind == MethodKind.Ordinary &&
m.OverriddenMethod == null );

yield return new ComponentInfo
(
Type: type,
PublicMethods: publicMethods,
Properties: parameterProperties,
InheritsFromChain: inheritsFromChain
);
}
}

/// <summary>
/// get the chain of inheritance to the BaseComponent or ComponentBase
/// Only return true if implements IComponent (that is the case for all BaseComponent and ComponentBase)
/// </summary>
/// <param name="type"></param>
/// <param name="baseType"></param>
/// <returns></returns>
private (bool qualifiesForApiDocs, IEnumerable<INamedTypeSymbol>, bool skipParamCheck) QualifiesForApiDocs( INamedTypeSymbol type,
INamedTypeSymbol baseType )
{

(bool continueProcessing, bool skipParamAndComponentCheck) = type switch
{
_ when type.TypeKind != TypeKind.Class || type.DeclaredAccessibility != Accessibility.Public => (false, false),
_ when type.Name.StartsWith( '_' ) => (false, false),
_ when type.Name.EndsWith( "Options" ) => (true, true),
_ when type.Name.EndsWith( "RouterTabsPageAttribute" ) => (true, true),
_ when !type.AllInterfaces.Any( i => i.Name == "IComponent" ) => (false, false),
_ => (true, false)
};

if ( !continueProcessing )
return (false, [], false);

List<INamedTypeSymbol> inheritsFromChain = [];
while ( type != null )
{
type = type.BaseType;
if ( type?.Name.Split( "." ).Last() == "ComponentBase" //for this to work, the inheritance (:ComponentBase) must be specified in .cs file.
|| SymbolEqualityComparer.Default.Equals( type, baseType )
)
return (true, inheritsFromChain, skipParamAndComponentCheck);
inheritsFromChain.Add( type );
}
return (true, [], skipParamAndComponentCheck);
}

private static string GenerateComponentsApiSource( Compilation compilation, ImmutableArray<ComponentInfo> components, string assemblyName )
{
IEnumerable<ApiDocsForComponent> componentsData = components.Select( component =>
{
string componentType = component.Type.ToStringWithGenerics();
string componentTypeName = StringHelpers.GetSimplifiedTypeName( component.Type );

var propertiesData = component.Properties.Select( property =>
InfoExtractor.GetPropertyDetails( compilation, property ) )
.Where( x => !x.Summary.Contains( ShouldOnlyBeUsedInternally ) );

var methodsData = component.PublicMethods.Select( InfoExtractor.GetMethodDetails )
.Where( x => !x.Summary.Contains( ShouldOnlyBeUsedInternally ) );

ApiDocsForComponent comp = new( type: componentType, typeName: componentTypeName,
properties: propertiesData, methods: methodsData,
inheritsFromChain: component.InheritsFromChain.Select( type => type.ToStringWithGenerics() ) );

return comp;
} );

return
$$"""
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Windows.Input;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;
using Blazorise.Docs.Models.ApiDocsDtos;
using Blazorise.Charts;
namespace Blazorise.Docs.ApiDocs;
public class ComponentApiSource_ForNamespace_{{assemblyName.Replace( ".", "_" )}}:IComponentsApiDocsSource
{
public Dictionary<Type, ApiDocsForComponent> Components { get; } =
new Dictionary<Type, ApiDocsForComponent>
{
{{componentsData.Where( comp => comp is not null ).Select( comp =>
{
return $$"""
{ typeof({{comp.Type}}),new ApiDocsForComponent(typeof({{comp.Type}}),
"{{comp.TypeName}}",
new List<ApiDocsForComponentProperty>{
{{comp.Properties.Select( prop =>
$"""
new ("{prop.Name}",typeof({prop.Type}), "{prop.TypeName}",{prop.DefaultValueString}, "{prop.Summary}","{prop.Remarks}", {( prop.IsBlazoriseEnum ? "true" : "false" )}),
""" ).StringJoin( " " )}}},
new List<ApiDocsForComponentMethod>{
{{comp.Methods.Select( method =>
$$"""
new ("{{method.Name}}","{{method.ReturnTypeName}}", "{{method.Summary}}" ,"{{method.Remarks}}",
new List<ApiDocsForComponentMethodParameter>{
{{method.Parameters.Select( param =>
$"""
new ("{param.Name}","{param.TypeName}" ),
"""
).StringJoin( " " )}} }),
""" ).StringJoin( " " )}}
},
new List<Type>{
{{comp.InheritsFromChain.Select( x => $"typeof({x})" ).StringJoin( "," )}}
}
)},
""";
}
).StringJoin( "\n" )}}
};
}
""";
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#region Using directives
using System.Collections.Generic;
#endregion

namespace Blazorise.Docs.Compiler.ApiDocsGenerator.Dtos;

/// <summary>
/// Easier to gather necessary info.
/// Almost keeps parity with Blazorise/Models/ApiDocsDtos.cs, changes here should be reflected there
/// </summary>
public class ApiDocsForComponent
{
public ApiDocsForComponent( string type, string typeName,
IEnumerable<ApiDocsForComponentProperty> properties,
IEnumerable<ApiDocsForComponentMethod> methods,
IEnumerable<string> inheritsFromChain )
{
Type = type;
TypeName = typeName;
Properties = properties;
Methods = methods;
InheritsFromChain = inheritsFromChain;
}

public string Type { get; }

public string TypeName { get; }
public IEnumerable<ApiDocsForComponentProperty> Properties { get; }
public IEnumerable<ApiDocsForComponentMethod> Methods { get; }

public IEnumerable<string> InheritsFromChain { get; }
}
Loading

0 comments on commit 4bba734

Please sign in to comment.