using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Reflection.Metadata.Ecma335; using System.Text; using System.Xml; using System.Xml.Linq; namespace Telegrator.Generators { /// /// Source Generator для автоматической генерации Markdown-документации по публичному API Telegrator. /// [Generator] public class ApiMarkdownGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValueProvider> typeDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: (node, _) => node is ClassDeclarationSyntax || node is InterfaceDeclarationSyntax || node is StructDeclarationSyntax || node is EnumDeclarationSyntax, transform: (ctx, _) => (BaseTypeDeclarationSyntax)ctx.Node ) .Where(typeDecl => typeDecl != null) .Collect(); var combined = typeDeclarations.Combine(context.CompilationProvider); context.RegisterSourceOutput(combined, (spc, source) => { IReadOnlyList typeDecls = source.Left; Compilation compilation = source.Right; string markdown = GenerateMarkdown(typeDecls, compilation); spc.AddSource("TelegramReactive_Api.md", markdown); }); } private string GenerateMarkdown(IReadOnlyList typeDecls, Compilation compilation) { StringBuilder sourceBuilder = new StringBuilder("/*\n"); // Writing caution message sourceBuilder.AppendLine("> [!CAUTION]"); sourceBuilder.AppendLine("> This page was generated using hand-writen compiler's XML-Summaries output parser"); sourceBuilder.AppendLine("> If you find any missing syntax, errors in structure, mis-formats or empty sections, please contact owner or open an issue\n"); // Collecting only public types IEnumerable publicTypes = typeDecls .Select(type => type.TryGetNamedType(compilation)) .Where(symbol => symbol != null) .Where(symbol => symbol.DeclaredAccessibility == Accessibility.Public); // Grouping by namespace IOrderedEnumerable> namespaces = publicTypes .GroupBy(t => t.ContainingNamespace.ToDisplayString()) .OrderBy(g => g.Key); foreach (IGrouping nsGroup in namespaces) { string ns = nsGroup.Key == "Telegrator" ? nsGroup.Key : nsGroup.Key.Substring("Telegrator.".Length); sourceBuilder.AppendFormat("# {0}\n\n", ns); foreach (INamedTypeSymbol type in nsGroup.OrderBy(t => t.Name)) { // Формируем generic-параметры для заголовка класса string genericArgs = type.FormatGenericTypes(); sourceBuilder.AppendFormat("## {0} `{1}{2}`\n\n", type.TypeKind, type.Name, genericArgs); string? typeSummary = type.ExtractSummary(); if (!string.IsNullOrWhiteSpace(typeSummary)) sourceBuilder.AppendFormat("> {0}\n\n", typeSummary); // Writing members if (type.TypeKind == TypeKind.Enum) { WriteEnumValues(sourceBuilder, type); } else { WriteConstructors(sourceBuilder, type); WriteProperties(sourceBuilder, type); WriteMethods(sourceBuilder, type); } } } return sourceBuilder.AppendLine().AppendLine("*/").ToString(); } private static void WriteConstructors(StringBuilder sourceBuilder, INamedTypeSymbol type) { if (type.TypeKind == TypeKind.Enum) return; // Getting ctors List ctors = type.Constructors .Where(c => c.DeclaredAccessibility == Accessibility.Public) .ToList(); // Checking for any if (ctors.Count == 0) return; // Writing sourceBuilder.AppendLine("**Constructors:**"); foreach (IMethodSymbol ctor in ctors) { // Формируем строку вида ClassName(Param1, Param2) с generic-аргументами string genericArgs = type.FormatGenericTypes(); string parameters = string.Join(", ", ctor.Parameters.Select(p => p.Type.GetShortName())); string signature = string.Format("{0}{1}({2})", type.Name, genericArgs, parameters); sourceBuilder.Append(" - `").Append(signature).Append("`").AppendLine(); // Writing summary string? propSummary = ctor.ExtractSummary(); if (!string.IsNullOrWhiteSpace(propSummary)) sourceBuilder.Append(" > ").Append(propSummary).AppendLine(); } sourceBuilder.AppendLine(); } private static void WriteEnumValues(StringBuilder sourceBuilder, INamedTypeSymbol type) { var members = type.GetMembers().OfType().Where(f => f.HasConstantValue && f.DeclaredAccessibility == Accessibility.Public).ToList(); if (members.Count == 0) return; sourceBuilder.AppendLine("**Values:**"); foreach (IFieldSymbol field in members) { sourceBuilder.Append("- `").Append(field.Name).Append("`"); string? summary = field.ExtractSummary(); if (!string.IsNullOrWhiteSpace(summary)) sourceBuilder.Append(" — ").Append(summary); sourceBuilder.AppendLine(); } sourceBuilder.AppendLine(); } private static void WriteProperties(StringBuilder sourceBuilder, INamedTypeSymbol type) { // Getting properties List props = type .GetMembers() .OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public) .ToList(); // Checking for any if (props.Count == 0) return; // Writing sourceBuilder.AppendLine("**Properties:**"); foreach (IPropertySymbol prop in props) { // Writing member sourceBuilder.AppendFormat("- `{0}`\n", prop.Name); // Writing summary string? propSummary = prop.ExtractSummary(); if (!string.IsNullOrWhiteSpace(propSummary)) sourceBuilder.AppendFormat(" > {0}", propSummary); sourceBuilder.AppendLine(); } sourceBuilder.AppendLine(); } private static void WriteMethods(StringBuilder sourceBuilder, INamedTypeSymbol type) { // Getting methods List methods = type .GetMembers() .OfType() .Where(m => m.DeclaredAccessibility == Accessibility.Public) .Where(m => m.MethodKind == MethodKind.Ordinary) .ToList(); // Checking for any if (methods.Count == 0) return; // Writing sourceBuilder.AppendLine("**Methods:**"); foreach (IMethodSymbol method in methods) { // Формируем generic-параметры для метода string genericArgs = method.FormatGenericTypes(); string parameters = string.Join(", ", method.Parameters.Select(p => p.Type.GetShortName())); sourceBuilder.AppendFormat(" - `{0}{1}({2})`\n", method.Name, genericArgs, parameters); // Writing summary string? propSummary = method.ExtractSummary(); if (!string.IsNullOrWhiteSpace(propSummary)) sourceBuilder.AppendFormat(" > {0}", propSummary); sourceBuilder.AppendLine(); } sourceBuilder.AppendLine(); } } internal static partial class TypesExtensions { public static string? ExtractSummary(this ISymbol symbol) { string? xmlDoc = symbol.GetDocumentationCommentXml(); if (string.IsNullOrWhiteSpace(xmlDoc)) return null; try { XDocument doc = XDocument.Parse(xmlDoc); XElement? summary = doc.Root?.Element("summary"); if (summary == null) return null; // Убираем лишние пробелы и переносы строк return summary.Value.Trim().Replace("\n", " ").Replace(" ", " "); } catch { // Игнорируем ошибки парсинга XML return null; } } public static string FormatGenericTypes(this INamedTypeSymbol methodSymbol) { if (methodSymbol.TypeParameters.Length == 0) return string.Empty; string typeParams = string.Join(", ", methodSymbol.TypeParameters.Select(tp => tp.Name)); return string.Format("<{0}>", typeParams); } public static string FormatGenericTypes(this IMethodSymbol methodSymbol) { if (methodSymbol.TypeParameters.Length == 0) return string.Empty; string typeParams = string.Join(", ", methodSymbol.TypeParameters.Select(tp => tp.Name)); return string.Format("<{0}>", typeParams); } public static string GetShortName(this ITypeSymbol type) { if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { string genericArgs = string.Join(", ", namedType.TypeArguments.Select(GetShortName)); return string.Format("{0}<{1}>", namedType.Name, genericArgs); } if (type.TypeKind == TypeKind.TypeParameter) { return type.Name; } return type.Name; } } }