Добавьте файлы проекта.

This commit is contained in:
2025-07-24 23:19:59 +04:00
commit 33d1f6218a
168 changed files with 15035 additions and 0 deletions
@@ -0,0 +1,267 @@
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
{
/// <summary>
/// Source Generator для автоматической генерации Markdown-документации по публичному API Telegrator.
/// </summary>
[Generator]
public class ApiMarkdownGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<BaseTypeDeclarationSyntax>> 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<BaseTypeDeclarationSyntax> typeDecls = source.Left;
Compilation compilation = source.Right;
string markdown = GenerateMarkdown(typeDecls, compilation);
spc.AddSource("TelegramReactive_Api.md", markdown);
});
}
private string GenerateMarkdown(IReadOnlyList<BaseTypeDeclarationSyntax> 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<INamedTypeSymbol> publicTypes = typeDecls
.Select(type => type.TryGetNamedType(compilation))
.Where(symbol => symbol != null)
.Where(symbol => symbol.DeclaredAccessibility == Accessibility.Public);
// Grouping by namespace
IOrderedEnumerable<IGrouping<string, INamedTypeSymbol>> namespaces = publicTypes
.GroupBy(t => t.ContainingNamespace.ToDisplayString())
.OrderBy(g => g.Key);
foreach (IGrouping<string, INamedTypeSymbol> 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<IMethodSymbol> 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<Type1, T>(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<IFieldSymbol>().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<IPropertySymbol> props = type
.GetMembers()
.OfType<IPropertySymbol>()
.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<IMethodSymbol> methods = type
.GetMembers()
.OfType<IMethodSymbol>()
.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;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace Telegrator.Generators
{
/// <summary>
/// Exception thrown when a target is not found during code generation.
/// </summary>
internal class TargteterNotFoundException() : Exception() { }
/// <summary>
/// Exception thrown when a base class type is not found during code generation.
/// </summary>
internal class BaseClassTypeNotFoundException() : Exception() { }
}
@@ -0,0 +1,209 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
namespace Telegrator.Generators
{
[Generator(LanguageNames.CSharp)]
public class ImplicitHandlerBuilderExtensionsGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if ANALYZERSDEBUG
Debugger.Launch();
#endif
IncrementalValueProvider<ImmutableArray<ClassDeclarationSyntax>> pipeline = context.SyntaxProvider
.CreateSyntaxProvider(SyntaxPredicate, SyntaxTransform)
.Where(declaration => declaration != null)
.Collect();
context.RegisterImplementationSourceOutput(pipeline, GenerateSource);
}
private static bool SyntaxPredicate(SyntaxNode node, CancellationToken _)
{
if (node is not ClassDeclarationSyntax)
return false;
return true;
}
private static ClassDeclarationSyntax SyntaxTransform(GeneratorSyntaxContext context, CancellationToken _)
{
ISymbol? symbol = context.SemanticModel.GetDeclaredSymbol(context.Node);
if (symbol is null)
return null!;
if (symbol is not ITypeSymbol typeSymbol)
return null!;
if (!typeSymbol.IsAssignableFrom("UpdateFilterAttribute"))
return null!;
return (ClassDeclarationSyntax)context.Node;
}
private static void GenerateSource(SourceProductionContext context, ImmutableArray<ClassDeclarationSyntax> declarations)
{
StringBuilder source = new StringBuilder();
Dictionary<string, string> targeters = [];
List<string> usingDirectives =
[
"using Telegrator.Handlers.Building;",
"using Telegrator.Handlers.Building.Components;"
];
StringBuilder sourceBuilder = new StringBuilder()
.AppendLine("namespace Telegrator")
.AppendLine("{")
.Append("\t//").Append(string.Join(", ", declarations.Select(decl => decl.Identifier.ToString()))).AppendLine()
.AppendLine("\tpublic static partial class HandlerBuilderExtensions")
.AppendLine("\t{");
List<ClassDeclarationSyntax> lateTargeterClasses = [];
foreach (ClassDeclarationSyntax classDeclaration in declarations)
{
try
{
usingDirectives.UnionAdd(classDeclaration.FindCompilationUnitSyntax().Usings.Select(use => use.ToString()));
ParseClassDeclaration(sourceBuilder, classDeclaration, targeters);
}
catch (TargteterNotFoundException)
{
lateTargeterClasses.Add(classDeclaration);
}
catch (Exception exc)
{
string errorFormat = string.Format("\t\t// failed to generate for {0} : {1}", classDeclaration.Identifier.ToString(), exc.GetType().Name);
sourceBuilder.AppendLine(errorFormat);
}
}
foreach (ClassDeclarationSyntax classDeclaration in lateTargeterClasses)
{
try
{
usingDirectives.UnionAdd(classDeclaration.FindCompilationUnitSyntax().Usings.Select(use => use.ToString()));
ParseClassDeclaration(sourceBuilder, classDeclaration, targeters);
}
catch (Exception exc)
{
string errorFormat = string.Format("\t\t// failed to generate for {0} : {1}", classDeclaration.Identifier.ToString(), exc.GetType().Name);
sourceBuilder.AppendLine(errorFormat);
}
}
sourceBuilder.AppendLine("\t}\n}");
sourceBuilder.Insert(0, string.Join("\n", usingDirectives.Select(use => use.ToString()).OrderBy(use => use)) + "\n\n");
context.AddSource("GeneratedHandlerBuilderExtensions.cs", sourceBuilder.ToString());
}
private static void ParseClassDeclaration(StringBuilder sourceBuilder, ClassDeclarationSyntax classDeclaration, Dictionary<string, string> targeters)
{
IEnumerable<MethodDeclarationSyntax> methods = classDeclaration.Members.OfType<MethodDeclarationSyntax>();
MethodDeclarationSyntax? targeterMethod = methods.FirstOrDefault(method => method.Identifier.ToString() == "GetFilterringTarget");
string className = classDeclaration.Identifier.ToString();
string filterName = className.Replace("Attribute", string.Empty);
string classTargetterMethodName = filterName + "_GetFilterringTarget";
if (targeterMethod != null)
{
targeters.Add(className, classTargetterMethodName);
RenderTargeterMethod(sourceBuilder, classTargetterMethodName, targeterMethod);
sourceBuilder.AppendLine();
}
else
{
if (classDeclaration.BaseList == null)
throw new Exception();
string baseClassName = classDeclaration.BaseList.Types
.ElementAt(0).GetBaseTypeSyntaxName();
if (!targeters.ContainsKey(baseClassName))
throw new TargteterNotFoundException();
classTargetterMethodName = targeters[baseClassName];
}
if (classDeclaration.Modifiers.Any(keyword => keyword.ValueText == "abstract"))
return;
if (classDeclaration.ParameterList != null)
{
if (classDeclaration.BaseList != null)
{
PrimaryConstructorBaseTypeSyntax primaryConstructor = (PrimaryConstructorBaseTypeSyntax)classDeclaration.BaseList.Types.ElementAt(0);
RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, classDeclaration.ParameterList.Parameters, primaryConstructor.ArgumentList.Arguments);
}
else
{
RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, classDeclaration.ParameterList.Parameters, []);
}
sourceBuilder.AppendLine();
}
foreach (ConstructorDeclarationSyntax constructor in classDeclaration.Members.OfType<ConstructorDeclarationSyntax>())
{
if (constructor.Initializer == null)
continue;
RenderExtensionMethod(sourceBuilder, filterName, classTargetterMethodName, constructor.ParameterList.Parameters, constructor.Initializer.ArgumentList.Arguments);
sourceBuilder.AppendLine();
}
}
private static void RenderExtensionMethod(StringBuilder sourceBuilder, string filterName, string classTargetterMethodName, SeparatedSyntaxList<ParameterSyntax> parameters, SeparatedSyntaxList<ArgumentSyntax> arguments)
{
if (filterName == "ChatType")
filterName = "InChatType"; // Because it conflicting
sourceBuilder
.Append("\t\t/// <summary>").AppendLine()
.Append("\t\t/// Adds ").Append(filterName).Append(" filter to implicit handler").AppendLine()
.Append("\t\t/// </summary>").AppendLine();
sourceBuilder.Append("\t\tpublic static TBuilder ").Append(filterName).Append("<TBuilder>(this TBuilder builder");
if (parameters.Any())
sourceBuilder.Append(", ").Append(parameters.ToFullString());
sourceBuilder
.Append(") where TBuilder : IHandlerBuilder").AppendLine()
.Append("\t\t{").AppendLine()
.Append("\t\t\tbuilder.AddTargetedFilter");
if (arguments.Count > 1)
sourceBuilder.Append("s");
sourceBuilder.Append("(").Append(classTargetterMethodName);
if (arguments.Any())
sourceBuilder.Append(", ").Append(arguments.ToFullString());
sourceBuilder
.Append(");").AppendLine()
.Append("\t\t\treturn builder;").AppendLine()
.Append("\t\t}").AppendLine();
}
private static void RenderTargeterMethod(StringBuilder sourceBuilder, string classTargetterMethodName, MethodDeclarationSyntax targeterMethod)
{
sourceBuilder.Append("\t\tprivate static ").Append(targeterMethod.ReturnType.ToString()).Append(" ").Append(classTargetterMethodName).Append(targeterMethod.ParameterList.ToFullString());
if (targeterMethod.ExpressionBody != null)
{
sourceBuilder.Append(targeterMethod.ExpressionBody.ToFullString()).Append(";").AppendLine();
}
else if (targeterMethod.Body != null)
{
sourceBuilder.Append(targeterMethod.Body.ToFullString());
}
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release;AnalyzersDebug</Configurations>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
</ItemGroup>
</Project>
+67
View File
@@ -0,0 +1,67 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Telegrator.Generators
{
internal static partial class TypeExtensions
{
public static INamedTypeSymbol TryGetNamedType(this BaseTypeDeclarationSyntax syntax, Compilation compilation)
{
SemanticModel semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
return semanticModel.GetDeclaredSymbol(syntax)!;
}
public static string GetBaseTypeSyntaxName(this BaseTypeSyntax baseClassSyntax)
{
if (baseClassSyntax is PrimaryConstructorBaseTypeSyntax parimaryConstructor)
return parimaryConstructor.Type.ToString();
if (baseClassSyntax is SimpleBaseTypeSyntax simpleBaseType)
return simpleBaseType.Type.ToString();
throw new BaseClassTypeNotFoundException();
}
public static bool IsAssignableFrom(this ITypeSymbol symbol, string className)
{
if (symbol.BaseType == null)
return false;
if (symbol.BaseType.Name == className)
return true;
return symbol.BaseType.IsAssignableFrom(className);
}
public static ITypeSymbol? Cast(this ITypeSymbol symbol, string className)
{
if (symbol.BaseType == null)
return null;
if (symbol.BaseType.Name == className)
return symbol.BaseType;
return symbol.BaseType.Cast(className);
}
public static CompilationUnitSyntax FindCompilationUnitSyntax(this SyntaxNode syntax)
{
while (syntax is not CompilationUnitSyntax)
syntax = syntax.Parent ?? throw new Exception();
return (CompilationUnitSyntax)syntax;
}
public static IList<TValue> UnionAdd<TValue>(this IList<TValue> source, IEnumerable<TValue> toUnion)
{
foreach (TValue toUnionValue in toUnion)
{
if (!source.Contains(toUnionValue, EqualityComparer<TValue>.Default))
source.Add(toUnionValue);
}
return source;
}
}
}