From c6d1abeacbd0e57eb7fbbee58b5706ed612635b4 Mon Sep 17 00:00:00 2001 From: Rikitav Date: Tue, 19 Aug 2025 01:39:09 +0400 Subject: [PATCH] Added keyboard markup generation from properties --- .../AnalyzerReleases.Unshipped.md | 2 + .../GeneratedKeyboardMarkupGenerator.cs | 218 ++++++++++++++---- .../Telegrator.Hosting.Web.csproj | 2 +- Telegrator.Hosting/Telegrator.Hosting.csproj | 2 +- Telegrator/Telegrator.csproj | 2 +- 5 files changed, 179 insertions(+), 47 deletions(-) diff --git a/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md index 363bfe5..dcb316e 100644 --- a/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md +++ b/Telegrator.Analyzers/AnalyzerReleases.Unshipped.md @@ -10,4 +10,6 @@ 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 TR0001 | Aspect | Error | DiagnosticsHelper \ No newline at end of file diff --git a/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs b/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs index e0e8284..d6bd496 100644 --- a/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs +++ b/Telegrator.Analyzers/GeneratedKeyboardMarkupGenerator.cs @@ -72,9 +72,11 @@ namespace Telegrator.Analyzers // 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 method", 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"); @@ -84,15 +86,14 @@ namespace Telegrator.Analyzers public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValueProvider> pipeline = context.SyntaxProvider - .CreateSyntaxProvider(Provide, Transform) - .Where(x => x != null) - .Collect(); + 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(pipeline, Execute); + context.RegisterSourceOutput(methodsPipeline, ExecuteMethodsPipeline); + context.RegisterSourceOutput(propertiesPipeline, ExecutePropertiessPipeline); } - private static bool Provide(SyntaxNode syntaxNode, CancellationToken cancellationToken) + private static bool ProvideMethods(SyntaxNode syntaxNode, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (syntaxNode is not MethodDeclarationSyntax method) @@ -104,13 +105,133 @@ namespace Telegrator.Analyzers return true; } - private static MethodDeclarationSyntax Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken) + 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 void Execute(SourceProductionContext context, ImmutableArray methods) + 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) @@ -155,35 +276,9 @@ namespace Telegrator.Analyzers if (anyErrors) return; - context.CancellationToken.ThrowIfCancellationRequested(); - SeparatedSyntaxList vertical = new SeparatedSyntaxList(); - - foreach (AttributeListSyntax attributeList in method.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()); - return; - } - - 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); - } - - FieldDeclarationSyntax genField = GeneratedFieldDeclaration(methodName, method.ReturnType.WithoutTrivia(), SyntaxFactory.CollectionExpression(vertical)); + 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) @@ -215,14 +310,48 @@ namespace Telegrator.Analyzers } compilationUnit = compilationUnit.WithUsings(usingDirectives); - context.AddSource("GeneratedKeyboards.g", compilationUnit.ToFullString()); + 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.IdentifierName(field.Declaration.Variables.ElementAt(0).Identifier))) + .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(SyntaxFactory.ParseToken(" => "), SyntaxFactory.IdentifierName(field.Declaration.Variables.ElementAt(0).Identifier))) .WithSemicolonToken(Semicolon); } @@ -232,7 +361,7 @@ namespace Telegrator.Analyzers ObjectCreationExpressionSyntax objectCreation = SyntaxFactory.ObjectCreationExpression(returnType.WithLeadingTrivia(WhitespaceTrivia), arguments, null); VariableDeclaratorSyntax declarator = SyntaxFactory.VariableDeclarator(identifier + "_generatedMarkup") - .WithInitializer(SyntaxFactory.EqualsValueClause(objectCreation)); + .WithInitializer(SyntaxFactory.EqualsValueClause(SyntaxFactory.ParseToken(" = "), objectCreation)); return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(returnType.WithTrailingTrivia(WhitespaceTrivia)).AddVariables(declarator)) .WithModifiers(Modifiers(SyntaxKind.PrivateKeyword, SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)); @@ -246,12 +375,12 @@ namespace Telegrator.Analyzers return SyntaxFactory.ArgumentList(SeparatedSyntaxList(attributeArgs.Arguments.Select(CastArgument))); } - private static NamespaceDeclarationSyntax GeneratedNamespaceDeclaration(MethodDeclarationSyntax method, IEnumerable generatedMembers) + private static NamespaceDeclarationSyntax GeneratedNamespaceDeclaration(MemberDeclarationSyntax member, IEnumerable generatedMembers) { - if (method.Parent is not ClassDeclarationSyntax containerClass) + if (member.Parent is not ClassDeclarationSyntax containerClass) throw new MemberAccessException(); - int times = method.CountParentTree(); + int times = member.CountParentTree(); ClassDeclarationSyntax generatedContainerClass = SyntaxFactory.ClassDeclaration(containerClass.Identifier) .WithMembers(new SyntaxList(generatedMembers.Select(member => member.DecorateMember(times + 1)))) .WithModifiers(containerClass.Modifiers.Decorate()) @@ -311,7 +440,7 @@ namespace Telegrator.Analyzers .Select(name => SyntaxFactory.IdentifierName(name).WithLeadingTrivia(WhitespaceTrivia)) .Select(name => SyntaxFactory.UsingDirective(name).WithTrailingTrivia(NewLineTrivia)); - private static bool HasGenAttributes(MethodDeclarationSyntax method) => method.AttributeLists.SelectMany(x => x.Attributes) + 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 @@ -321,5 +450,6 @@ namespace Telegrator.Analyzers => 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); } } diff --git a/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj b/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj index 3999310..2f3453b 100644 --- a/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj +++ b/Telegrator.Hosting.Web/Telegrator.Hosting.Web.csproj @@ -15,7 +15,7 @@ True LICENSE README.md - 1.15.1 + 1.15.2 diff --git a/Telegrator.Hosting/Telegrator.Hosting.csproj b/Telegrator.Hosting/Telegrator.Hosting.csproj index 8c07210..ff5435d 100644 --- a/Telegrator.Hosting/Telegrator.Hosting.csproj +++ b/Telegrator.Hosting/Telegrator.Hosting.csproj @@ -16,7 +16,7 @@ True LICENSE README.md - 1.15.1 + 1.15.2 diff --git a/Telegrator/Telegrator.csproj b/Telegrator/Telegrator.csproj index 11434b9..6f6589b 100644 --- a/Telegrator/Telegrator.csproj +++ b/Telegrator/Telegrator.csproj @@ -17,7 +17,7 @@ telegram;bot;mediator;attributes;aspect;hosting;host;framework;easy;simple;handlers README.md LICENSE - 1.15.1 + 1.15.2