From 2957b198893c9a5c3b1388cdbcc782c5621f5900 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:36:40 +0000 Subject: [PATCH] WIP: Auto-generate triple-slash comments without additional packages Co-authored-by: omegaleo <5655201+omegaleo@users.noreply.github.com> --- ...OmegaLeo.HelperLib.CommentGenerator.csproj | 20 ++ .../Program.cs | 275 ++++++++++++++++++ .../Helpers/BenchmarkUtility.cs | 57 +++- 3 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 OmegaLeo.HelperLib.CommentGenerator/OmegaLeo.HelperLib.CommentGenerator.csproj create mode 100644 OmegaLeo.HelperLib.CommentGenerator/Program.cs diff --git a/OmegaLeo.HelperLib.CommentGenerator/OmegaLeo.HelperLib.CommentGenerator.csproj b/OmegaLeo.HelperLib.CommentGenerator/OmegaLeo.HelperLib.CommentGenerator.csproj new file mode 100644 index 0000000..f1f76fe --- /dev/null +++ b/OmegaLeo.HelperLib.CommentGenerator/OmegaLeo.HelperLib.CommentGenerator.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + Exe + net8.0 + enable + enable + latest + + + diff --git a/OmegaLeo.HelperLib.CommentGenerator/Program.cs b/OmegaLeo.HelperLib.CommentGenerator/Program.cs new file mode 100644 index 0000000..a1fbca9 --- /dev/null +++ b/OmegaLeo.HelperLib.CommentGenerator/Program.cs @@ -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 "); + 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 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 + sb.AppendLine($"{indent}/// "); + sb.AppendLine($"{indent}/// {description}"); + sb.AppendLine($"{indent}/// "); + + // Add for generic types + if (node is TypeDeclarationSyntax typeDecl && typeDecl.TypeParameterList != null) + { + foreach (var typeParam in typeDecl.TypeParameterList.Parameters) + { + sb.AppendLine($"{indent}/// The {typeParam.Identifier.Text} type parameter"); + } + } + + // Add 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}/// {paramDesc}"); + } + + // Add if not void + if (!method.ReturnType.ToString().Contains("void")) + { + sb.AppendLine($"{indent}/// The return value"); + } + } + + 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"; + } +} diff --git a/OmegaLeo.HelperLib/Helpers/BenchmarkUtility.cs b/OmegaLeo.HelperLib/Helpers/BenchmarkUtility.cs index 460c347..6635a40 100755 --- a/OmegaLeo.HelperLib/Helpers/BenchmarkUtility.cs +++ b/OmegaLeo.HelperLib/Helpers/BenchmarkUtility.cs @@ -5,7 +5,10 @@ using OmegaLeo.HelperLib.Shared.Attributes; namespace OmegaLeo.HelperLib.Helpers { - [Documentation(nameof(BenchmarkUtility), "Utility class for benchmarking code execution time.", null, @"```csharp + /// + /// Utility class for benchmarking code execution time. + /// +[Documentation(nameof(BenchmarkUtility), "Utility class for benchmarking code execution time.", null, @"```csharp BenchmarkUtility.Start(""MyBenchmark""); // Code to benchmark BenchmarkUtility.Stop(""MyBenchmark""); @@ -20,7 +23,12 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark""); private static Dictionary> _benchmarks = new Dictionary>(); private static Dictionary _stopwatches = new Dictionary(); - [Documentation("GetStopwatch", "Retrieves or creates a Stopwatch instance for the given key.", null, null)] + /// + /// Retrieves or creates a Stopwatch instance for the given key. + /// + /// The key parameter + /// The return value +[Documentation("GetStopwatch", "Retrieves or creates a Stopwatch instance for the given key.", null, null)] private static Stopwatch GetStopwatch(string key) { Stopwatch stopwatch; @@ -34,7 +42,12 @@ var results = BenchmarkUtility.GetResults(""MyBenchmark""); return stopwatch; } - [Documentation("Record", "Records the execution time of the provided action and returns the elapsed time in milliseconds.", null, @"```csharp + /// + /// Records the execution time of the provided action and returns the elapsed time in milliseconds. + /// + /// The actionToRecord parameter + /// The return value +[Documentation("Record", "Records the execution time of the provided action and returns the elapsed time in milliseconds.", null, @"```csharp var time = BenchmarkUtility.Record(() => { // Code to benchmark @@ -53,7 +66,13 @@ Console.WriteLine($""Elapsed time: {time} ms""); 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 + /// + /// Records the execution time of the provided action, saves it under the given key, and returns the elapsed time in milliseconds. + /// + /// The key parameter + /// The actionToRecord parameter + /// The return value +[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"", () => { // Code to benchmark @@ -85,7 +104,11 @@ Console.WriteLine($""Average time: {averageTime} ms""); return stopwatch.ElapsedMilliseconds; } - [Documentation("Start", "Starts or restarts the stopwatch for the given key.", null, null)] + /// + /// Starts or restarts the stopwatch for the given key. + /// + /// The key parameter +[Documentation("Start", "Starts or restarts the stopwatch for the given key.", null, null)] /// /// Starts or restarts the stopwatch for the given key. /// @@ -95,7 +118,11 @@ Console.WriteLine($""Average time: {averageTime} ms""); GetStopwatch(key).Restart(); } - [Documentation("Stop", "Stops the stopwatch for the given key and records the elapsed time.", null, null)] + /// + /// Stops the stopwatch for the given key and records the elapsed time. + /// + /// The key parameter +[Documentation("Stop", "Stops the stopwatch for the given key and records the elapsed time.", null, null)] /// /// Stops the stopwatch for the given key and records the elapsed time. /// @@ -111,7 +138,12 @@ Console.WriteLine($""Average time: {averageTime} ms""); _benchmarks[key].Add(GetStopwatch(key).ElapsedMilliseconds); } - [Documentation("GetResults", "Retrieves the list of recorded times for the given key.", null, null)] + /// + /// Retrieves the list of recorded times for the given key. + /// + /// The key parameter + /// The return value +[Documentation("GetResults", "Retrieves the list of recorded times for the given key.", null, null)] /// /// Retrieves the list of recorded times for the given key. /// @@ -122,13 +154,20 @@ Console.WriteLine($""Average time: {averageTime} ms""); return _benchmarks.ContainsKey(key) ? _benchmarks[key] : new List(); } - [Documentation("ClearResults", "Clears all recorded benchmark results.", null, null)] + /// + /// Clears all recorded benchmark results. + /// +[Documentation("ClearResults", "Clears all recorded benchmark results.", null, null)] public static void ClearResults() { _benchmarks.Clear(); } - [Documentation("GetAllResults", "Retrieves all recorded benchmark results.", null, null)] + /// + /// Retrieves all recorded benchmark results. + /// + /// The return value +[Documentation("GetAllResults", "Retrieves all recorded benchmark results.", null, null)] public static Dictionary> GetAllResults() { return new Dictionary>(_benchmarks);