From c43a1cdfa22d2b97f7e4e3bd24b20164cb48345e Mon Sep 17 00:00:00 2001 From: Rikitav Date: Fri, 6 Mar 2026 20:01:54 +0400 Subject: [PATCH] * Refactored analyzers * Added warnings reporting for invalid handler declaration (Hardcoded names) --- .../AnalyzerReleases.Unshipped.md | 17 +- .../DeveloperHelperAnalyzer.cs | 275 ++++-- .../GeneratedKeyboardMarkupGenerator.cs | 927 +++++++++--------- Telegrator.Analyzers/Models.cs | 12 - .../Descriptors/DescribedHandlerDescriptor.cs | 2 +- Telegrator/Polling/UpdateHandlersPool.cs | 8 +- Telegrator/Polling/UpdateRouter.cs | 2 + 7 files changed, 675 insertions(+), 568 deletions(-) delete mode 100644 Telegrator.Analyzers/Models.cs diff --git a/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md index addebe4..3dd91af 100644 --- a/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md +++ b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md @@ -5,10 +5,13 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -TG_1001 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1002 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1003 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1004 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1005 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1006 | Modelling | Error | GeneratedKeyboardMarkupGenerator -TG_1007 | Modelling | Error | GeneratedKeyboardMarkupGenerator \ No newline at end of file +TLG201 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG202 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG203 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG204 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG205 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG206 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG207 | Telegrator.Modelling | Error | GeneratedKeyboardMarkupGenerator +TLG101 | Telegrator.Design | Warning | DeveloperHelperAnalyzer +TLG102 | Telegrator.Design | Warning | DeveloperHelperAnalyzer +TLG103 | Telegrator.Design | Warning | DeveloperHelperAnalyzer \ No newline at end of file diff --git a/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs b/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs index f9cfbc0..4e84bcc 100644 --- a/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs +++ b/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs @@ -1,117 +1,200 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using System.Collections.Immutable; using System.Text; -using Telegrator.Analyzers.RoslynExtensions; -namespace Telegrator.Analyzers +namespace Telegrator.Analyzers; + +[Generator(LanguageNames.CSharp)] +public class DeveloperHelperAnalyzer : IIncrementalGenerator { - [Generator(LanguageNames.CSharp)] - public class DeveloperHelperAnalyzer : IIncrementalGenerator + private static readonly DiagnosticDescriptor MissingBaseClassWarning = new( + id: "TLG101", + title: "Missing handlers base class", + messageFormat: "Class '{0}' has attribute [{1}], but doesn't inherits {1}", + category: "Telegrator.Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MissingAttributeWarning = new( + id: "TLG102", + title: "Missing handler annotation", + messageFormat: "Class '{0}' inherits '{1}', but doesn't have required annotation [{1}]", + category: "Telegrator.Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MismatchedHandlerWarning = new( + id: "TLG103", + title: "Handlers Annotation and BaseClass mismatch", + messageFormat: "Class '{0}' has attribute [{1}], but inherits '{2}'", + category: "Telegrator.Design", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) + IncrementalValueProvider> pipeline = context.SyntaxProvider + .CreateSyntaxProvider(Provide, Transform) + .Where(handler => handler != null) + .Collect(); + + context.RegisterSourceOutput(pipeline, Execute); + } + + private static bool Provide(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (syntaxNode is not ClassDeclarationSyntax classSyntax) + return false; + + if (classSyntax.BaseList?.Types.Count == 0 && classSyntax.AttributeLists.Count == 0) + return false; + + return true; + } + + private static HandlerDeclarationModel Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)context.Node; + string? foundAttribute = classSyntax.GetHandlerAttributeName(); + string? foundBaseClass = classSyntax.GetHandlerBaseClassName(); + + if (foundAttribute == null && foundBaseClass == null) + return null!; + + string namespaceName = classSyntax.GetNamespace(); + return new HandlerDeclarationModel( + classSyntax.Identifier.Text, + namespaceName, + foundAttribute, + foundBaseClass, + classSyntax.Identifier.GetLocation() + ); + } + + private static void Execute(SourceProductionContext context, ImmutableArray handlers) + { + if (handlers.IsDefaultOrEmpty) + return; + + List members = []; + foreach (HandlerDeclarationModel handler in handlers) { - IncrementalValueProvider> pipeline = context.SyntaxProvider - .CreateSyntaxProvider(Provide, Transform) - .Where(handler => handler != null) - .Collect(); + context.CancellationToken.ThrowIfCancellationRequested(); - context.RegisterSourceOutput(pipeline, Execute); - } - - private static bool Provide(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if (syntaxNode is not ClassDeclarationSyntax classSyntax) - return false; - - if (classSyntax.BaseList?.Types.Count == 0 && classSyntax.AttributeLists.SelectMany(list => list.Attributes).Count() == 0) - return false; - - return true; - } - - private static HandlerDeclarationModel Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken) - { - ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)context.Node; - IEnumerable attributes = []; //classSyntax.GetHandlerAttributes(); - BaseTypeSyntax? baseType = classSyntax.GetHandlerBaseClass(); - - if (baseType == null && !attributes.Any()) - return null!; - - return new HandlerDeclarationModel(classSyntax, attributes, baseType); - } - - private static void Execute(SourceProductionContext context, ImmutableArray handlers) - { - StringBuilder sourceBuilder = new StringBuilder(); - List usingDirectives = []; - - sourceBuilder - .AppendTabs(0).Append("namespace Telegrator.Analyzers").AppendLine() - .AppendTabs(0).Append("{").AppendLine() - .AppendTabs(1).Append("public static partial class AnalyzerExport").AppendLine() - .AppendTabs(1).Append("{").AppendLine(); - - foreach (HandlerDeclarationModel handler in handlers) + if (handler.AttributeName != null && handler.BaseClassName == null) { - context.CancellationToken.ThrowIfCancellationRequested(); - try - { - usingDirectives.UnionAdd(handler.ClassDeclaration.FindAncestor().Usings.Select(use => use.ToString())); - ParseHandlerDeclaration(context, sourceBuilder, handler, context.CancellationToken); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - sourceBuilder.AppendLine() - .Append("// Failed to parse ").Append(handler.ClassDeclaration.Identifier.ToString()).AppendLine() - .Append(ex).AppendLine(); - } + context.ReportDiagnostic(Diagnostic.Create(MissingBaseClassWarning, handler.Location, handler.ClassName, handler.AttributeName)); + continue; } - sourceBuilder.AppendLine("\t}\n}"); - sourceBuilder.Insert(0, string.Join("\n", usingDirectives.OrderBy(use => use)) + "\n\n"); - context.AddSource("DeveloperHelperAnalyzer.cs", sourceBuilder.ToString()); + if (handler.AttributeName == null && handler.BaseClassName != null) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAttributeWarning, handler.Location, handler.ClassName, handler.BaseClassName)); + continue; + } + + if (handler.AttributeName != handler.BaseClassName) + { + context.ReportDiagnostic(Diagnostic.Create(MismatchedHandlerWarning, handler.Location, handler.ClassName, handler.AttributeName, handler.BaseClassName)); + continue; + } + + FieldDeclarationSyntax fieldDeclaration = GenerateTypeField(handler); + members.Add(fieldDeclaration); } - private static void ParseHandlerDeclaration(SourceProductionContext context, StringBuilder sourceBuilder, HandlerDeclarationModel handler, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.Append("//").Append(handler.ClassDeclaration.Identifier.ToString()).AppendLine(); - //context.ReportDiagnostic(DiagnosticsHelper.Test.Create(handler.ClassDeclaration.Identifier.GetLocation())); - } + if (members.Count == 0) + return; + + // 4. Сборка итогового файла + ClassDeclarationSyntax classDeclaration = SyntaxFactory.ClassDeclaration("AnalyzerExport") + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.PartialKeyword))) + .WithMembers(SyntaxFactory.List(members)); + + NamespaceDeclarationSyntax namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName("Telegrator.Analyzers")) + .WithMembers(SyntaxFactory.SingletonList(classDeclaration)); + + CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit() + .WithMembers(SyntaxFactory.SingletonList(namespaceDeclaration)) + .NormalizeWhitespace(); + + SourceText sourceText = SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8); + context.AddSource("AnalyzerExport.g.cs", sourceText); } - internal static class DeveloperHelperAnalyzerExtensions + private static FieldDeclarationSyntax GenerateTypeField(HandlerDeclarationModel handler) { - private static readonly string[] AttributeNames = - [ - "AnyUpdateHandlerAttribute", - "CallbackQueryHandlerAttribute", - "CommandHandlerAttribute", - "WelcomeHandlerAttribute", - "MessageHandlerAttribute" - ]; + string fullTypeName = handler.Namespace == "Global" + ? handler.ClassName + : $"{handler.Namespace}.{handler.ClassName}"; - private static readonly string[] HandlersNames = - [ - "AnyUpdateHandler", - "CallbackQueryHandler", - "CommandHandler", - "WelcomeHandler", - "MessageHandler" - ]; + TypeOfExpressionSyntax typeofExpression = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(fullTypeName)); + VariableDeclaratorSyntax variableDeclarator = SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier($"{handler.ClassName}Type")) + .WithInitializer(SyntaxFactory.EqualsValueClause(typeofExpression)); - public static IEnumerable GetHandlerAttributes(this ClassDeclarationSyntax classSyntax) - { - IEnumerable attributes = classSyntax.AttributeLists.SelectMany(list => list.Attributes); - return attributes.IntersectBy(AttributeNames, attr => attr.Name.ToString()); - } - - public static BaseTypeSyntax? GetHandlerBaseClass(this ClassDeclarationSyntax classSyntax) - { - return classSyntax.BaseList?.Types.FirstOrDefault(type => HandlersNames.Contains(type.ToString())); - } + return SyntaxFactory.FieldDeclaration( + SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName("System.Type")) + .WithVariables(SyntaxFactory.SingletonSeparatedList(variableDeclarator))) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))); } } + +internal class HandlerDeclarationModel(string className, string namespaceName, string? attributeName, string? baseClassName, Location location) +{ + public readonly string ClassName = className; + public readonly string Namespace = namespaceName; + public readonly string? AttributeName = attributeName; + public readonly string? BaseClassName = baseClassName; + public readonly Location Location = location; +} + +internal static class DeveloperHelperAnalyzerExtensions +{ + private static readonly string[] HandlersNames = + [ + "AnyUpdateHandler", + "CallbackQueryHandler", + "CommandHandler", + "WelcomeHandler", + "MessageHandler" + ]; + + // Ищет атрибут и возвращает его нормализованное имя (без суффикса Attribute) + public static string? GetHandlerAttributeName(this ClassDeclarationSyntax classSyntax) + { + string attributeName = classSyntax.AttributeLists + .SelectMany(list => list.Attributes) + .Select(attr => attr.Name.ToString()) + .FirstOrDefault(name => HandlersNames.Any(h => name == h || name == h + "Attribute")); + + return attributeName?.Replace("Attribute", ""); + } + + // Ищет базовый класс из нашего списка + public static string? GetHandlerBaseClassName(this ClassDeclarationSyntax classSyntax) + { + if (classSyntax.BaseList == null) + return null; + + return classSyntax.BaseList.Types + .Select(t => t.Type.ToString()) + .FirstOrDefault(name => HandlersNames.Contains(name)); + } + + // Достает namespace, в котором объявлен класс + public static string GetNamespace(this ClassDeclarationSyntax classDeclaration) + { + BaseNamespaceDeclarationSyntax? namespaceDeclaration = classDeclaration.FirstAncestorOrSelf(); + return namespaceDeclaration?.Name.ToString() ?? "Global"; + } +} \ No newline at end of file diff --git a/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs b/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs index d6bd496..655fdb2 100644 --- a/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs +++ b/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs @@ -1,455 +1,488 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using System.Collections.Immutable; -using Telegrator.Analyzers.RoslynExtensions; +using System.Text; -namespace Telegrator.Analyzers +namespace Telegrator.Analyzers; + +[Generator(LanguageNames.CSharp)] +public class GeneratedKeyboardMarkupGenerator : IIncrementalGenerator { - [Generator(LanguageNames.CSharp)] - public class GeneratedKeyboardMarkupGenerator : IIncrementalGenerator + // Return types + private const string InlineReturnType = "InlineKeyboardMarkup"; + private const string ReplyReturnType = "ReplyKeyboardMarkup"; + + // Attribute names + private const string CallbackDataAttribute = "CallbackButton"; + private const string CallbackGameAttribute = "GameButton"; + private const string CopyTextAttribute = "CopyTextButton"; + private const string LoginRequestAttribute = "LoginRequestButton"; + private const string PayRequestAttribute = "PayRequestButton"; + private const string SwitchQueryAttribute = "SwitchQueryButton"; + private const string QueryChosenAttribute = "QueryChosenButton"; + private const string QueryCurrentAttribute = "QueryCurrentButton"; + private const string UrlRedirectAttribute = "UrlRedirectButton"; + private const string RequestChatAttribute = "RequestChatButton"; + private const string RequestContactAttribute = "RequestContactButton"; + private const string RequestLocationAttribute = "RequestLocationButton"; + private const string RequestPoolAttribute = "RequestPoolButton"; + private const string RequestUsersAttribute = "RequestUsersButton"; + private const string WebAppAttribute = "WebApp"; + + // Markup lists + private static readonly string[] InlineAttributes = [CallbackDataAttribute, CallbackGameAttribute, CopyTextAttribute, LoginRequestAttribute, PayRequestAttribute, UrlRedirectAttribute, WebAppAttribute, SwitchQueryAttribute, QueryChosenAttribute, QueryCurrentAttribute]; + private static readonly string[] ReplyAttributes = [RequestChatAttribute, RequestContactAttribute, RequestLocationAttribute, RequestPoolAttribute, RequestUsersAttribute, WebAppAttribute]; + + // Usings + private static readonly string[] DefaultUsings = ["Telegram.Bot.Types.ReplyMarkups"]; + + // Markup layouts + private static readonly Dictionary InlineKeyboardLayout = new Dictionary() { - // Return types - private const string InlineReturnType = "InlineKeyboardMarkup"; - private const string ReplyReturnType = "ReplyKeyboardMarkup"; - - // Attribute names - private const string CallbackDataAttribute = "CallbackButton"; - private const string CallbackGameAttribute = "GameButton"; - private const string CopyTextAttribute = "CopyTextButton"; - private const string LoginRequestAttribute = "LoginRequestButton"; - private const string PayRequestAttribute = "PayRequestButton"; - private const string SwitchQueryAttribute = "SwitchQueryButton"; - private const string QueryChosenAttribute = "QueryChosenButton"; - private const string QueryCurrentAttribute = "QueryCurrentButton"; - private const string UrlRedirectAttribute = "UrlRedirectButton"; - private const string RequestChatAttribute = "RequestChatButton"; - private const string RequestContactAttribute = "RequestContactButton"; - private const string RequestLocationAttribute = "RequestLocationButton"; - private const string RequestPoolAttribute = "RequestPoolButton"; - private const string RequestUsersAttribute = "RequestUsersButton"; - private const string WebAppAttribute = "WebApp"; - - // Markup lists - private static readonly string[] InlineAttributes = [CallbackDataAttribute, CallbackGameAttribute, CopyTextAttribute, LoginRequestAttribute, PayRequestAttribute, UrlRedirectAttribute, WebAppAttribute, SwitchQueryAttribute, QueryChosenAttribute, QueryCurrentAttribute]; - private static readonly string[] ReplyAttributes = [RequestChatAttribute, RequestContactAttribute, RequestLocationAttribute, RequestPoolAttribute, RequestUsersAttribute, WebAppAttribute]; - - // Usings - private static readonly string[] DefaultUsings = ["Telegram.Bot.Types.ReplyMarkups"]; - - // Markup layouts - private static readonly Dictionary InlineKeyboardLayout = new Dictionary() - { - { CallbackDataAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackData") }, - { CallbackGameAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackGame") }, - { CopyTextAttribute, AccessExpression("InlineKeyboardButton", "WithCopyText") }, - { LoginRequestAttribute, AccessExpression("InlineKeyboardButton", "WithLoginUrl") }, - { PayRequestAttribute, AccessExpression("InlineKeyboardButton", "WithPay") }, - { SwitchQueryAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQuery") }, - { QueryChosenAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryChosenChat") }, - { QueryCurrentAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryCurrentChat") }, - { UrlRedirectAttribute, AccessExpression("InlineKeyboardButton", "WithUrl") }, - { WebAppAttribute, AccessExpression("InlineKeyboardButton", "WithWebApp") }, - }; - - private static readonly Dictionary ReplyKeyboardLayout = new Dictionary() - { - { RequestChatAttribute, AccessExpression("KeyboardButton", "WithRequestChat") }, - { RequestContactAttribute, AccessExpression("KeyboardButton", "WithRequestContact") }, - { RequestLocationAttribute, AccessExpression("KeyboardButton", "WithRequestLocation") }, - { RequestPoolAttribute, AccessExpression("KeyboardButton", "WithRequestPoll") }, - { RequestUsersAttribute, AccessExpression("KeyboardButton", "WithRequestUsers") }, - { WebAppAttribute, AccessExpression("KeyboardButton", "WithWebApp") } - }; - - // Markup map - private static readonly Dictionary> LayoutNames = new Dictionary>() - { - { InlineReturnType, InlineKeyboardLayout }, - { ReplyReturnType, ReplyKeyboardLayout } - }; - - // Diagnostic descriptors - private static readonly DiagnosticDescriptor WrongReturnType = new DiagnosticDescriptor("TG_1001", "Wrong return type", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor UnsupportedAttribute = new DiagnosticDescriptor("TG_1002", "Unsupported or invalid attribute", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor NotPartialMethod = new DiagnosticDescriptor("TG_1003", "Not a partial member", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor UseBodylessMethod = new DiagnosticDescriptor("TG_1004", "Use bodyless method", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor UseParametrlessMethod = new DiagnosticDescriptor("TG_1005", "Use parametrless method", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor UseGetOnlyProperty = new DiagnosticDescriptor("TG_1006", "Use property with only get accessor", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - private static readonly DiagnosticDescriptor UseBodylessGetAccessor = new DiagnosticDescriptor("TG_1007", "Use bodyless get accessor", string.Empty, "Modelling", DiagnosticSeverity.Error, true); - - // Trivias - private static SyntaxTrivia TabulationTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, "\t"); - private static SyntaxTrivia WhitespaceTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "); - private static SyntaxTrivia NewLineTrivia => SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, "\n"); - private static SyntaxToken Semicolon => SyntaxFactory.Token(SyntaxKind.SemicolonToken); - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - IncrementalValueProvider> methodsPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideMethods, TransformMethods).Where(x => x != null).Collect(); - IncrementalValueProvider> propertiesPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideProperties, TransformProperties).Where(x => x != null).Collect(); - - context.RegisterSourceOutput(methodsPipeline, ExecuteMethodsPipeline); - context.RegisterSourceOutput(propertiesPipeline, ExecutePropertiessPipeline); - } - - private static bool ProvideMethods(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if (syntaxNode is not MethodDeclarationSyntax method) - return false; - - if (!HasGenAttributes(method)) - return false; - - return true; - } - - private static bool ProvideProperties(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if (syntaxNode is not PropertyDeclarationSyntax property) - return false; - - if (!HasGenAttributes(property)) - return false; - - return true; - } - - private static MethodDeclarationSyntax TransformMethods(GeneratorSyntaxContext context, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return (MethodDeclarationSyntax)context.Node; - } - - private static PropertyDeclarationSyntax TransformProperties(GeneratorSyntaxContext context, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return (PropertyDeclarationSyntax)context.Node; - } - - private static void ExecutePropertiessPipeline(SourceProductionContext context, ImmutableArray properties) - { - List models = []; - foreach (PropertyDeclarationSyntax prop in properties) - { - context.CancellationToken.ThrowIfCancellationRequested(); - try - { - string methodName = prop.Identifier.Text; - string returnType = prop.Type.ToString(); - bool anyErrors = false; - - if (!LayoutNames.TryGetValue(returnType, out var layout)) - { - WrongReturnType.Report(context, prop.Type.GetLocation()); - anyErrors = true; - } - - if (!prop.Modifiers.HasModifiers("partial")) - { - NotPartialMethod.Report(context, prop.Identifier.GetLocation()); - anyErrors = true; - } - - if (prop.Initializer != null) - { - UseGetOnlyProperty.Report(context, prop.Initializer.GetLocation()); - anyErrors = true; - } - - if (prop.ExpressionBody != null) - { - UseGetOnlyProperty.Report(context, prop.ExpressionBody.GetLocation()); - anyErrors = true; - } - - if (prop.AccessorList != null) - { - foreach (AccessorDeclarationSyntax accessor in prop.AccessorList.Accessors) - { - if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration)) - { - UseGetOnlyProperty.Report(context, accessor.GetLocation()); - anyErrors = true; - continue; - } - - if (accessor.Body != null) - { - UseBodylessGetAccessor.Report(context, accessor.Body.GetLocation()); - anyErrors = true; - continue; - } - - if (accessor.ExpressionBody != null) - { - UseBodylessGetAccessor.Report(context, accessor.ExpressionBody.GetLocation()); - anyErrors = true; - continue; - } - } - } - - if (anyErrors) - return; - - SeparatedSyntaxList matrix = ParseAttributesMatrix(context, layout, prop); - PropertyDeclarationSyntax genProp = GeneratedPropertyDeclaration(prop, SyntaxFactory.CollectionExpression(matrix)); - models.Add(new GeneratedMarkupPropertyModel(prop, genProp)); - } - catch (Exception ex) - { - context.AddSource(prop.Identifier.ToString(), ex.ToString()); - } - } - - context.CancellationToken.ThrowIfCancellationRequested(); - CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit(); - SyntaxList usingDirectives = ParseUsings(DefaultUsings).ToSyntaxList(); - - foreach (GeneratedMarkupPropertyModel model in models) - { - context.CancellationToken.ThrowIfCancellationRequested(); - - try - { - if (model.OriginalProperty.Parent is not ClassDeclarationSyntax) - throw new MissingMemberException(); - - NamespaceDeclarationSyntax genNamespace = GeneratedNamespaceDeclaration(model.OriginalProperty, [model.GeneratedProperty]); - compilationUnit = compilationUnit.AddMembers(genNamespace); - } - catch (Exception ex) - { - context.AddSource(model.OriginalProperty.Identifier.ToString(), ex.ToString()); - } - } - - compilationUnit = compilationUnit.WithUsings(usingDirectives); - context.AddSource("GeneratedKeyboards.Properties.g", compilationUnit.ToFullString()); - } - - private static void ExecuteMethodsPipeline(SourceProductionContext context, ImmutableArray methods) - { - List models = []; - foreach (MethodDeclarationSyntax method in methods) - { - context.CancellationToken.ThrowIfCancellationRequested(); - try - { - string methodName = method.Identifier.Text; - string returnType = method.ReturnType.ToString(); - bool anyErrors = false; - - if (!LayoutNames.TryGetValue(returnType, out var layout)) - { - WrongReturnType.Report(context, method.ReturnType.GetLocation()); - anyErrors = true; - } - - if (!method.Modifiers.HasModifiers("partial")) - { - NotPartialMethod.Report(context, method.Identifier.GetLocation()); - anyErrors = true; - } - - if (method.ParameterList.Parameters.Any()) - { - UseParametrlessMethod.Report(context, method.ParameterList.GetLocation()); - anyErrors = true; - } - - if (method.ExpressionBody != null) - { - UseBodylessMethod.Report(context, method.ExpressionBody.GetLocation()); - anyErrors = true; - } - - if (method.Body != null) - { - UseBodylessMethod.Report(context, method.Body.GetLocation()); - anyErrors = true; - } - - if (anyErrors) - return; - - SeparatedSyntaxList matrix = ParseAttributesMatrix(context, layout, method); - FieldDeclarationSyntax genField = GeneratedFieldDeclaration(methodName, method.ReturnType.WithoutTrivia(), SyntaxFactory.CollectionExpression(matrix)); - MethodDeclarationSyntax genMethod = GeneratedMethodDeclaration(methodName, method.Modifiers, method.ReturnType, genField); - models.Add(new GeneratedMarkupMethodModel(method, genField, genMethod)); - } - catch (Exception ex) - { - context.AddSource(method.Identifier.ToString(), ex.ToString()); - } - } - - context.CancellationToken.ThrowIfCancellationRequested(); - CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit(); - SyntaxList usingDirectives = ParseUsings(DefaultUsings).ToSyntaxList(); - - foreach (GeneratedMarkupMethodModel model in models) - { - context.CancellationToken.ThrowIfCancellationRequested(); - - try - { - if (model.OriginalMethod.Parent is not ClassDeclarationSyntax) - throw new MissingMemberException(); - - NamespaceDeclarationSyntax genNamespace = GeneratedNamespaceDeclaration(model.OriginalMethod, [model.GeneratedField, model.GeneratedMethod]); - compilationUnit = compilationUnit.AddMembers(genNamespace); - } - catch (Exception ex) - { - context.AddSource(model.OriginalMethod.Identifier.ToString(), ex.ToString()); - } - } - - compilationUnit = compilationUnit.WithUsings(usingDirectives); - context.AddSource("GeneratedKeyboards.Methods.g", compilationUnit.ToFullString()); - } - - private static SeparatedSyntaxList ParseAttributesMatrix(SourceProductionContext context, Dictionary layout, MemberDeclarationSyntax member) - { - SeparatedSyntaxList vertical = new SeparatedSyntaxList(); - foreach (AttributeListSyntax attributeList in member.AttributeLists) - { - context.CancellationToken.ThrowIfCancellationRequested(); - SeparatedSyntaxList horizontal = new SeparatedSyntaxList(); - - foreach (AttributeSyntax attribute in attributeList.Attributes) - { - context.CancellationToken.ThrowIfCancellationRequested(); - if (!layout.TryGetValue(attribute.Name.ToString(), out var accessSyntax)) - { - UnsupportedAttribute.Report(context, attribute.Name.GetLocation()); - continue; - } - - InvocationExpressionSyntax expression = SyntaxFactory.InvocationExpression(accessSyntax, ConvertArguments(attribute.ArgumentList)); - horizontal = horizontal.Add(SyntaxFactory.ExpressionElement(expression)); - } - - ExpressionElementSyntax element = SyntaxFactory.ExpressionElement(SyntaxFactory.CollectionExpression(horizontal)); - vertical = vertical.Add(element); - } - - return vertical; - } - - private static PropertyDeclarationSyntax GeneratedPropertyDeclaration(PropertyDeclarationSyntax property, CollectionExpressionSyntax collection) - { - return SyntaxFactory.PropertyDeclaration(property.Type, property.Identifier) - .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseToken(" => "), collection)); - } - - private static MethodDeclarationSyntax GeneratedMethodDeclaration(string identifier, SyntaxTokenList modifiers, TypeSyntax returnType, FieldDeclarationSyntax field) - { - return SyntaxFactory.MethodDeclaration(returnType.WithTrailingTrivia(WhitespaceTrivia), identifier) - .WithModifiers(modifiers) - .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseToken(" => "), SyntaxFactory.IdentifierName(field.Declaration.Variables.ElementAt(0).Identifier))) - .WithSemicolonToken(Semicolon); - } - - private static FieldDeclarationSyntax GeneratedFieldDeclaration(string identifier, TypeSyntax returnType, CollectionExpressionSyntax collection) - { - ArgumentListSyntax arguments = SyntaxFactory.ArgumentList(SeparatedSyntaxList(SyntaxFactory.Argument(collection))); - ObjectCreationExpressionSyntax objectCreation = SyntaxFactory.ObjectCreationExpression(returnType.WithLeadingTrivia(WhitespaceTrivia), arguments, null); - - VariableDeclaratorSyntax declarator = SyntaxFactory.VariableDeclarator(identifier + "_generatedMarkup") - .WithInitializer(SyntaxFactory.EqualsValueClause(SyntaxFactory.ParseToken(" = "), objectCreation)); - - return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(returnType.WithTrailingTrivia(WhitespaceTrivia)).AddVariables(declarator)) - .WithModifiers(Modifiers(SyntaxKind.PrivateKeyword, SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)); - } - - private static ArgumentListSyntax ConvertArguments(AttributeArgumentListSyntax? attributeArgs) - { - if (attributeArgs == null) - return SyntaxFactory.ArgumentList(); - - return SyntaxFactory.ArgumentList(SeparatedSyntaxList(attributeArgs.Arguments.Select(CastArgument))); - } - - private static NamespaceDeclarationSyntax GeneratedNamespaceDeclaration(MemberDeclarationSyntax member, IEnumerable generatedMembers) - { - if (member.Parent is not ClassDeclarationSyntax containerClass) - throw new MemberAccessException(); - - int times = member.CountParentTree(); - ClassDeclarationSyntax generatedContainerClass = SyntaxFactory.ClassDeclaration(containerClass.Identifier) - .WithMembers(new SyntaxList(generatedMembers.Select(member => member.DecorateMember(times + 1)))) - .WithModifiers(containerClass.Modifiers.Decorate()) - .DecorateType(times); - - MemberDeclarationSyntax generated = generatedContainerClass; - MemberDeclarationSyntax inspecting = containerClass; - - while (inspecting.Parent != null) - { - times -= 1; - if (inspecting.Parent is not MemberDeclarationSyntax inspectingMember) - break; - - inspecting = inspectingMember; - switch (inspectingMember) - { - case ClassDeclarationSyntax classDeclaration: - { - generated = SyntaxFactory.ClassDeclaration(classDeclaration.Identifier) - .WithMembers([generated]) - .WithModifiers(classDeclaration.Modifiers.Decorate()) - .DecorateType(times); - - break; - } - - case StructDeclarationSyntax structDeclaration: - { - generated = SyntaxFactory.StructDeclaration(structDeclaration.Identifier) - .WithMembers([generated]) - .WithModifiers(structDeclaration.Modifiers.Decorate()) - .DecorateType(times); - - break; - } - - case NamespaceDeclarationSyntax namespaceDeclaration: - { - //foundNamespaceDeclaration = namespaceDeclaration; - return SyntaxFactory.NamespaceDeclaration(namespaceDeclaration.Name) - .WithMembers([generated]).Decorate(); - } - } - } - - throw new AncestorNotFoundException(); - } - - private static ArgumentSyntax CastArgument(AttributeArgumentSyntax argument) - => SyntaxFactory.Argument(argument.Expression).WithNameColon(argument.NameColon); - - private static SyntaxTokenList Modifiers(params SyntaxKind[] kinds) - => new SyntaxTokenList(kinds.Select(SyntaxFactory.Token).Select(mod => mod.WithTrailingTrivia(WhitespaceTrivia))); - - private static IEnumerable ParseUsings(params string[] names) => names - .Select(name => SyntaxFactory.IdentifierName(name).WithLeadingTrivia(WhitespaceTrivia)) - .Select(name => SyntaxFactory.UsingDirective(name).WithTrailingTrivia(NewLineTrivia)); - - private static bool HasGenAttributes(MemberDeclarationSyntax member) => member.AttributeLists.SelectMany(x => x.Attributes) - .Select(x => x.Name.ToString()).Intersect(InlineAttributes.Concat(ReplyAttributes)).Any(); - - private static SeparatedSyntaxList SeparatedSyntaxList(params IEnumerable elements) where T : SyntaxNode - => new SeparatedSyntaxList().AddRange(elements); - - private static MemberAccessExpressionSyntax AccessExpression(string className, string methodName) - => SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName(className), SyntaxFactory.IdentifierName(methodName)); - - private record class GeneratedMarkupMethodModel(MethodDeclarationSyntax OriginalMethod, FieldDeclarationSyntax GeneratedField, MethodDeclarationSyntax GeneratedMethod); - private record class GeneratedMarkupPropertyModel(PropertyDeclarationSyntax OriginalProperty, PropertyDeclarationSyntax GeneratedProperty); + { CallbackDataAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackData") }, + { CallbackGameAttribute, AccessExpression("InlineKeyboardButton", "WithCallbackGame") }, + { CopyTextAttribute, AccessExpression("InlineKeyboardButton", "WithCopyText") }, + { LoginRequestAttribute, AccessExpression("InlineKeyboardButton", "WithLoginUrl") }, + { PayRequestAttribute, AccessExpression("InlineKeyboardButton", "WithPay") }, + { SwitchQueryAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQuery") }, + { QueryChosenAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryChosenChat") }, + { QueryCurrentAttribute, AccessExpression("InlineKeyboardButton", "WithSwitchInlineQueryCurrentChat") }, + { UrlRedirectAttribute, AccessExpression("InlineKeyboardButton", "WithUrl") }, + { WebAppAttribute, AccessExpression("InlineKeyboardButton", "WithWebApp") }, + }; + + private static readonly Dictionary ReplyKeyboardLayout = new Dictionary() + { + { RequestChatAttribute, AccessExpression("KeyboardButton", "WithRequestChat") }, + { RequestContactAttribute, AccessExpression("KeyboardButton", "WithRequestContact") }, + { RequestLocationAttribute, AccessExpression("KeyboardButton", "WithRequestLocation") }, + { RequestPoolAttribute, AccessExpression("KeyboardButton", "WithRequestPoll") }, + { RequestUsersAttribute, AccessExpression("KeyboardButton", "WithRequestUsers") }, + { WebAppAttribute, AccessExpression("KeyboardButton", "WithWebApp") } + }; + + // Markup map + private static readonly Dictionary> LayoutNames = new Dictionary>() + { + { InlineReturnType, InlineKeyboardLayout }, + { ReplyReturnType, ReplyKeyboardLayout } + }; + + // Diagnostic descriptors + private static readonly DiagnosticDescriptor WrongReturnType = new DiagnosticDescriptor("TLG201", "Wrong return type", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor UnsupportedAttribute = new DiagnosticDescriptor("TLG202", "Unsupported or invalid attribute", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor NotPartialMethod = new DiagnosticDescriptor("TLG203", "Not a partial member", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor UseBodylessMethod = new DiagnosticDescriptor("TLG204", "Use bodyless method", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor UseParametrlessMethod = new DiagnosticDescriptor("TLG205", "Use parametrless method", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor UseGetOnlyProperty = new DiagnosticDescriptor("TLG206", "Use property with only get accessor", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor UseBodylessGetAccessor = new DiagnosticDescriptor("TLG207", "Use bodyless get accessor", string.Empty, "Telegrator.Modelling", DiagnosticSeverity.Error, true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValueProvider> methodsPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideMethods, TransformMethods).Where(x => x != null).Collect(); + IncrementalValueProvider> propertiesPipeline = context.SyntaxProvider.CreateSyntaxProvider(ProvideProperties, TransformProperties).Where(x => x != null).Collect(); + + context.RegisterSourceOutput(methodsPipeline, ExecuteMethodsPipeline); + context.RegisterSourceOutput(propertiesPipeline, ExecutePropertiesPipeline); } -} + + private static bool ProvideMethods(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (syntaxNode is not MethodDeclarationSyntax method) + return false; + + if (!HasGenAttributes(method)) + return false; + + return true; + } + + private static bool ProvideProperties(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (syntaxNode is not PropertyDeclarationSyntax property) + return false; + + if (!HasGenAttributes(property)) + return false; + + return true; + } + + private static MethodDeclarationSyntax TransformMethods(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return (MethodDeclarationSyntax)context.Node; + } + + private static PropertyDeclarationSyntax TransformProperties(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return (PropertyDeclarationSyntax)context.Node; + } + + private static void ExecutePropertiesPipeline(SourceProductionContext context, ImmutableArray properties) + { + List models = new List(); + + foreach (PropertyDeclarationSyntax prop in properties) + { + context.CancellationToken.ThrowIfCancellationRequested(); + try + { + string returnType = prop.Type.ToString(); + bool anyErrors = false; + + Dictionary layout; + if (!LayoutNames.TryGetValue(returnType, out layout!)) + { + context.ReportDiagnostic(Diagnostic.Create(WrongReturnType, prop.Type.GetLocation())); + anyErrors = true; + } + + if (!prop.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + context.ReportDiagnostic(Diagnostic.Create(NotPartialMethod, prop.Identifier.GetLocation())); + anyErrors = true; + } + + if (prop.Initializer != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, prop.Initializer.GetLocation())); + anyErrors = true; + } + + if (prop.ExpressionBody != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, prop.ExpressionBody.GetLocation())); + anyErrors = true; + } + + if (prop.AccessorList != null) + { + foreach (AccessorDeclarationSyntax accessor in prop.AccessorList.Accessors) + { + if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration)) + { + context.ReportDiagnostic(Diagnostic.Create(UseGetOnlyProperty, accessor.GetLocation())); + anyErrors = true; + continue; + } + + if (accessor.Body != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseBodylessGetAccessor, accessor.Body.GetLocation())); + anyErrors = true; + continue; + } + + if (accessor.ExpressionBody != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseBodylessGetAccessor, accessor.ExpressionBody.GetLocation())); + anyErrors = true; + continue; + } + } + } + + if (anyErrors || layout == null) + continue; + + SeparatedSyntaxList matrix = ParseAttributesMatrix(context, layout, prop); + PropertyDeclarationSyntax genProp = GeneratedPropertyDeclaration(prop, SyntaxFactory.CollectionExpression(matrix)); + models.Add(new GeneratedMarkupPropertyModel(prop, genProp)); + } + catch (Exception ex) + { + context.AddSource($"{prop.Identifier}_Error.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8)); + } + } + + if (models.Count == 0) + return; + + CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit(); + SyntaxList usingDirectives = SyntaxFactory.List(ParseUsings(DefaultUsings)); + + foreach (GeneratedMarkupPropertyModel model in models) + { + context.CancellationToken.ThrowIfCancellationRequested(); + try + { + MemberDeclarationSyntax wrappedMember = WrapInParentDeclarations(model.OriginalProperty, new List { model.GeneratedProperty }); + compilationUnit = compilationUnit.AddMembers(wrappedMember); + } + catch (Exception ex) + { + context.AddSource($"{model.OriginalProperty.Identifier}_GenError.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8)); + } + } + + compilationUnit = compilationUnit.WithUsings(usingDirectives).NormalizeWhitespace(); + context.AddSource("GeneratedKeyboards.Properties.g.cs", SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8)); + } + + private static void ExecuteMethodsPipeline(SourceProductionContext context, ImmutableArray methods) + { + List models = new List(); + + foreach (MethodDeclarationSyntax method in methods) + { + context.CancellationToken.ThrowIfCancellationRequested(); + try + { + string methodName = method.Identifier.Text; + string returnType = method.ReturnType.ToString(); + bool anyErrors = false; + + Dictionary layout; + if (!LayoutNames.TryGetValue(returnType, out layout!)) + { + context.ReportDiagnostic(Diagnostic.Create(WrongReturnType, method.ReturnType.GetLocation())); + anyErrors = true; + } + + if (!method.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + context.ReportDiagnostic(Diagnostic.Create(NotPartialMethod, method.Identifier.GetLocation())); + anyErrors = true; + } + + if (method.ParameterList.Parameters.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(UseParametrlessMethod, method.ParameterList.GetLocation())); + anyErrors = true; + } + + if (method.ExpressionBody != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseBodylessMethod, method.ExpressionBody.GetLocation())); + anyErrors = true; + } + + if (method.Body != null) + { + context.ReportDiagnostic(Diagnostic.Create(UseBodylessMethod, method.Body.GetLocation())); + anyErrors = true; + } + + if (anyErrors || layout == null) + continue; + + SeparatedSyntaxList matrix = ParseAttributesMatrix(context, layout, method); + FieldDeclarationSyntax genField = GeneratedFieldDeclaration(methodName, method.ReturnType, SyntaxFactory.CollectionExpression(matrix)); + MethodDeclarationSyntax genMethod = GeneratedMethodDeclaration(methodName, method.Modifiers, method.ReturnType, genField); + models.Add(new GeneratedMarkupMethodModel(method, genField, genMethod)); + } + catch (Exception ex) + { + context.AddSource($"{method.Identifier}_Error.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8)); + } + } + + if (models.Count == 0) + return; + + CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit(); + SyntaxList usingDirectives = SyntaxFactory.List(ParseUsings(DefaultUsings)); + + foreach (GeneratedMarkupMethodModel model in models) + { + context.CancellationToken.ThrowIfCancellationRequested(); + try + { + MemberDeclarationSyntax wrappedMembers = WrapInParentDeclarations(model.OriginalMethod, new List { model.GeneratedField, model.GeneratedMethod }); + compilationUnit = compilationUnit.AddMembers(wrappedMembers); + } + catch (Exception ex) + { + context.AddSource($"{model.OriginalMethod.Identifier}_GenError.g.cs", SourceText.From($"/* {ex} */", Encoding.UTF8)); + } + } + + compilationUnit = compilationUnit.WithUsings(usingDirectives).NormalizeWhitespace(); + context.AddSource("GeneratedKeyboards.Methods.g.cs", SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8)); + } + + private static SeparatedSyntaxList ParseAttributesMatrix(SourceProductionContext context, Dictionary layout, MemberDeclarationSyntax member) + { + SeparatedSyntaxList vertical = new SeparatedSyntaxList(); + + foreach (AttributeListSyntax attributeList in member.AttributeLists) + { + context.CancellationToken.ThrowIfCancellationRequested(); + SeparatedSyntaxList horizontal = new SeparatedSyntaxList(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + MemberAccessExpressionSyntax accessSyntax; + if (!layout.TryGetValue(attribute.Name.ToString(), out accessSyntax!)) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedAttribute, attribute.Name.GetLocation())); + continue; + } + + InvocationExpressionSyntax expression = SyntaxFactory.InvocationExpression(accessSyntax, ConvertArguments(attribute.ArgumentList)); + horizontal = horizontal.Add(SyntaxFactory.ExpressionElement(expression)); + } + + ExpressionElementSyntax element = SyntaxFactory.ExpressionElement(SyntaxFactory.CollectionExpression(horizontal)); + vertical = vertical.Add(element); + } + + return vertical; + } + + private static PropertyDeclarationSyntax GeneratedPropertyDeclaration(PropertyDeclarationSyntax property, CollectionExpressionSyntax collection) + { + return SyntaxFactory.PropertyDeclaration(property.Type, property.Identifier) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(collection)) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + } + + private static MethodDeclarationSyntax GeneratedMethodDeclaration(string identifier, SyntaxTokenList modifiers, TypeSyntax returnType, FieldDeclarationSyntax field) + { + VariableDeclaratorSyntax targetVariable = field.Declaration.Variables.First(); + + return SyntaxFactory.MethodDeclaration(returnType, identifier) + .WithModifiers(modifiers) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.IdentifierName(targetVariable.Identifier))) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); + } + + private static FieldDeclarationSyntax GeneratedFieldDeclaration(string identifier, TypeSyntax returnType, CollectionExpressionSyntax collection) + { + ArgumentSyntax argument = SyntaxFactory.Argument(collection); + ArgumentListSyntax arguments = SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(argument)); + ObjectCreationExpressionSyntax objectCreation = SyntaxFactory.ObjectCreationExpression(returnType, arguments, null); + + VariableDeclaratorSyntax declarator = SyntaxFactory.VariableDeclarator(identifier + "_generatedMarkup") + .WithInitializer(SyntaxFactory.EqualsValueClause(objectCreation)); + + SyntaxTokenList fieldModifiers = SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)); + + return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(returnType).AddVariables(declarator)) + .WithModifiers(fieldModifiers); + } + + private static ArgumentListSyntax ConvertArguments(AttributeArgumentListSyntax? attributeArgs) + { + if (attributeArgs == null) + return SyntaxFactory.ArgumentList(); + + IEnumerable arguments = attributeArgs.Arguments.Select(CastArgument); + return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments)); + } + + private static ArgumentSyntax CastArgument(AttributeArgumentSyntax argument) + { + if (argument.NameColon != null) + { + return SyntaxFactory.Argument(argument.Expression).WithNameColon(argument.NameColon); + } + + return SyntaxFactory.Argument(argument.Expression); + } + + private static MemberDeclarationSyntax WrapInParentDeclarations(MemberDeclarationSyntax originalMember, List generatedMembers) + { + SyntaxNode? parentNode = originalMember.Parent; + + if (parentNode is not ClassDeclarationSyntax) + { + throw new InvalidOperationException("Generated member must be contained within a class."); + } + + MemberDeclarationSyntax currentDeclaration = SyntaxFactory.ClassDeclaration(((ClassDeclarationSyntax)parentNode).Identifier) + .WithMembers(SyntaxFactory.List(generatedMembers)) + .WithModifiers(((ClassDeclarationSyntax)parentNode).Modifiers); + + if (!currentDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + currentDeclaration = ((ClassDeclarationSyntax)currentDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + } + + parentNode = parentNode.Parent; + + while (parentNode is TypeDeclarationSyntax typeDeclaration) + { + ClassDeclarationSyntax wrappingClass = SyntaxFactory.ClassDeclaration(typeDeclaration.Identifier) + .WithMembers(SyntaxFactory.SingletonList(currentDeclaration)) + .WithModifiers(typeDeclaration.Modifiers); + + if (!wrappingClass.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + wrappingClass = wrappingClass.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + } + + currentDeclaration = wrappingClass; + parentNode = parentNode.Parent; + } + + if (parentNode is BaseNamespaceDeclarationSyntax namespaceDeclaration) + { + currentDeclaration = SyntaxFactory.NamespaceDeclaration(namespaceDeclaration.Name) + .WithMembers(SyntaxFactory.SingletonList(currentDeclaration)); + } + + return currentDeclaration; + } + + private static IEnumerable ParseUsings(params string[] names) + { + return names.Select(name => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(name))); + } + + private static bool HasGenAttributes(MemberDeclarationSyntax member) + { + IEnumerable memberAttributes = member.AttributeLists + .SelectMany(x => x.Attributes) + .Select(x => x.Name.ToString()); + + IEnumerable targetAttributes = InlineAttributes.Concat(ReplyAttributes); + + return memberAttributes.Intersect(targetAttributes).Any(); + } + + private static MemberAccessExpressionSyntax AccessExpression(string className, string methodName) + { + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(className), + SyntaxFactory.IdentifierName(methodName)); + } + + private class GeneratedMarkupMethodModel + { + public MethodDeclarationSyntax OriginalMethod { get; } + public FieldDeclarationSyntax GeneratedField { get; } + public MethodDeclarationSyntax GeneratedMethod { get; } + + public GeneratedMarkupMethodModel(MethodDeclarationSyntax originalMethod, FieldDeclarationSyntax generatedField, MethodDeclarationSyntax generatedMethod) + { + OriginalMethod = originalMethod; + GeneratedField = generatedField; + GeneratedMethod = generatedMethod; + } + } + + private class GeneratedMarkupPropertyModel + { + public PropertyDeclarationSyntax OriginalProperty { get; } + public PropertyDeclarationSyntax GeneratedProperty { get; } + + public GeneratedMarkupPropertyModel(PropertyDeclarationSyntax originalProperty, PropertyDeclarationSyntax generatedProperty) + { + OriginalProperty = originalProperty; + GeneratedProperty = generatedProperty; + } + } +} \ No newline at end of file diff --git a/Telegrator.Analyzers/Models.cs b/Telegrator.Analyzers/Models.cs deleted file mode 100644 index 5f1c7e0..0000000 --- a/Telegrator.Analyzers/Models.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Telegrator.Analyzers -{ - internal class HandlerDeclarationModel(ClassDeclarationSyntax classDeclaration, IEnumerable handlerAttributes, BaseTypeSyntax? baseType) - { - public ClassDeclarationSyntax ClassDeclaration { get; } = classDeclaration; - public IEnumerable HandlerAttributes { get; } = handlerAttributes; - public BaseTypeSyntax? BaseType { get; } = baseType; - public bool HasAttributes => HandlerAttributes.Any(); - } -} diff --git a/Telegrator/MadiatorCore/Descriptors/DescribedHandlerDescriptor.cs b/Telegrator/MadiatorCore/Descriptors/DescribedHandlerDescriptor.cs index 5c379c6..d5eb717 100644 --- a/Telegrator/MadiatorCore/Descriptors/DescribedHandlerDescriptor.cs +++ b/Telegrator/MadiatorCore/Descriptors/DescribedHandlerDescriptor.cs @@ -104,7 +104,7 @@ namespace Telegrator.MadiatorCore.Descriptors ResetEvent.Wait(cancellationToken); } - public void ReportResult(Result result) + public void ReportResult(Result? result) { if (result != null) throw new InvalidOperationException("Result already reported"); diff --git a/Telegrator/Polling/UpdateHandlersPool.cs b/Telegrator/Polling/UpdateHandlersPool.cs index 3e1dd48..9ee262f 100644 --- a/Telegrator/Polling/UpdateHandlersPool.cs +++ b/Telegrator/Polling/UpdateHandlersPool.cs @@ -131,23 +131,21 @@ namespace Telegrator.Polling handlerInfo.ReportResult(lastResult); ExecutionLimiter?.Release(1); - - if (lastResult.RouteNext) - { - Alligator.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); - } } catch (NotImplementedException) { _ = 0xBAD + 0xC0DE; + handlerInfo.ReportResult(null); } catch (OperationCanceledException) { _ = 0xDEADBEEF; + handlerInfo.ReportResult(null); } catch (Exception ex) { Alligator.LogError("Failed to process handler '{0}' (Update {1})", exception: ex, handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); + handlerInfo.ReportResult(null); } } diff --git a/Telegrator/Polling/UpdateRouter.cs b/Telegrator/Polling/UpdateRouter.cs index 3c640f4..111a8df 100644 --- a/Telegrator/Polling/UpdateRouter.cs +++ b/Telegrator/Polling/UpdateRouter.cs @@ -128,6 +128,8 @@ namespace Telegrator.Polling if (lastResult != null && !lastResult.RouteNext) break; + + Alligator.LogTrace("Handler '{0}' requested route continuation (Update {1})", handlerInfo.DisplayString, handlerInfo.HandlingUpdate.Id); } // Checking if awaiting handlers has exclusive routing