WIP: Auto-generate triple-slash comments without additional packages
Co-authored-by: omegaleo <5655201+omegaleo@users.noreply.github.com>
This commit is contained in:
@@ -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,7 +5,10 @@ using OmegaLeo.HelperLib.Shared.Attributes;
|
|||||||
|
|
||||||
namespace OmegaLeo.HelperLib.Helpers
|
namespace OmegaLeo.HelperLib.Helpers
|
||||||
{
|
{
|
||||||
[Documentation(nameof(BenchmarkUtility), "Utility class for benchmarking code execution time.", null, @"```csharp
|
/// <summary>
|
||||||
|
/// Utility class for benchmarking code execution time.
|
||||||
|
/// </summary>
|
||||||
|
[Documentation(nameof(BenchmarkUtility), "Utility class for benchmarking code execution time.", null, @"```csharp
|
||||||
BenchmarkUtility.Start(""MyBenchmark"");
|
BenchmarkUtility.Start(""MyBenchmark"");
|
||||||
// Code to benchmark
|
// Code to benchmark
|
||||||
BenchmarkUtility.Stop(""MyBenchmark"");
|
BenchmarkUtility.Stop(""MyBenchmark"");
|
||||||
@@ -20,7 +23,12 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark"");
|
|||||||
private static Dictionary<string, List<long>> _benchmarks = new Dictionary<string, List<long>>();
|
private static Dictionary<string, List<long>> _benchmarks = new Dictionary<string, List<long>>();
|
||||||
private static Dictionary<string, Stopwatch> _stopwatches = new Dictionary<string, Stopwatch>();
|
private static Dictionary<string, Stopwatch> _stopwatches = new Dictionary<string, Stopwatch>();
|
||||||
|
|
||||||
[Documentation("GetStopwatch", "Retrieves or creates a Stopwatch instance for the given key.", null, null)]
|
/// <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)
|
private static Stopwatch GetStopwatch(string key)
|
||||||
{
|
{
|
||||||
Stopwatch stopwatch;
|
Stopwatch stopwatch;
|
||||||
@@ -34,7 +42,12 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark"");
|
|||||||
return stopwatch;
|
return stopwatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Documentation("Record", "Records the execution time of the provided action and returns the elapsed time in milliseconds.", null, @"```csharp
|
/// <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(() =>
|
var time = BenchmarkUtility.Record(() =>
|
||||||
{
|
{
|
||||||
// Code to benchmark
|
// Code to benchmark
|
||||||
@@ -53,7 +66,13 @@ Console.WriteLine($""Elapsed time: {time} ms"");
|
|||||||
return stopwatch.ElapsedMilliseconds;
|
return stopwatch.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
[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
|
/// <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"", () =>
|
var time = BenchmarkUtility.RecordAndSaveToResults(""MyBenchmark"", () =>
|
||||||
{
|
{
|
||||||
// Code to benchmark
|
// Code to benchmark
|
||||||
@@ -85,7 +104,11 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
|||||||
return stopwatch.ElapsedMilliseconds;
|
return stopwatch.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Documentation("Start", "Starts or restarts the stopwatch for the given key.", null, null)]
|
/// <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>
|
/// <summary>
|
||||||
/// Starts or restarts the stopwatch for the given key.
|
/// Starts or restarts the stopwatch for the given key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -95,7 +118,11 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
|||||||
GetStopwatch(key).Restart();
|
GetStopwatch(key).Restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
[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.
|
||||||
|
/// </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>
|
/// <summary>
|
||||||
/// Stops the stopwatch for the given key and records the elapsed time.
|
/// Stops the stopwatch for the given key and records the elapsed time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -111,7 +138,12 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
|||||||
_benchmarks[key].Add(GetStopwatch(key).ElapsedMilliseconds);
|
_benchmarks[key].Add(GetStopwatch(key).ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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.
|
||||||
|
/// </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>
|
/// <summary>
|
||||||
/// Retrieves the list of recorded times for the given key.
|
/// Retrieves the list of recorded times for the given key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -122,13 +154,20 @@ Console.WriteLine($""Average time: {averageTime} ms"");
|
|||||||
return _benchmarks.ContainsKey(key) ? _benchmarks[key] : new List<long>();
|
return _benchmarks.ContainsKey(key) ? _benchmarks[key] : new List<long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Documentation("ClearResults", "Clears all recorded benchmark results.", null, null)]
|
/// <summary>
|
||||||
|
/// Clears all recorded benchmark results.
|
||||||
|
/// </summary>
|
||||||
|
[Documentation("ClearResults", "Clears all recorded benchmark results.", null, null)]
|
||||||
public static void ClearResults()
|
public static void ClearResults()
|
||||||
{
|
{
|
||||||
_benchmarks.Clear();
|
_benchmarks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Documentation("GetAllResults", "Retrieves all recorded benchmark results.", null, null)]
|
/// <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()
|
public static Dictionary<string, List<long>> GetAllResults()
|
||||||
{
|
{
|
||||||
return new Dictionary<string, List<long>>(_benchmarks);
|
return new Dictionary<string, List<long>>(_benchmarks);
|
||||||
|
|||||||
Reference in New Issue
Block a user