From fcd09709e0b47184b3ad100baa3afddf1698d84f Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Fri, 17 Oct 2025 19:28:29 +0700 Subject: [PATCH] Refactor coding style: use explicit types and improve formatting - Replace var with explicit type declarations - Standardize whitespace and indentation - Update example code to use PascalCase for fields - Consolidate attribute class declaration to single line --- .../file-name-rule-analyzer.cs | 42 +- .../interface-delegation-attribute.cs | 16 +- .../interface-delegation-generator.cs | 606 ++++++------ .../markdown-docs-generator.cs | 883 +++++++++--------- .../xml-docs-to-markdown-analyzer.cs | 106 +-- 5 files changed, 827 insertions(+), 826 deletions(-) diff --git a/source/timewarp-source-generators/file-name-rule-analyzer.cs b/source/timewarp-source-generators/file-name-rule-analyzer.cs index fe0b85f..b188927 100644 --- a/source/timewarp-source-generators/file-name-rule-analyzer.cs +++ b/source/timewarp-source-generators/file-name-rule-analyzer.cs @@ -5,7 +5,7 @@ public class FileNameRuleAnalyzer : IIncrementalGenerator { public const string DiagnosticId = "TWA001"; private const string Category = "Naming"; - + private static readonly DiagnosticDescriptor Rule = new( DiagnosticId, "File name should use kebab-case", @@ -15,10 +15,10 @@ public class FileNameRuleAnalyzer : IIncrementalGenerator isEnabledByDefault: false, description: "C# file names should use kebab-case format with hyphens separating words, all lowercase." ); - + // Regex pattern for valid kebab-case file names private static readonly Regex KebabCasePattern = new(@"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.cs$", RegexOptions.Compiled); - + // Default exception patterns private static readonly string[] DefaultExceptions = [ @@ -38,7 +38,7 @@ public class FileNameRuleAnalyzer : IIncrementalGenerator "AnalyzerReleases.Shipped.md", "AnalyzerReleases.Unshipped.md" ]; - + public void Initialize(IncrementalGeneratorInitializationContext context) { // Create a value provider that provides all syntax trees with config options @@ -49,7 +49,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) (Compilation compilation, AnalyzerConfigOptionsProvider configOptions) = source; return compilation.SyntaxTrees.Select(tree => (tree, configOptions)); }); - + // Register diagnostics for each syntax tree context.RegisterSourceOutput(syntaxTreesWithConfig, (spc, source) => { @@ -57,28 +57,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AnalyzeFileNaming(spc, tree, configOptions); }); } - + private void AnalyzeFileNaming(SourceProductionContext context, SyntaxTree tree, AnalyzerConfigOptionsProvider configOptions) { string filePath = tree.FilePath; - + // Skip if file path is empty or null if (string.IsNullOrEmpty(filePath)) return; - + string fileName = Path.GetFileName(filePath); - + // Skip if not a C# file if (!fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) return; - + // Get configured exceptions string[] exceptions = GetConfiguredExceptions(configOptions, tree); - + // Check if file matches any exception pattern if (IsFileExcepted(fileName, exceptions)) return; - + // Check if file name follows kebab-case pattern if (!KebabCasePattern.IsMatch(fileName)) { @@ -86,35 +86,35 @@ private void AnalyzeFileNaming(SourceProductionContext context, SyntaxTree tree, tree, TextSpan.FromBounds(0, 0) ); - + var diagnostic = Diagnostic.Create(Rule, location, fileName); context.ReportDiagnostic(diagnostic); } } - + private string[] GetConfiguredExceptions(AnalyzerConfigOptionsProvider configOptions, SyntaxTree tree) { // Get file-specific options AnalyzerConfigOptions options = configOptions.GetOptions(tree); - + // Try to get configured exceptions from .editorconfig if (options.TryGetValue( - "dotnet_diagnostic.TWA001.excluded_files", + "dotnet_diagnostic.TWA001.excluded_files", out string? configuredExceptions) && !string.IsNullOrEmpty(configuredExceptions)) { // Split by semicolon and trim whitespace IEnumerable additionalExceptions = configuredExceptions .Split([';'], StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()); - + // Merge defaults with configured exceptions return [.. DefaultExceptions, .. additionalExceptions]; } - + // Return default exceptions if not configured return DefaultExceptions; } - + private bool IsFileExcepted(string fileName, string[] exceptions) { foreach (string exception in exceptions) @@ -125,7 +125,7 @@ private bool IsFileExcepted(string fileName, string[] exceptions) string pattern = exception .Replace(".", "\\.") .Replace("*", ".*"); - + if (Regex.IsMatch(fileName, $"^{pattern}$", RegexOptions.IgnoreCase)) return true; } @@ -136,7 +136,7 @@ private bool IsFileExcepted(string fileName, string[] exceptions) return true; } } - + return false; } } \ No newline at end of file diff --git a/source/timewarp-source-generators/interface-delegation-attribute.cs b/source/timewarp-source-generators/interface-delegation-attribute.cs index 6da621b..bbaeb83 100644 --- a/source/timewarp-source-generators/interface-delegation-attribute.cs +++ b/source/timewarp-source-generators/interface-delegation-attribute.cs @@ -23,22 +23,20 @@ namespace TimeWarp.SourceGenerators; /// public partial class DataService : ILogger, IDataProcessor<string> /// { /// [Implements] -/// private readonly ILogger _logger; +/// private readonly ILogger Logger; /// /// [Implements] -/// private readonly IDataProcessor<string> _processor; +/// private readonly IDataProcessor<string> Processor; /// /// public DataService(ILogger logger, IDataProcessor<string> processor) /// { -/// _logger = logger; -/// _processor = processor; +/// Logger = logger; +/// Processor = processor; /// } /// } /// -/// The generator will create forwarding implementations for all ILogger methods/properties to _logger -/// and all IDataProcessor<string> methods/properties to _processor. +/// The generator will create forwarding implementations for all ILogger methods/properties to Logger +/// and all IDataProcessor<string> methods/properties to Processor. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] -public class ImplementsAttribute : Attribute -{ -} +public class ImplementsAttribute : Attribute { } diff --git a/source/timewarp-source-generators/interface-delegation-generator.cs b/source/timewarp-source-generators/interface-delegation-generator.cs index 8700a9b..ee65410 100644 --- a/source/timewarp-source-generators/interface-delegation-generator.cs +++ b/source/timewarp-source-generators/interface-delegation-generator.cs @@ -1,41 +1,43 @@ namespace TimeWarp.SourceGenerators; +using System.Collections.Immutable; + [Generator] public class InterfaceDelegationGenerator : IIncrementalGenerator { - private static readonly DiagnosticDescriptor ClassNotPartialDescriptor = new( - id: "TW1001", - title: "Class must be partial for interface delegation", - messageFormat: "Class '{0}' must be marked as partial to use [Implements] attribute", - category: "InterfaceDelegation", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor InterfaceNotImplementedDescriptor = new( - id: "TW1002", - title: "Class does not implement the delegated interface", - messageFormat: "Class '{0}' must implement interface '{1}' to delegate to field/property '{2}'", - category: "InterfaceDelegation", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor DuplicateDelegationDescriptor = new( - id: "TW1003", - title: "Multiple fields delegate the same interface", - messageFormat: "Interface '{0}' is delegated by multiple fields/properties in class '{1}'", - category: "InterfaceDelegation", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - public void Initialize(IncrementalGeneratorInitializationContext context) + private static readonly DiagnosticDescriptor ClassNotPartialDescriptor = new( + id: "TW1001", + title: "Class must be partial for interface delegation", + messageFormat: "Class '{0}' must be marked as partial to use [Implements] attribute", + category: "InterfaceDelegation", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor InterfaceNotImplementedDescriptor = new( + id: "TW1002", + title: "Class does not implement the delegated interface", + messageFormat: "Class '{0}' must implement interface '{1}' to delegate to field/property '{2}'", + category: "InterfaceDelegation", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor DuplicateDelegationDescriptor = new( + id: "TW1003", + title: "Multiple fields delegate the same interface", + messageFormat: "Interface '{0}' is delegated by multiple fields/properties in class '{1}'", + category: "InterfaceDelegation", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Generate the ImplementsAttribute source code + context.RegisterPostInitializationOutput(ctx => { - // Generate the ImplementsAttribute source code - context.RegisterPostInitializationOutput(ctx => - { - var attributeSource = @"// + string attributeSource = @"// #nullable enable namespace TimeWarp.SourceGenerators @@ -49,329 +51,329 @@ internal class ImplementsAttribute : System.Attribute } } "; - ctx.AddSource("ImplementsAttribute.g.cs", SourceText.From(attributeSource, Encoding.UTF8)); - }); - - // Find all fields and properties marked with [Implements] attribute - IncrementalValuesProvider<(MemberDeclarationSyntax?, ClassDeclarationSyntax?, SemanticModel?)> implementsMembers = - context.SyntaxProvider - .CreateSyntaxProvider( - predicate: (node, _) => IsMemberWithAttribute(node), - transform: (ctx, _) => - { - var member = (MemberDeclarationSyntax)ctx.Node; - - // Check if any symbol from this member has the ImplementsAttribute - var symbolsToCheck = new List(); - if (member is FieldDeclarationSyntax fieldDecl) - { - // For fields, check each variable declarator - foreach (var variable in fieldDecl.Declaration.Variables) - { - var symbol = ctx.SemanticModel.GetDeclaredSymbol(variable); - if (symbol != null) symbolsToCheck.Add(symbol); - } - } - else - { - var symbol = ctx.SemanticModel.GetDeclaredSymbol(member); - if (symbol != null) symbolsToCheck.Add(symbol); - } - - // Check if any of these symbols have the ImplementsAttribute - var hasImplementsAttribute = symbolsToCheck.Any(s => - s.GetAttributes().Any(ad => - ad.AttributeClass?.Name == "ImplementsAttribute" || - ad.AttributeClass?.Name == "Implements")); - - if (!hasImplementsAttribute) - return ((MemberDeclarationSyntax?)null, (ClassDeclarationSyntax?)null, (SemanticModel?)null); - - var classDeclaration = member.Ancestors().OfType().FirstOrDefault(); - return (member, classDeclaration, ctx.SemanticModel); - }) - .Where(tuple => tuple.Item1 != null && tuple.Item2 != null); - - // Generate delegation code for each class - context.RegisterSourceOutput(implementsMembers.Collect(), (sourceContext, members) => - { - // Group by class - var groupedByClass = members.GroupBy(m => m.Item2); - - foreach (var classGroup in groupedByClass) - { - var classDeclaration = classGroup.Key!; - var className = classDeclaration.Identifier.Text; - - // Check if class is partial - if (!classDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + ctx.AddSource("ImplementsAttribute.g.cs", SourceText.From(attributeSource, Encoding.UTF8)); + }); + + // Find all fields and properties marked with [Implements] attribute + IncrementalValuesProvider<(MemberDeclarationSyntax?, ClassDeclarationSyntax?, SemanticModel?)> implementsMembers = + context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, _) => IsMemberWithAttribute(node), + transform: (ctx, _) => { - sourceContext.ReportDiagnostic( - Diagnostic.Create( - ClassNotPartialDescriptor, - classDeclaration.Identifier.GetLocation(), - className - ) - ); - continue; - } - - // Get semantic model (use first member's semantic model) - var semanticModel = classGroup.First().Item3!; - var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); - if (classSymbol == null) continue; - - // Get namespace - var namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); - var namespaceName = namespaceDecl?.Name.ToString(); - - // Process each [Implements] member - var delegations = new List<(string MemberName, ITypeSymbol InterfaceType, List GeneratedCode)>(); - var seenInterfaces = new HashSet(); - - foreach (var item in classGroup) - { - var member = item.Item1!; - // Get the symbols for this member (fields may have multiple variables) - var memberSymbols = new List(); - if (member is FieldDeclarationSyntax fieldDecl) + var member = (MemberDeclarationSyntax)ctx.Node; + + // Check if any symbol from this member has the ImplementsAttribute + var symbolsToCheck = new List(); + if (member is FieldDeclarationSyntax fieldDecl) + { + // For fields, check each variable declarator + foreach (VariableDeclaratorSyntax variable in fieldDecl.Declaration.Variables) { - foreach (var variable in fieldDecl.Declaration.Variables) - { - var symbol = semanticModel.GetDeclaredSymbol(variable); - if (symbol != null) memberSymbols.Add(symbol); - } + ISymbol? symbol = ctx.SemanticModel.GetDeclaredSymbol(variable); + if (symbol != null) symbolsToCheck.Add(symbol); } - else - { - var symbol = semanticModel.GetDeclaredSymbol(member); - if (symbol != null) memberSymbols.Add(symbol); - } - - foreach (var memberSymbol in memberSymbols) - { - // Check if this specific symbol has the ImplementsAttribute - var hasImplementsAttr = memberSymbol.GetAttributes().Any(ad => + } + else + { + ISymbol? symbol = ctx.SemanticModel.GetDeclaredSymbol(member); + if (symbol != null) symbolsToCheck.Add(symbol); + } + + // Check if any of these symbols have the ImplementsAttribute + bool hasImplementsAttribute = symbolsToCheck.Any(s => + s.GetAttributes().Any(ad => ad.AttributeClass?.Name == "ImplementsAttribute" || - ad.AttributeClass?.Name == "Implements"); + ad.AttributeClass?.Name == "Implements")); - if (!hasImplementsAttr) continue; + if (!hasImplementsAttribute) + return ((MemberDeclarationSyntax?)null, (ClassDeclarationSyntax?)null, (SemanticModel?)null); - var memberName = memberSymbol.Name; - var memberType = memberSymbol switch - { - IFieldSymbol field => field.Type, - IPropertySymbol property => property.Type, - _ => null - }; + ClassDeclarationSyntax classDeclaration = member.Ancestors().OfType().FirstOrDefault(); + return (member, classDeclaration, ctx.SemanticModel); + }) + .Where(tuple => tuple.Item1 != null && tuple.Item2 != null); - if (memberType == null) continue; + // Generate delegation code for each class + context.RegisterSourceOutput(implementsMembers.Collect(), (sourceContext, members) => + { + // Group by class + IEnumerable> groupedByClass = members.GroupBy(m => m.Item2); - // Check if the member type is an interface - if (memberType.TypeKind != TypeKind.Interface) - { - // Could be a class that implements an interface - find matching interface - var implementedInterfaces = memberType.AllInterfaces; - var matchingInterface = implementedInterfaces.FirstOrDefault(i => - classSymbol.AllInterfaces.Any(ci => SymbolEqualityComparer.Default.Equals(ci, i))); - - if (matchingInterface != null) - memberType = matchingInterface; - else - continue; - } + foreach (IGrouping? classGroup in groupedByClass) + { + ClassDeclarationSyntax classDeclaration = classGroup.Key!; + string className = classDeclaration.Identifier.Text; - var interfaceType = memberType; - var interfaceFullName = interfaceType.ToDisplayString(); + // Check if class is partial + if (!classDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + sourceContext.ReportDiagnostic( + Diagnostic.Create( + ClassNotPartialDescriptor, + classDeclaration.Identifier.GetLocation(), + className + ) + ); + continue; + } - // Check if class implements this interface - var implementsInterface = classSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i, interfaceType)); + // Get semantic model (use first member's semantic model) + SemanticModel semanticModel = classGroup.First().Item3!; + INamedTypeSymbol? classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + if (classSymbol == null) continue; - if (!implementsInterface) - { - sourceContext.ReportDiagnostic( - Diagnostic.Create( - InterfaceNotImplementedDescriptor, - member.GetLocation(), - className, - interfaceFullName, - memberName - ) - ); - continue; - } + // Get namespace + BaseNamespaceDeclarationSyntax namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); + string? namespaceName = namespaceDecl?.Name.ToString(); - // Check for duplicate delegations - if (!seenInterfaces.Add(interfaceFullName)) - { - sourceContext.ReportDiagnostic( - Diagnostic.Create( - DuplicateDelegationDescriptor, - member.GetLocation(), - interfaceFullName, - className - ) - ); - continue; - } + // Process each [Implements] member + var delegations = new List<(string MemberName, ITypeSymbol InterfaceType, List GeneratedCode)>(); + var seenInterfaces = new HashSet(); - // Generate delegation code for this interface - var generatedCode = GenerateInterfaceDelegation( - interfaceType, - memberName, - classSymbol, - semanticModel - ); + foreach ((MemberDeclarationSyntax?, ClassDeclarationSyntax?, SemanticModel?) item in classGroup) + { + MemberDeclarationSyntax member = item.Item1!; + // Get the symbols for this member (fields may have multiple variables) + var memberSymbols = new List(); + if (member is FieldDeclarationSyntax fieldDecl) + { + foreach (VariableDeclaratorSyntax variable in fieldDecl.Declaration.Variables) + { + ISymbol? symbol = semanticModel.GetDeclaredSymbol(variable); + if (symbol != null) memberSymbols.Add(symbol); + } + } + else + { + ISymbol? symbol = semanticModel.GetDeclaredSymbol(member); + if (symbol != null) memberSymbols.Add(symbol); + } + + foreach (ISymbol memberSymbol in memberSymbols) + { + // Check if this specific symbol has the ImplementsAttribute + bool hasImplementsAttr = memberSymbol.GetAttributes().Any(ad => + ad.AttributeClass?.Name == "ImplementsAttribute" || + ad.AttributeClass?.Name == "Implements"); + + if (!hasImplementsAttr) continue; + + string memberName = memberSymbol.Name; + ITypeSymbol? memberType = memberSymbol switch + { + IFieldSymbol field => field.Type, + IPropertySymbol property => property.Type, + _ => null + }; - delegations.Add((memberName, interfaceType, generatedCode)); - } - } + if (memberType == null) continue; - // Generate the source file - if (delegations.Any()) - { - var sourceCode = GenerateSourceFile( - className, - namespaceName, - delegations - ); - - sourceContext.AddSource( - $"{className}.implements.g.cs", - SourceText.From(sourceCode, Encoding.UTF8) - ); - } + // Check if the member type is an interface + if (memberType.TypeKind != TypeKind.Interface) + { + // Could be a class that implements an interface - find matching interface + ImmutableArray implementedInterfaces = memberType.AllInterfaces; + INamedTypeSymbol? matchingInterface = implementedInterfaces.FirstOrDefault(i => + classSymbol.AllInterfaces.Any(ci => SymbolEqualityComparer.Default.Equals(ci, i))); + + if (matchingInterface != null) + memberType = matchingInterface; + else + continue; } - }); - } - private static bool IsMemberWithAttribute(SyntaxNode node) - { - if (node is not MemberDeclarationSyntax member) - return false; + ITypeSymbol interfaceType = memberType; + string interfaceFullName = interfaceType.ToDisplayString(); - if (member is not FieldDeclarationSyntax && member is not PropertyDeclarationSyntax) - return false; + // Check if class implements this interface + bool implementsInterface = classSymbol.AllInterfaces.Any(i => + SymbolEqualityComparer.Default.Equals(i, interfaceType)); - // Check if it has any attributes - return member.AttributeLists.Count > 0; - } + if (!implementsInterface) + { + sourceContext.ReportDiagnostic( + Diagnostic.Create( + InterfaceNotImplementedDescriptor, + member.GetLocation(), + className, + interfaceFullName, + memberName + ) + ); + continue; + } - private static List GenerateInterfaceDelegation( - ITypeSymbol interfaceType, - string delegateMemberName, - INamedTypeSymbol classSymbol, - SemanticModel semanticModel) - { - var code = new List(); + // Check for duplicate delegations + if (!seenInterfaces.Add(interfaceFullName)) + { + sourceContext.ReportDiagnostic( + Diagnostic.Create( + DuplicateDelegationDescriptor, + member.GetLocation(), + interfaceFullName, + className + ) + ); + continue; + } + + // Generate delegation code for this interface + List generatedCode = GenerateInterfaceDelegation( + interfaceType, + memberName, + classSymbol, + semanticModel + ); - // Get all members of the interface - var interfaceMembers = interfaceType.GetMembers(); + delegations.Add((memberName, interfaceType, generatedCode)); + } + } - foreach (var member in interfaceMembers) + // Generate the source file + if (delegations.Any()) { - // Skip if the class already implements this member explicitly - var existingImplementation = classSymbol.GetMembers(member.Name) + string sourceCode = GenerateSourceFile( + className, + namespaceName, + delegations + ); + + sourceContext.AddSource( + $"{className}.implements.g.cs", + SourceText.From(sourceCode, Encoding.UTF8) + ); + } + } + }); + } + + private static bool IsMemberWithAttribute(SyntaxNode node) + { + if (node is not MemberDeclarationSyntax member) + return false; + + if (member is not FieldDeclarationSyntax && member is not PropertyDeclarationSyntax) + return false; + + // Check if it has any attributes + return member.AttributeLists.Count > 0; + } + + private static List GenerateInterfaceDelegation( + ITypeSymbol interfaceType, + string delegateMemberName, + INamedTypeSymbol classSymbol, + SemanticModel semanticModel) + { + var code = new List(); + + // Get all members of the interface + ImmutableArray interfaceMembers = interfaceType.GetMembers(); + + foreach (ISymbol member in interfaceMembers) + { + // Skip if the class already implements this member explicitly + ISymbol? existingImplementation = classSymbol.GetMembers(member.Name) .FirstOrDefault(m => !m.IsImplicitlyDeclared); - if (existingImplementation != null) - continue; // Allow manual override - - switch (member) - { - case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary: - code.Add(GenerateMethodDelegation(method, delegateMemberName)); - break; + if (existingImplementation != null) + continue; // Allow manual override - case IPropertySymbol property: - code.Add(GeneratePropertyDelegation(property, delegateMemberName)); - break; + switch (member) + { + case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary: + code.Add(GenerateMethodDelegation(method, delegateMemberName)); + break; - case IEventSymbol eventSymbol: - code.Add(GenerateEventDelegation(eventSymbol, delegateMemberName)); - break; - } - } + case IPropertySymbol property: + code.Add(GeneratePropertyDelegation(property, delegateMemberName)); + break; - return code; + case IEventSymbol eventSymbol: + code.Add(GenerateEventDelegation(eventSymbol, delegateMemberName)); + break; + } } - private static string GenerateMethodDelegation(IMethodSymbol method, string delegateMemberName) - { - var returnType = method.ReturnType.ToDisplayString(); - var methodName = method.Name; - var parameters = string.Join(", ", method.Parameters.Select(p => + return code; + } + + private static string GenerateMethodDelegation(IMethodSymbol method, string delegateMemberName) + { + string returnType = method.ReturnType.ToDisplayString(); + string methodName = method.Name; + string parameters = string.Join(", ", method.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}")); - var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); + string arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); - var returnKeyword = method.ReturnsVoid ? "" : "return "; + string returnKeyword = method.ReturnsVoid ? "" : "return "; - return $@" public {returnType} {methodName}({parameters}) + return $@" public {returnType} {methodName}({parameters}) {{ {returnKeyword}{delegateMemberName}.{methodName}({arguments}); }}"; - } + } - private static string GeneratePropertyDelegation(IPropertySymbol property, string delegateMemberName) - { - var propertyType = property.Type.ToDisplayString(); - var propertyName = property.Name; + private static string GeneratePropertyDelegation(IPropertySymbol property, string delegateMemberName) + { + string propertyType = property.Type.ToDisplayString(); + string propertyName = property.Name; - var getter = property.GetMethod != null ? $" get => {delegateMemberName}.{propertyName};" : ""; - var setter = property.SetMethod != null ? $" set => {delegateMemberName}.{propertyName} = value;" : ""; + string getter = property.GetMethod != null ? $" get => {delegateMemberName}.{propertyName};" : ""; + string setter = property.SetMethod != null ? $" set => {delegateMemberName}.{propertyName} = value;" : ""; - return $@" public {propertyType} {propertyName} + return $@" public {propertyType} {propertyName} {{ {getter}{setter} }}"; - } + } - private static string GenerateEventDelegation(IEventSymbol eventSymbol, string delegateMemberName) - { - var eventType = eventSymbol.Type.ToDisplayString(); - var eventName = eventSymbol.Name; + private static string GenerateEventDelegation(IEventSymbol eventSymbol, string delegateMemberName) + { + string eventType = eventSymbol.Type.ToDisplayString(); + string eventName = eventSymbol.Name; - return $@" public event {eventType} {eventName} + return $@" public event {eventType} {eventName} {{ add => {delegateMemberName}.{eventName} += value; remove => {delegateMemberName}.{eventName} -= value; }}"; - } + } - private static string GenerateSourceFile( - string className, - string? namespaceName, - List<(string MemberName, ITypeSymbol InterfaceType, List GeneratedCode)> delegations) - { - var builder = new StringBuilder(); + private static string GenerateSourceFile( + string className, + string? namespaceName, + List<(string MemberName, ITypeSymbol InterfaceType, List GeneratedCode)> delegations) + { + var builder = new StringBuilder(); - builder.AppendLine("// "); - builder.AppendLine("#nullable enable"); - builder.AppendLine(); + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); - if (!string.IsNullOrEmpty(namespaceName)) - { - builder.AppendLine($"namespace {namespaceName};"); - builder.AppendLine(); - } + if (!string.IsNullOrEmpty(namespaceName)) + { + builder.AppendLine($"namespace {namespaceName};"); + builder.AppendLine(); + } - builder.AppendLine($"// Interface delegation for {className}"); - builder.AppendLine($"public partial class {className}"); - builder.AppendLine("{"); + builder.AppendLine($"// Interface delegation for {className}"); + builder.AppendLine($"public partial class {className}"); + builder.AppendLine("{"); - foreach (var (memberName, interfaceType, generatedCode) in delegations) - { - builder.AppendLine($" // Delegation to {memberName} for {interfaceType.ToDisplayString()}"); - foreach (var code in generatedCode) - { - builder.AppendLine(code); - builder.AppendLine(); - } - } + foreach ((string? memberName, ITypeSymbol? interfaceType, List? generatedCode) in delegations) + { + builder.AppendLine($" // Delegation to {memberName} for {interfaceType.ToDisplayString()}"); + foreach (string code in generatedCode) + { + builder.AppendLine(code); + builder.AppendLine(); + } + } - builder.AppendLine("}"); + builder.AppendLine("}"); - return builder.ToString(); - } + return builder.ToString(); + } } diff --git a/source/timewarp-source-generators/markdown-docs-generator.cs b/source/timewarp-source-generators/markdown-docs-generator.cs index d8a509d..14f618c 100644 --- a/source/timewarp-source-generators/markdown-docs-generator.cs +++ b/source/timewarp-source-generators/markdown-docs-generator.cs @@ -3,93 +3,93 @@ namespace TimeWarp.SourceGenerators; [Generator] public class MarkdownDocsGenerator : IIncrementalGenerator { - // Regex pattern for checking if a file name is kebab-case - private static readonly Regex KebabCasePattern = new(@"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", RegexOptions.Compiled); - private static readonly DiagnosticDescriptor MarkdownDocsGeneratorLoadedDescriptor = new( - id: "TWG001", - title: "MarkdownDocs Generator Loaded", - messageFormat: "The MarkdownDocs generator has been loaded and initialized", - category: "SourceGenerator", - DiagnosticSeverity.Info, - isEnabledByDefault: true - ); - - public void Initialize(IncrementalGeneratorInitializationContext context) + // Regex pattern for checking if a file name is kebab-case + private static readonly Regex KebabCasePattern = new(@"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly DiagnosticDescriptor MarkdownDocsGeneratorLoadedDescriptor = new( + id: "TWG001", + title: "MarkdownDocs Generator Loaded", + messageFormat: "The MarkdownDocs generator has been loaded and initialized", + category: "SourceGenerator", + DiagnosticSeverity.Info, + isEnabledByDefault: true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Report initialization diagnostic + IncrementalValueProvider initializationTrigger = context.CompilationProvider + .Select((compilation, _) => true); + + context.RegisterSourceOutput(initializationTrigger, (sourceContext, _) => { - // Report initialization diagnostic - IncrementalValueProvider initializationTrigger = context.CompilationProvider - .Select((compilation, _) => true); + sourceContext.ReportDiagnostic( + Diagnostic.Create(MarkdownDocsGeneratorLoadedDescriptor, Location.None) + ); + }); + + // Find all class declarations in C# files + IncrementalValuesProvider<(ClassDeclarationSyntax ClassDeclaration, string? Namespace, string FilePath)> classDeclarations = + context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => + { + var classDeclaration = (ClassDeclarationSyntax)ctx.Node; + var namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); + var filePath = ctx.Node.SyntaxTree.FilePath; + return (classDeclaration, namespaceDecl?.Name.ToString(), filePath); + }); + + // Find all .md files + IncrementalValuesProvider markdownFiles = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)); + + // Group class declarations by class name to avoid duplicates + var groupedClasses = classDeclarations + .Collect() + .Select((classes, _) => classes + .GroupBy(c => (c.ClassDeclaration.Identifier.Text, c.Namespace)) + .Select(g => g.First()) + .ToArray()); + + // Combine into pairs + var pairs = groupedClasses.Combine(markdownFiles.Collect()); + + // Generate documentation for matching pairs + context.RegisterSourceOutput(pairs, (sourceContext, pair) => + { + var (classGroups, markdownTexts) = pair; + + foreach (var (classDeclaration, namespaceName, filePath) in classGroups) + { + var className = classDeclaration.Identifier.Text; + + // Extract source file name without extension + var sourceFileName = Path.GetFileNameWithoutExtension(filePath); + + // Find matching markdown file using appropriate strategy + AdditionalText? matchingMd = null; - context.RegisterSourceOutput(initializationTrigger, (sourceContext, _) => + // If source file is kebab-case, try to match kebab-case markdown first + if (IsKebabCase(sourceFileName)) { - sourceContext.ReportDiagnostic( - Diagnostic.Create(MarkdownDocsGeneratorLoadedDescriptor, Location.None) - ); - }); - - // Find all class declarations in C# files - IncrementalValuesProvider<(ClassDeclarationSyntax ClassDeclaration, string? Namespace, string FilePath)> classDeclarations = - context.SyntaxProvider - .CreateSyntaxProvider( - predicate: (s, _) => s is ClassDeclarationSyntax, - transform: (ctx, _) => - { - var classDeclaration = (ClassDeclarationSyntax)ctx.Node; - var namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); - var filePath = ctx.Node.SyntaxTree.FilePath; - return (classDeclaration, namespaceDecl?.Name.ToString(), filePath); - }); - - // Find all .md files - IncrementalValuesProvider markdownFiles = context.AdditionalTextsProvider - .Where(file => file.Path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)); - - // Group class declarations by class name to avoid duplicates - var groupedClasses = classDeclarations - .Collect() - .Select((classes, _) => classes - .GroupBy(c => (c.ClassDeclaration.Identifier.Text, c.Namespace)) - .Select(g => g.First()) - .ToArray()); - - // Combine into pairs - var pairs = groupedClasses.Combine(markdownFiles.Collect()); - - // Generate documentation for matching pairs - context.RegisterSourceOutput(pairs, (sourceContext, pair) => + matchingMd = markdownTexts.FirstOrDefault(md => + Path.GetFileNameWithoutExtension(md.Path).Equals(sourceFileName, StringComparison.OrdinalIgnoreCase)); + } + + // Fall back to class name match (for backward compatibility or PascalCase files) + if (matchingMd == null) { - var (classGroups, markdownTexts) = pair; - - foreach (var (classDeclaration, namespaceName, filePath) in classGroups) - { - var className = classDeclaration.Identifier.Text; - - // Extract source file name without extension - var sourceFileName = Path.GetFileNameWithoutExtension(filePath); - - // Find matching markdown file using appropriate strategy - AdditionalText? matchingMd = null; - - // If source file is kebab-case, try to match kebab-case markdown first - if (IsKebabCase(sourceFileName)) - { - matchingMd = markdownTexts.FirstOrDefault(md => - Path.GetFileNameWithoutExtension(md.Path).Equals(sourceFileName, StringComparison.OrdinalIgnoreCase)); - } - - // Fall back to class name match (for backward compatibility or PascalCase files) - if (matchingMd == null) - { - matchingMd = markdownTexts.FirstOrDefault(md => - Path.GetFileNameWithoutExtension(md.Path).Equals(className, StringComparison.OrdinalIgnoreCase)); - } + matchingMd = markdownTexts.FirstOrDefault(md => + Path.GetFileNameWithoutExtension(md.Path).Equals(className, StringComparison.OrdinalIgnoreCase)); + } - if (matchingMd != null) - { - var markdownContent = matchingMd.GetText()?.ToString() ?? string.Empty; - var (classDocs, methodDocs, propertyDocs) = ConvertMarkdownToXmlDocs(markdownContent, classDeclaration); + if (matchingMd != null) + { + var markdownContent = matchingMd.GetText()?.ToString() ?? string.Empty; + var (classDocs, methodDocs, propertyDocs) = ConvertMarkdownToXmlDocs(markdownContent, classDeclaration); - var sourceText = SourceText.From($@"// Auto-generated documentation for {className} + var sourceText = SourceText.From($@"// Auto-generated documentation for {className} #nullable enable {(namespaceName != null ? $"namespace {namespaceName};" : "")} @@ -102,422 +102,423 @@ public partial class {className} }} ", Encoding.UTF8); - sourceContext.AddSource($"{className}.docs.g.cs", sourceText); - } - } - }); - } - - private static (string ClassDocs, string MethodDocs, string PropertyDocs) ConvertMarkdownToXmlDocs(string markdownContent, ClassDeclarationSyntax classDeclaration) - { - var classBuilder = new StringBuilder(); - var methodBuilder = new StringBuilder(); - var propertyBuilder = new StringBuilder(); - var reader = new StringReader(markdownContent); - string? line; - - // State tracking - var currentSection = ""; - var contentBuilder = new StringBuilder(); - var inMethodSection = false; - var inPropertySection = false; - var currentMethod = ""; - var currentMethodDescription = ""; - var currentProperty = ""; - var currentPropertyDescription = ""; - - // Get method signatures from the class declaration - var methodSignatures = classDeclaration.Members - .OfType() - .ToDictionary( - m => m.Identifier.Text, - m => $"public partial {m.ReturnType} {m.Identifier}({string.Join(", ", m.ParameterList.Parameters.Select(p => $"{p.Type} {p.Identifier}{(p.Default != null ? " = " + p.Default.Value.ToString() : "")}"))})" - ); - - // Get property signatures from the class declaration (only partial properties) - var propertySignatures = classDeclaration.Members - .OfType() - .Where(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - .ToDictionary( - p => p.Identifier.Text, - p => { - var modifiers = string.Join(" ", p.Modifiers.Select(m => m.Text)); - var accessors = new List(); - - if (p.AccessorList != null) - { - foreach (var accessor in p.AccessorList.Accessors) - { - if (accessor.Keyword.Text == "get") - { - // For non-nullable reference types, initialize with default - var typeStr = p.Type?.ToString() ?? ""; - if (typeStr == "string" && !typeStr.EndsWith("?")) - { - accessors.Add("get => field ??= \"\""); - } - else - { - accessors.Add("get => field"); - } - } - else if (accessor.Keyword.Text == "set") - { - accessors.Add("set => field = value"); - } - else if (accessor.Keyword.Text == "init") - { - accessors.Add("init => field = value"); - } - } - } - - var accessorString = string.Join("; ", accessors); - return $"{modifiers} {p.Type} {p.Identifier} {{ {accessorString}; }}"; - } - ); - - while ((line = reader.ReadLine()) != null) - { - if (line.StartsWith("# ")) // Class name - skip - continue; - - if (line.StartsWith("## ")) - { - // Process previous section - if (inMethodSection) - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); - else if (inPropertySection) - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); - else - ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); - - // Start new section - currentSection = line.Substring(3).Trim(); - inMethodSection = currentSection == "Methods"; - inPropertySection = currentSection == "Properties"; - contentBuilder.Clear(); - currentMethodDescription = ""; - currentPropertyDescription = ""; - continue; - } - - if (line.StartsWith("### ") && inMethodSection) + sourceContext.AddSource($"{className}.docs.g.cs", sourceText); + } + } + }); + } + + private static (string ClassDocs, string MethodDocs, string PropertyDocs) ConvertMarkdownToXmlDocs(string markdownContent, ClassDeclarationSyntax classDeclaration) + { + var classBuilder = new StringBuilder(); + var methodBuilder = new StringBuilder(); + var propertyBuilder = new StringBuilder(); + var reader = new StringReader(markdownContent); + string? line; + + // State tracking + var currentSection = ""; + var contentBuilder = new StringBuilder(); + var inMethodSection = false; + var inPropertySection = false; + var currentMethod = ""; + var currentMethodDescription = ""; + var currentProperty = ""; + var currentPropertyDescription = ""; + + // Get method signatures from the class declaration + var methodSignatures = classDeclaration.Members + .OfType() + .ToDictionary( + m => m.Identifier.Text, + m => $"public partial {m.ReturnType} {m.Identifier}({string.Join(", ", m.ParameterList.Parameters.Select(p => $"{p.Type} {p.Identifier}{(p.Default != null ? " = " + p.Default.Value.ToString() : "")}"))})" + ); + + // Get property signatures from the class declaration (only partial properties) + var propertySignatures = classDeclaration.Members + .OfType() + .Where(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + .ToDictionary( + p => p.Identifier.Text, + p => { - // Process previous method if exists - if (!string.IsNullOrEmpty(currentMethod)) - { - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); - } - - // Start new method - currentMethod = line.Substring(4).Trim(); - contentBuilder.Clear(); - currentMethodDescription = ""; - continue; - } + var modifiers = string.Join(" ", p.Modifiers.Select(m => m.Text)); + var accessors = new List(); - if (line.StartsWith("### ") && inPropertySection) - { - // Process previous property if exists - if (!string.IsNullOrEmpty(currentProperty)) + if (p.AccessorList != null) + { + foreach (var accessor in p.AccessorList.Accessors) { - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + if (accessor.Keyword.Text == "get") + { + // For non-nullable reference types, initialize with default + var typeStr = p.Type?.ToString() ?? ""; + if (typeStr == "string" && !typeStr.EndsWith("?")) + { + accessors.Add("get => field ??= \"\""); + } + else + { + accessors.Add("get => field"); + } + } + else if (accessor.Keyword.Text == "set") + { + accessors.Add("set => field = value"); + } + else if (accessor.Keyword.Text == "init") + { + accessors.Add("init => field = value"); + } } + } - // Start new property - currentProperty = line.Substring(4).Trim(); - contentBuilder.Clear(); - currentPropertyDescription = ""; - continue; - } - - // If we haven't hit any section yet, this is the class summary - if (string.IsNullOrEmpty(currentSection) && !string.IsNullOrWhiteSpace(line)) - { - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"/// {line.Trim()}"); - classBuilder.AppendLine("/// "); - continue; + var accessorString = string.Join("; ", accessors); + return $"{modifiers} {p.Type} {p.Identifier} {{ {accessorString}; }}"; } + ); - // Capture method description (text before any #### subsections) - if (inMethodSection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentMethodDescription)) - { - currentMethodDescription = line.Trim(); - continue; - } - - // Capture property description (text before any #### subsections) - if (inPropertySection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentPropertyDescription)) - { - currentPropertyDescription = line.Trim(); - continue; - } - - // Add content to current section - if (!string.IsNullOrWhiteSpace(line)) - { - contentBuilder.AppendLine(line); - } - } + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("# ")) // Class name - skip + continue; - // Process the last section + if (line.StartsWith("## ")) + { + // Process previous section if (inMethodSection) - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); else if (inPropertySection) - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); else - ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); - - return (classBuilder.ToString(), methodBuilder.ToString(), propertyBuilder.ToString()); - } - - private static void ProcessSection(StringBuilder builder, string section, string content) - { - if (string.IsNullOrEmpty(content)) - return; - - switch (section) + ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); + + // Start new section + currentSection = line.Substring(3).Trim(); + inMethodSection = currentSection == "Methods"; + inPropertySection = currentSection == "Properties"; + contentBuilder.Clear(); + currentMethodDescription = ""; + currentPropertyDescription = ""; + continue; + } + + if (line.StartsWith("### ") && inMethodSection) + { + // Process previous method if exists + if (!string.IsNullOrEmpty(currentMethod)) { - case "Remarks": - builder.AppendLine("/// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($"/// {line.Trim()}"); - } - builder.AppendLine("/// "); - break; - - case "See Also": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"\[(.*?)\]\((.*?)\)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Convert angle brackets to curly braces for XML documentation - var xmlType = type.Replace('<', '{').Replace('>', '}'); - builder.AppendLine($"/// "); - } - } - break; - - case "Inheritance": - if (content.Contains("@")) - { - var match = Regex.Match(content, @"@(\S+)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Extract the type name and keep T as the generic parameter - var baseType = type.Split('<')[0]; - builder.AppendLine($"/// "); - } - } - break; + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + } - case "References": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"@(\S+)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Handle fully qualified type names - if (!type.Contains(".")) - { - type = "System." + type; - } - builder.AppendLine($"/// "); - } - } - break; + // Start new method + currentMethod = line.Substring(4).Trim(); + contentBuilder.Clear(); + currentMethodDescription = ""; + continue; + } + + if (line.StartsWith("### ") && inPropertySection) + { + // Process previous property if exists + if (!string.IsNullOrEmpty(currentProperty)) + { + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); } - } - private static void ProcessMethodSection(StringBuilder builder, string methodName, string description, string content, Dictionary methodSignatures) - { - if (string.IsNullOrEmpty(methodName)) - return; + // Start new property + currentProperty = line.Substring(4).Trim(); + contentBuilder.Clear(); + currentPropertyDescription = ""; + continue; + } + + // If we haven't hit any section yet, this is the class summary + if (string.IsNullOrEmpty(currentSection) && !string.IsNullOrWhiteSpace(line)) + { + classBuilder.AppendLine("/// "); + classBuilder.AppendLine($"/// {line.Trim()}"); + classBuilder.AppendLine("/// "); + continue; + } + + // Capture method description (text before any #### subsections) + if (inMethodSection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentMethodDescription)) + { + currentMethodDescription = line.Trim(); + continue; + } + + // Capture property description (text before any #### subsections) + if (inPropertySection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentPropertyDescription)) + { + currentPropertyDescription = line.Trim(); + continue; + } + + // Add content to current section + if (!string.IsNullOrWhiteSpace(line)) + { + contentBuilder.AppendLine(line); + } + } - // Handle constructor specially - if (methodName == "Constructor") - methodName = methodSignatures.Keys.FirstOrDefault(k => k.Contains("ctor")) ?? ""; + // Process the last section + if (inMethodSection) + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + else if (inPropertySection) + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + else + ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); - if (!methodSignatures.ContainsKey(methodName)) - return; + return (classBuilder.ToString(), methodBuilder.ToString(), propertyBuilder.ToString()); + } - builder.AppendLine($" // Documentation for {methodName}"); + private static void ProcessSection(StringBuilder builder, string section, string content) + { + if (string.IsNullOrEmpty(content)) + return; - // Add method summary if available - if (!string.IsNullOrEmpty(description)) + switch (section) + { + case "Remarks": + builder.AppendLine("/// "); + foreach (var line in content.Split('\n')) { - builder.AppendLine(" /// "); - builder.AppendLine($" /// {description}"); - builder.AppendLine(" /// "); + builder.AppendLine($"/// {line.Trim()}"); } + builder.AppendLine("/// "); + break; - var reader = new StringReader(content); - string? line; - var currentSubSection = ""; - var subSectionContent = new StringBuilder(); - - while ((line = reader.ReadLine()) != null) + case "See Also": + foreach (var line in content.Split('\n')) { - if (line.StartsWith("#### ")) - { - // Process previous subsection - ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"\[(.*?)\]\((.*?)\)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Convert angle brackets to curly braces for XML documentation + var xmlType = type.Replace('<', '{').Replace('>', '}'); + builder.AppendLine($"/// "); + } + } + break; - // Start new subsection - currentSubSection = line.Substring(5).Trim(); - subSectionContent.Clear(); - continue; - } + case "Inheritance": + if (content.Contains("@")) + { + var match = Regex.Match(content, @"@(\S+)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Extract the type name and keep T as the generic parameter + var baseType = type.Split('<')[0]; + builder.AppendLine($"/// "); + } + } + break; - // Add content to current subsection - if (!string.IsNullOrWhiteSpace(line)) + case "References": + foreach (var line in content.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"@(\S+)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Handle fully qualified type names + if (!type.Contains(".")) { - subSectionContent.AppendLine(line); + type = "System." + type; } + builder.AppendLine($"/// "); + } } + break; + } + } - // Process the last subsection - ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + private static void ProcessMethodSection(StringBuilder builder, string methodName, string description, string content, Dictionary methodSignatures) + { + if (string.IsNullOrEmpty(methodName)) + return; - // Add the method signature with a semicolon - builder.AppendLine($" {methodSignatures[methodName]};"); - builder.AppendLine(); - } + // Handle constructor specially + if (methodName == "Constructor") + methodName = methodSignatures.Keys.FirstOrDefault(k => k.Contains("ctor")) ?? ""; - private static void ProcessMethodSubSection(StringBuilder builder, string subSection, string content) + if (!methodSignatures.ContainsKey(methodName)) + return; + + builder.AppendLine($" // Documentation for {methodName}"); + + // Add method summary if available + if (!string.IsNullOrEmpty(description)) { - if (string.IsNullOrEmpty(content)) - return; + builder.AppendLine(" /// "); + builder.AppendLine($" /// {description}"); + builder.AppendLine(" /// "); + } - switch (subSection) - { - case "Parameters": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); - if (match.Success) - { - builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); - } - } - break; + var reader = new StringReader(content); + string? line; + var currentSubSection = ""; + var subSectionContent = new StringBuilder(); - case "Returns": - builder.AppendLine(" /// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($" /// {line.Trim()}"); - } - builder.AppendLine(" /// "); - break; + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("#### ")) + { + // Process previous subsection + ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - case "Exceptions": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); - if (match.Success) - { - builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); - } - } - break; - } + // Start new subsection + currentSubSection = line.Substring(5).Trim(); + subSectionContent.Clear(); + continue; + } + + // Add content to current subsection + if (!string.IsNullOrWhiteSpace(line)) + { + subSectionContent.AppendLine(line); + } } - private static void ProcessPropertySection(StringBuilder builder, string propertyName, string description, string content, Dictionary propertySignatures) - { - if (string.IsNullOrEmpty(propertyName)) - return; + // Process the last subsection + ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - if (!propertySignatures.ContainsKey(propertyName)) - return; + // Add the method signature with a semicolon + builder.AppendLine($" {methodSignatures[methodName]};"); + builder.AppendLine(); + } - builder.AppendLine($" // Documentation for {propertyName}"); + private static void ProcessMethodSubSection(StringBuilder builder, string subSection, string content) + { + if (string.IsNullOrEmpty(content)) + return; - // Add property summary if available - if (!string.IsNullOrEmpty(description)) + switch (subSection) + { + case "Parameters": + foreach (var line in content.Split('\n')) { - builder.AppendLine(" /// "); - builder.AppendLine($" /// {description}"); - builder.AppendLine(" /// "); + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); + if (match.Success) + { + builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); + } } + break; - var reader = new StringReader(content); - string? line; - var currentSubSection = ""; - var subSectionContent = new StringBuilder(); + case "Returns": + builder.AppendLine(" /// "); + foreach (var line in content.Split('\n')) + { + builder.AppendLine($" /// {line.Trim()}"); + } + builder.AppendLine(" /// "); + break; - while ((line = reader.ReadLine()) != null) + case "Exceptions": + foreach (var line in content.Split('\n')) { - if (line.StartsWith("#### ")) - { - // Process previous subsection - ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); + if (match.Success) + { + builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); + } + } + break; + } + } - // Start new subsection - currentSubSection = line.Substring(5).Trim(); - subSectionContent.Clear(); - continue; - } + private static void ProcessPropertySection(StringBuilder builder, string propertyName, string description, string content, Dictionary propertySignatures) + { + if (string.IsNullOrEmpty(propertyName)) + return; - // Add content to current subsection - if (!string.IsNullOrWhiteSpace(line)) - { - subSectionContent.AppendLine(line); - } - } + if (!propertySignatures.ContainsKey(propertyName)) + return; - // Process the last subsection - ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + builder.AppendLine($" // Documentation for {propertyName}"); - // Add the property signature - builder.AppendLine($" {propertySignatures[propertyName]}"); - builder.AppendLine(); + // Add property summary if available + if (!string.IsNullOrEmpty(description)) + { + builder.AppendLine(" /// "); + builder.AppendLine($" /// {description}"); + builder.AppendLine(" /// "); } - private static void ProcessPropertySubSection(StringBuilder builder, string subSection, string content) + var reader = new StringReader(content); + string? line; + var currentSubSection = ""; + var subSectionContent = new StringBuilder(); + + while ((line = reader.ReadLine()) != null) { - if (string.IsNullOrEmpty(content)) - return; + if (line.StartsWith("#### ")) + { + // Process previous subsection + ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - switch (subSection) - { - case "Value": - builder.AppendLine(" /// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($" /// {line.Trim()}"); - } - builder.AppendLine(" /// "); - break; - } + // Start new subsection + currentSubSection = line.Substring(5).Trim(); + subSectionContent.Clear(); + continue; + } + + // Add content to current subsection + if (!string.IsNullOrWhiteSpace(line)) + { + subSectionContent.AppendLine(line); + } } - private static string ConvertToKebabCase(string pascalCase) - { - if (string.IsNullOrEmpty(pascalCase)) - return pascalCase; - - // Insert hyphens before uppercase letters (except the first character) - var kebabCase = Regex.Replace(pascalCase, "(?"); + foreach (var line in content.Split('\n')) + { + builder.AppendLine($" /// {line.Trim()}"); + } + builder.AppendLine(" /// "); + break; } + } + + private static string ConvertToKebabCase(string pascalCase) + { + if (string.IsNullOrEmpty(pascalCase)) + return pascalCase; + + // Insert hyphens before uppercase letters (except the first character) + var kebabCase = Regex.Replace(pascalCase, "(? SupportedDiagnostics => ImmutableArray.Create(Rule); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); - public override void Initialize(AnalysisContext context) - { - // This already excludes generated code from analysis - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); + public override void Initialize(AnalysisContext context) + { + // This already excludes generated code from analysis + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); - // Register for class declarations - context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); - context.RegisterSyntaxNodeAction(AnalyzeInterfaceDeclaration, SyntaxKind.InterfaceDeclaration); - } + // Register for class declarations + context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeInterfaceDeclaration, SyntaxKind.InterfaceDeclaration); + } - private void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) - { - var classDeclaration = (ClassDeclarationSyntax)context.Node; - AnalyzeTypeDeclaration(context, classDeclaration, classDeclaration.Identifier); - } + private void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + AnalyzeTypeDeclaration(context, classDeclaration, classDeclaration.Identifier); + } - private void AnalyzeInterfaceDeclaration(SyntaxNodeAnalysisContext context) - { - var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node; - AnalyzeTypeDeclaration(context, interfaceDeclaration, interfaceDeclaration.Identifier); - } + private void AnalyzeInterfaceDeclaration(SyntaxNodeAnalysisContext context) + { + var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node; + AnalyzeTypeDeclaration(context, interfaceDeclaration, interfaceDeclaration.Identifier); + } - private void AnalyzeTypeDeclaration(SyntaxNodeAnalysisContext context, TypeDeclarationSyntax typeDeclaration, SyntaxToken identifier) - { - // Check if the type has XML documentation - var trivia = typeDeclaration.GetLeadingTrivia(); - var hasXmlDocs = trivia.Any(t => t.HasStructure && t.GetStructure() is DocumentationCommentTriviaSyntax); + private void AnalyzeTypeDeclaration(SyntaxNodeAnalysisContext context, TypeDeclarationSyntax typeDeclaration, SyntaxToken identifier) + { + // Check if the type has XML documentation + SyntaxTriviaList trivia = typeDeclaration.GetLeadingTrivia(); + bool hasXmlDocs = trivia.Any(t => t.HasStructure && t.GetStructure() is DocumentationCommentTriviaSyntax); - if (!hasXmlDocs) + if (!hasXmlDocs) + { + // Also check members for XML documentation + foreach (MemberDeclarationSyntax member in typeDeclaration.Members) + { + SyntaxTriviaList memberTrivia = member.GetLeadingTrivia(); + if (memberTrivia.Any(t => t.HasStructure && t.GetStructure() is DocumentationCommentTriviaSyntax)) { - // Also check members for XML documentation - foreach (var member in typeDeclaration.Members) - { - var memberTrivia = member.GetLeadingTrivia(); - if (memberTrivia.Any(t => t.HasStructure && t.GetStructure() is DocumentationCommentTriviaSyntax)) - { - hasXmlDocs = true; - break; - } - } + hasXmlDocs = true; + break; } + } + } - if (hasXmlDocs) - { - // Report diagnostic on the type name - var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Text); - context.ReportDiagnostic(diagnostic); - } + if (hasXmlDocs) + { + // Report diagnostic on the type name + var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Text); + context.ReportDiagnostic(diagnostic); } + } } \ No newline at end of file