WIP: Auto-generate triple-slash comments without additional packages

Co-authored-by: omegaleo <5655201+omegaleo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-11 23:36:40 +00:00
parent 5f42d8c9b2
commit 2957b19889
3 changed files with 343 additions and 9 deletions
@@ -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";
}
}