Files
Telegrator/Telegrator.Analyzers/DeveloperHelperAnalyzer.cs
T

200 lines
8.3 KiB
C#
Raw Normal View History

2025-07-24 23:19:59 +04:00
using Microsoft.CodeAnalysis;
2026-03-06 20:01:54 +04:00
using Microsoft.CodeAnalysis.CSharp;
2025-07-24 23:19:59 +04:00
using Microsoft.CodeAnalysis.CSharp.Syntax;
2026-03-06 20:01:54 +04:00
using Microsoft.CodeAnalysis.Text;
2025-07-24 23:19:59 +04:00
using System.Collections.Immutable;
using System.Text;
2026-03-06 20:01:54 +04:00
namespace Telegrator.Analyzers;
[Generator(LanguageNames.CSharp)]
public class DeveloperHelperAnalyzer : IIncrementalGenerator
2025-07-24 23:19:59 +04:00
{
2026-03-06 20:01:54 +04:00
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)
2025-07-24 23:19:59 +04:00
{
2026-03-06 20:01:54 +04:00
IncrementalValueProvider<ImmutableArray<HandlerDeclarationModel>> pipeline = context.SyntaxProvider
.CreateSyntaxProvider(Provide, Transform)
.Where(handler => handler != null)
.Collect();
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
context.RegisterSourceOutput(pipeline, Execute);
}
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
private static bool Provide(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (syntaxNode is not ClassDeclarationSyntax classSyntax)
return false;
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
if (classSyntax.BaseList?.Types.Count == 0 && classSyntax.AttributeLists.Count == 0)
return false;
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
return true;
}
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
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()
);
}
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
private static void Execute(SourceProductionContext context, ImmutableArray<HandlerDeclarationModel> handlers)
{
if (handlers.IsDefaultOrEmpty)
return;
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
List<MemberDeclarationSyntax> members = [];
foreach (HandlerDeclarationModel handler in handlers)
2025-07-24 23:19:59 +04:00
{
2026-03-06 20:01:54 +04:00
context.CancellationToken.ThrowIfCancellationRequested();
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
if (handler.AttributeName != null && handler.BaseClassName == null)
{
context.ReportDiagnostic(Diagnostic.Create(MissingBaseClassWarning, handler.Location, handler.ClassName, handler.AttributeName));
continue;
}
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
if (handler.AttributeName == null && handler.BaseClassName != null)
2025-07-24 23:19:59 +04:00
{
2026-03-06 20:01:54 +04:00
context.ReportDiagnostic(Diagnostic.Create(MissingAttributeWarning, handler.Location, handler.ClassName, handler.BaseClassName));
continue;
2025-07-24 23:19:59 +04:00
}
2026-03-06 20:01:54 +04:00
if (handler.AttributeName != handler.BaseClassName)
{
context.ReportDiagnostic(Diagnostic.Create(MismatchedHandlerWarning, handler.Location, handler.ClassName, handler.AttributeName, handler.BaseClassName));
continue;
}
2025-07-24 23:19:59 +04:00
2026-03-06 20:01:54 +04:00
FieldDeclarationSyntax fieldDeclaration = GenerateTypeField(handler);
members.Add(fieldDeclaration);
2025-07-24 23:19:59 +04:00
}
2026-03-06 20:01:54 +04:00
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<MemberDeclarationSyntax>(classDeclaration));
CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit()
.WithMembers(SyntaxFactory.SingletonList<MemberDeclarationSyntax>(namespaceDeclaration))
.NormalizeWhitespace();
SourceText sourceText = SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8);
context.AddSource("AnalyzerExport.g.cs", sourceText);
2025-07-24 23:19:59 +04:00
}
2026-03-06 20:01:54 +04:00
private static FieldDeclarationSyntax GenerateTypeField(HandlerDeclarationModel handler)
2025-07-24 23:19:59 +04:00
{
2026-03-06 20:01:54 +04:00
string fullTypeName = handler.Namespace == "Global"
? handler.ClassName
: $"{handler.Namespace}.{handler.ClassName}";
TypeOfExpressionSyntax typeofExpression = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(fullTypeName));
VariableDeclaratorSyntax variableDeclarator = SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier($"{handler.ClassName}Type"))
.WithInitializer(SyntaxFactory.EqualsValueClause(typeofExpression));
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)));
2025-07-24 23:19:59 +04:00
}
}
2026-03-06 20:01:54 +04:00
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<BaseNamespaceDeclarationSyntax>();
return namespaceDeclaration?.Name.ToString() ?? "Global";
}
}