WIP: Generate XML documentation from DocumentationAttribute for IDE IntelliSense #12
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OmegaLeo.HelperLib.Shared\OmegaLeo.HelperLib.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace OmegaLeo.HelperLib.CommentGenerator;
|
||||
|
||||
class Program
|
||||
{
|
||||
static int Main(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Usage: dotnet run <directory-path>");
|
||||
Console.WriteLine("Generates triple-slash comments from DocumentationAttribute in C# files.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var directory = args[0];
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Directory '{directory}' does not exist.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var csFiles = Directory.GetFiles(directory, "*.cs", SearchOption.AllDirectories)
|
||||
.Where(f => !f.Contains("/obj/") && !f.Contains("\\obj\\") &&
|
||||
!f.Contains("/bin/") && !f.Contains("\\bin\\"))
|
||||
.ToList();
|
||||
|
||||
Console.WriteLine($"[CommentGenerator] Processing {csFiles.Count} C# files in {directory}...");
|
||||
|
||||
int modifiedCount = 0;
|
||||
foreach (var filePath in csFiles)
|
||||
{
|
||||
if (ProcessFile(filePath))
|
||||
{
|
||||
modifiedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[CommentGenerator] ✅ Complete! Modified {modifiedCount} files.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool ProcessFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var code = File.ReadAllText(filePath);
|
||||
var tree = CSharpSyntaxTree.ParseText(code);
|
||||
var root = tree.GetRoot();
|
||||
|
||||
var rewriter = new DocumentationCommentRewriter();
|
||||
var newRoot = rewriter.Visit(root);
|
||||
|
||||
if (rewriter.Modified)
|
||||
{
|
||||
File.WriteAllText(filePath, newRoot.ToFullString());
|
||||
Console.WriteLine($" ✓ {Path.GetFileName(filePath)}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($" ✗ Error processing {Path.GetFileName(filePath)}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentationCommentRewriter : CSharpSyntaxRewriter
|
||||
{
|
||||
public bool Modified { get; private set; }
|
||||
|
||||
public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node)
|
||||
{
|
||||
node = (ClassDeclarationSyntax)base.VisitClassDeclaration(node)!;
|
||||
return AddDocumentationComment(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node)
|
||||
{
|
||||
node = (StructDeclarationSyntax)base.VisitStructDeclaration(node)!;
|
||||
return AddDocumentationComment(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
|
||||
{
|
||||
node = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!;
|
||||
return AddDocumentationComment(node);
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node)
|
||||
{
|
||||
node = (PropertyDeclarationSyntax)base.VisitPropertyDeclaration(node)!;
|
||||
return AddDocumentationComment(node);
|
||||
}
|
||||
|
||||
private T AddDocumentationComment<T>(T node) where T : MemberDeclarationSyntax
|
||||
{
|
||||
// Check if already has /// comments
|
||||
if (HasXmlDocumentation(node))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
// Find DocumentationAttribute
|
||||
var docAttr = node.AttributeLists
|
||||
.SelectMany(al => al.Attributes)
|
||||
.FirstOrDefault(a => a.Name.ToString().Contains("Documentation"));
|
||||
|
||||
if (docAttr == null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
// Extract attribute arguments
|
||||
var args = docAttr.ArgumentList?.Arguments;
|
||||
if (args == null || args.Value.Count < 2)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
// Get description (2nd argument - index 1)
|
||||
var description = GetStringLiteral(args.Value[1].Expression);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
// Get args array (3rd argument - index 2) if present
|
||||
string[]? paramDescriptions = null;
|
||||
if (args.Value.Count >= 3)
|
||||
{
|
||||
paramDescriptions = GetStringArray(args.Value[2].Expression);
|
||||
}
|
||||
|
||||
// Generate XML documentation comment text
|
||||
var commentText = GenerateXmlCommentText(node, description, paramDescriptions);
|
||||
|
||||
// Add the comment as leading trivia
|
||||
var existingLeadingTrivia = node.GetLeadingTrivia();
|
||||
var newTrivia = SyntaxFactory.ParseLeadingTrivia(commentText);
|
||||
var combinedTrivia = existingLeadingTrivia.AddRange(newTrivia);
|
||||
|
||||
Modified = true;
|
||||
return node.WithLeadingTrivia(combinedTrivia);
|
||||
}
|
||||
|
||||
private bool HasXmlDocumentation(SyntaxNode node)
|
||||
{
|
||||
return node.GetLeadingTrivia()
|
||||
.Any(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
|
||||
t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia));
|
||||
}
|
||||
|
||||
private string? GetStringLiteral(ExpressionSyntax expr)
|
||||
{
|
||||
if (expr is LiteralExpressionSyntax literal &&
|
||||
literal.IsKind(SyntaxKind.StringLiteralExpression))
|
||||
{
|
||||
return literal.Token.ValueText;
|
||||
}
|
||||
|
||||
// Handle verbatim strings @"..."
|
||||
var text = expr.ToString();
|
||||
if (text.StartsWith("@\"") && text.EndsWith("\""))
|
||||
{
|
||||
return text.Substring(2, text.Length - 3);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string[]? GetStringArray(ExpressionSyntax expr)
|
||||
{
|
||||
if (expr.ToString() == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expr is ImplicitArrayCreationExpressionSyntax implicitArray)
|
||||
{
|
||||
return implicitArray.Initializer.Expressions
|
||||
.Select(e => GetStringLiteral(e))
|
||||
.Where(s => s != null)
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
if (expr is ArrayCreationExpressionSyntax arrayExpr && arrayExpr.Initializer != null)
|
||||
{
|
||||
return arrayExpr.Initializer.Expressions
|
||||
.Select(e => GetStringLiteral(e))
|
||||
.Where(s => s != null)
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GenerateXmlCommentText(
|
||||
MemberDeclarationSyntax node,
|
||||
string description,
|
||||
string[]? paramDescriptions)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var indent = GetIndentation(node);
|
||||
|
||||
// Add <summary>
|
||||
sb.AppendLine($"{indent}/// <summary>");
|
||||
sb.AppendLine($"{indent}/// {description}");
|
||||
sb.AppendLine($"{indent}/// </summary>");
|
||||
|
||||
// Add <typeparam> for generic types
|
||||
if (node is TypeDeclarationSyntax typeDecl && typeDecl.TypeParameterList != null)
|
||||
{
|
||||
foreach (var typeParam in typeDecl.TypeParameterList.Parameters)
|
||||
{
|
||||
sb.AppendLine($"{indent}/// <typeparam name=\"{typeParam.Identifier.Text}\">The {typeParam.Identifier.Text} type parameter</typeparam>");
|
||||
}
|
||||
}
|
||||
|
||||
// Add <param> tags for methods
|
||||
if (node is MethodDeclarationSyntax method)
|
||||
{
|
||||
var parameters = method.ParameterList.Parameters;
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
var paramDesc = GetParameterDescription(param.Identifier.Text, paramDescriptions);
|
||||
sb.AppendLine($"{indent}/// <param name=\"{param.Identifier.Text}\">{paramDesc}</param>");
|
||||
}
|
||||
|
||||
// Add <returns> if not void
|
||||
if (!method.ReturnType.ToString().Contains("void"))
|
||||
{
|
||||
sb.AppendLine($"{indent}/// <returns>The return value</returns>");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetIndentation(SyntaxNode node)
|
||||
{
|
||||
var trivia = node.GetLeadingTrivia();
|
||||
var whitespace = trivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia));
|
||||
return whitespace.ToString();
|
||||
}
|
||||
|
||||
private string GetParameterDescription(string paramName, string[]? paramDescriptions)
|
||||
{
|
||||
if (paramDescriptions == null)
|
||||
{
|
||||
return $"The {paramName} parameter";
|
||||
}
|
||||
|
||||
foreach (var desc in paramDescriptions)
|
||||
{
|
||||
if (desc.StartsWith($"{paramName}:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return desc.Substring(paramName.Length + 1).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return $"The {paramName} parameter";
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using OmegaLeo.HelperLib.Shared.Attributes;
|
||||
|
||||
namespace OmegaLeo.HelperLib.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for benchmarking code execution time.
|
||||
/// </summary>
|
||||
[Documentation(nameof(BenchmarkUtility), "Utility class for benchmarking code execution time.", null, @"```csharp
|
||||
BenchmarkUtility.Start(""MyBenchmark"");
|
||||
// Code to benchmark
|
||||
@@ -20,6 +23,11 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark"");
|
||||
private static Dictionary<string, List<long>> _benchmarks = new Dictionary<string, List<long>>();
|
||||
private static Dictionary<string, Stopwatch> _stopwatches = new Dictionary<string, Stopwatch>();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves or creates a Stopwatch instance for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key parameter</param>
|
||||
/// <returns>The return value</returns>
|
||||
[Documentation("GetStopwatch", "Retrieves or creates a Stopwatch instance for the given key.", null, null)]
|
||||
private static Stopwatch GetStopwatch(string key)
|
||||
{
|
||||
@@ -34,6 +42,11 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark"");
|
||||
return stopwatch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the execution time of the provided action and returns the elapsed time in milliseconds.
|
||||
/// </summary>
|
||||
/// <param name="actionToRecord">The actionToRecord parameter</param>
|
||||
/// <returns>The return value</returns>
|
||||
[Documentation("Record", "Records the execution time of the provided action and returns the elapsed time in milliseconds.", null, @"```csharp
|
||||
var time = BenchmarkUtility.Record(() =>
|
||||
{
|
||||
@@ -53,6 +66,12 @@ Console.WriteLine($""Elapsed time: {time} ms"");
|
||||
return stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the execution time of the provided action, saves it under the given key, and returns the elapsed time in milliseconds.
|
||||
/// </summary>
|
||||
/// <param name="key">The key parameter</param>
|
||||
/// <param name="actionToRecord">The actionToRecord parameter</param>
|
||||
/// <returns>The return value</returns>
|
||||
[Documentation("RecordAndSaveToResults", "Records the execution time of the provided action, saves it under the given key, and returns the elapsed time in milliseconds.", null, @"```csharp
|
||||
var time = BenchmarkUtility.RecordAndSaveToResults(""MyBenchmark"", () =>
|
||||
{
|
||||
@@ -85,6 +104,10 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
return stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts or restarts the stopwatch for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key parameter</param>
|
||||
[Documentation("Start", "Starts or restarts the stopwatch for the given key.", null, null)]
|
||||
/// <summary>
|
||||
/// Starts or restarts the stopwatch for the given key.
|
||||
@@ -95,6 +118,10 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
GetStopwatch(key).Restart();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the stopwatch for the given key and records the elapsed time.
|
||||
/// </summary>
|
||||
/// <param name="key">The key parameter</param>
|
||||
[Documentation("Stop", "Stops the stopwatch for the given key and records the elapsed time.", null, null)]
|
||||
/// <summary>
|
||||
/// Stops the stopwatch for the given key and records the elapsed time.
|
||||
@@ -111,6 +138,11 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
_benchmarks[key].Add(GetStopwatch(key).ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the list of recorded times for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key parameter</param>
|
||||
/// <returns>The return value</returns>
|
||||
[Documentation("GetResults", "Retrieves the list of recorded times for the given key.", null, null)]
|
||||
/// <summary>
|
||||
/// Retrieves the list of recorded times for the given key.
|
||||
@@ -122,12 +154,19 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
||||
return _benchmarks.ContainsKey(key) ? _benchmarks[key] : new List<long>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all recorded benchmark results.
|
||||
/// </summary>
|
||||
[Documentation("ClearResults", "Clears all recorded benchmark results.", null, null)]
|
||||
public static void ClearResults()
|
||||
{
|
||||
_benchmarks.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all recorded benchmark results.
|
||||
/// </summary>
|
||||
/// <returns>The return value</returns>
|
||||
[Documentation("GetAllResults", "Retrieves all recorded benchmark results.", null, null)]
|
||||
public static Dictionary<string, List<long>> GetAllResults()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user