399 lines
18 KiB
C#
399 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
[Generator]
|
|
public class StateMachineGenerator : IIncrementalGenerator
|
|
{
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
// 1. Собираем все типы с атрибутом [StateMachine]
|
|
var stateMachineTypes = context.SyntaxProvider
|
|
.CreateSyntaxProvider(
|
|
predicate: static (s, _) => IsStateMachineCandidate(s),
|
|
transform: static (ctx, _) => GetStateMachineType(ctx))
|
|
.Where(static m => m is not null)
|
|
.Collect();
|
|
|
|
// 2. Собираем все типы с атрибутом [State]
|
|
var stateTypes = context.SyntaxProvider
|
|
.CreateSyntaxProvider(
|
|
predicate: static (s, _) => IsStateCandidate(s),
|
|
transform: static (ctx, _) => GetStateType(ctx))
|
|
.Where(static m => m is not null)
|
|
.Collect();
|
|
|
|
// 3. Получаем компиляцию для семантического анализа
|
|
var compilation = context.CompilationProvider;
|
|
|
|
// 4. Объединяем все данные
|
|
var combinedData = stateMachineTypes.Combine(stateTypes).Combine(compilation);
|
|
|
|
// 5. Регистрируем генерацию кода
|
|
context.RegisterSourceOutput(combinedData, static (spc, source) =>
|
|
Execute(spc, source.Left.Left, source.Left.Right, source.Right));
|
|
}
|
|
|
|
private static bool IsStateMachineCandidate(SyntaxNode node)
|
|
{
|
|
return node is ClassDeclarationSyntax classDecl &&
|
|
classDecl.AttributeLists.Count > 0;
|
|
}
|
|
|
|
private static bool IsStateCandidate(SyntaxNode node)
|
|
{
|
|
return node is ClassDeclarationSyntax classDecl &&
|
|
classDecl.AttributeLists.Count > 0;
|
|
}
|
|
|
|
private static INamedTypeSymbol? GetStateMachineType(GeneratorSyntaxContext context)
|
|
{
|
|
var classDecl = (ClassDeclarationSyntax)context.Node;
|
|
var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDecl);
|
|
|
|
if (classSymbol == null) return null;
|
|
|
|
// Проверяем наличие атрибута [StateMachine]
|
|
var stateMachineAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName("StateMachine");
|
|
if (stateMachineAttribute == null) return null;
|
|
|
|
return classSymbol.GetAttributes()
|
|
.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, stateMachineAttribute))
|
|
? classSymbol
|
|
: null;
|
|
}
|
|
|
|
private static INamedTypeSymbol? GetStateType(GeneratorSyntaxContext context)
|
|
{
|
|
var classDecl = (ClassDeclarationSyntax)context.Node;
|
|
var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDecl);
|
|
|
|
if (classSymbol == null) return null;
|
|
|
|
// Проверяем наличие атрибута [State]
|
|
var stateAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName("State");
|
|
if (stateAttribute == null) return null;
|
|
|
|
return classSymbol.GetAttributes()
|
|
.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, stateAttribute))
|
|
? classSymbol
|
|
: null;
|
|
}
|
|
|
|
private static void Execute(
|
|
SourceProductionContext context,
|
|
ImmutableArray<INamedTypeSymbol> stateMachineTypes,
|
|
ImmutableArray<INamedTypeSymbol> stateTypes,
|
|
Compilation compilation)
|
|
{
|
|
if (stateMachineTypes.IsDefaultOrEmpty && stateTypes.IsDefaultOrEmpty)
|
|
return;
|
|
|
|
var validStateMachines = stateMachineTypes.Where(t => t != null).ToList();
|
|
var validStates = stateTypes.Where(t => t != null).ToList();
|
|
|
|
if (!validStateMachines.Any() && !validStates.Any())
|
|
return;
|
|
|
|
// Получаем символы атрибутов
|
|
var stateMachineAttributeSymbol = compilation.GetTypeByMetadataName("StateMachine");
|
|
var stateAttributeSymbol = compilation.GetTypeByMetadataName("State");
|
|
var stateRaisingEventAttributeSymbol = compilation.GetTypeByMetadataName("StateRaisingEvent");
|
|
|
|
if (stateMachineAttributeSymbol == null || stateAttributeSymbol == null)
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
new DiagnosticDescriptor("SMG001", "Missing attributes",
|
|
"StateMachine or State attributes not found", "StateMachine", DiagnosticSeverity.Error, true),
|
|
Location.None));
|
|
return;
|
|
}
|
|
|
|
// Генерируем код для StateMachine и States
|
|
GenerateStateClasses(context, validStates, stateAttributeSymbol, stateRaisingEventAttributeSymbol, compilation);
|
|
GenerateStateMachineClasses(context, validStateMachines, validStates, stateMachineAttributeSymbol, stateAttributeSymbol, compilation);
|
|
}
|
|
|
|
private static void GenerateStateMachineClasses(
|
|
SourceProductionContext context,
|
|
List<INamedTypeSymbol> stateMachines,
|
|
List<INamedTypeSymbol> allStates,
|
|
INamedTypeSymbol stateMachineAttributeSymbol,
|
|
INamedTypeSymbol stateAttributeSymbol,
|
|
Compilation compilation)
|
|
{
|
|
// if (allStates.Count == 0) throw new Exception("RETARDED");
|
|
foreach (var sm in stateMachines)
|
|
{
|
|
// Получаем параметры атрибута [StateMachine]
|
|
var smAttribute = sm.GetAttributes()
|
|
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, stateMachineAttributeSymbol));
|
|
|
|
if (smAttribute == null) continue;
|
|
|
|
var stateMachineContextType = GetNamedTypeArgument(smAttribute, "StateMachineContextType");
|
|
var stateMachineEnumType = GetNamedTypeArgument(smAttribute, "StateMachineEnumType");
|
|
|
|
if (stateMachineContextType == null || stateMachineEnumType == null)
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
new DiagnosticDescriptor("SMG002", "Invalid StateMachine attribute",
|
|
$"StateMachine {sm.Name} has invalid attribute parameters", "StateMachine", DiagnosticSeverity.Error, true),
|
|
sm.Locations.FirstOrDefault()));
|
|
continue;
|
|
}
|
|
|
|
// Находим все states для этого state machine
|
|
var statesForThisMachine = allStates
|
|
.Where(st =>
|
|
{
|
|
var stAttribute = st.GetAttributes()
|
|
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, stateAttributeSymbol));
|
|
|
|
if (stAttribute == null) return false;
|
|
|
|
var stContextType = GetNamedTypeArgument(stAttribute, "StateMachineEnumType"); // БЛЯТЬ ИДИ НАХУЙ ПИДАРАС ТРАНСВИСТИТНЫЙ ПИЗДУЙ В ТАЙЛАНД
|
|
// Сравниваем по полному имени типа, как в оригинальном коде
|
|
return stContextType != null &&
|
|
stContextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ==
|
|
stateMachineContextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
})
|
|
.ToList();
|
|
|
|
// Собираем пространства имен для states
|
|
var stateNamespaces = new HashSet<string>();
|
|
foreach (var st in statesForThisMachine)
|
|
{
|
|
if (st.ContainingNamespace != null && !st.ContainingNamespace.IsGlobalNamespace)
|
|
{
|
|
stateNamespaces.Add(st.ContainingNamespace.ToDisplayString());
|
|
}
|
|
}
|
|
|
|
// Отладка: проверяем, найдены ли стейты
|
|
if (statesForThisMachine.Count == 0)
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
new DiagnosticDescriptor("SMG004", "No states found",
|
|
$"No states found for StateMachine {sm.Name} with context {stateMachineContextType.Name}",
|
|
"StateMachine", DiagnosticSeverity.Warning, true),
|
|
sm.Locations.FirstOrDefault()));
|
|
}
|
|
|
|
// Генерируем код для StateMachine
|
|
var stateMachineCode = GenerateStateMachineClassCode(
|
|
sm, stateMachineContextType, stateMachineEnumType, statesForThisMachine, stateNamespaces);
|
|
|
|
var fileName = $"{sm.ContainingNamespace?.ToDisplayString() ?? ""}.{sm.Name}.StateMachine.Generated.cs"
|
|
.Replace("<", "_").Replace(">", "_").Replace("+", "_");
|
|
|
|
context.AddSource(fileName, SourceText.From(stateMachineCode, Encoding.UTF8));
|
|
}
|
|
}
|
|
|
|
private static void GenerateStateClasses(
|
|
SourceProductionContext context,
|
|
List<INamedTypeSymbol> states,
|
|
INamedTypeSymbol stateAttributeSymbol,
|
|
INamedTypeSymbol? stateRaisingEventAttributeSymbol,
|
|
Compilation compilation)
|
|
{
|
|
foreach (var st in states)
|
|
{
|
|
var stateAttribute = st.GetAttributes()
|
|
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, stateAttributeSymbol));
|
|
|
|
if (stateAttribute == null) continue;
|
|
|
|
var stateEnumType = GetNamedTypeArgument(stateAttribute, "StateEnum");
|
|
var stateMachineContextType = GetNamedTypeArgument(stateAttribute, "StateMachineContext");
|
|
var isSilent = GetBooleanArgument(stateAttribute, "SilentBitch");
|
|
var hasRaisingEvent = stateRaisingEventAttributeSymbol != null &&
|
|
st.HasAttribute(stateRaisingEventAttributeSymbol);
|
|
|
|
if (stateEnumType == null || stateMachineContextType == null)
|
|
{
|
|
context.ReportDiagnostic(Diagnostic.Create(
|
|
new DiagnosticDescriptor("SMG003", "Invalid State attribute",
|
|
$"State {st.Name} has invalid attribute parameters", "StateMachine", DiagnosticSeverity.Error, true),
|
|
st.Locations.FirstOrDefault()));
|
|
continue;
|
|
}
|
|
|
|
// Генерируем код для State
|
|
var stateCode = GenerateStateClassCode(
|
|
st, stateEnumType, stateMachineContextType, isSilent, hasRaisingEvent);
|
|
|
|
var fileName = $"{st.ContainingNamespace?.ToDisplayString() ?? ""}.{st.Name}.State.Generated.cs"
|
|
.Replace("<", "_").Replace(">", "_").Replace("+", "_");
|
|
|
|
context.AddSource(fileName, SourceText.From(stateCode, Encoding.UTF8));
|
|
}
|
|
}
|
|
|
|
private static string GenerateStateMachineClassCode(
|
|
INamedTypeSymbol sm,
|
|
INamedTypeSymbol contextType,
|
|
INamedTypeSymbol enumType,
|
|
List<INamedTypeSymbol> states,
|
|
HashSet<string> stateNamespaces)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("// <auto-generated/>");
|
|
sb.AppendLine("#pragma warning disable CS0105");
|
|
sb.AppendLine("using Code.Infrastructure.StateMachine;");
|
|
|
|
// Добавляем using для всех пространств имен states
|
|
foreach (var ns in stateNamespaces.OrderBy(n => n))
|
|
{
|
|
if (!string.IsNullOrEmpty(ns) && ns != sm.ContainingNamespace?.ToDisplayString())
|
|
{
|
|
sb.AppendLine($"using {ns};");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine("#pragma warning restore CS0105");
|
|
sb.AppendLine();
|
|
|
|
if (sm.ContainingNamespace != null && !sm.ContainingNamespace.IsGlobalNamespace)
|
|
{
|
|
sb.AppendLine($"namespace {sm.ContainingNamespace.ToDisplayString()}");
|
|
sb.AppendLine("{");
|
|
}
|
|
|
|
sb.AppendLine($"public partial class {sm.Name} : AbstractStateMachine<{enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}, {contextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>");
|
|
sb.AppendLine("{");
|
|
|
|
// Конструктор с добавлением всех стейтов
|
|
sb.AppendLine($" public {sm.Name}({contextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} context, EventBus.EventBus eventBus) : base(context)");
|
|
sb.AppendLine(" {");
|
|
|
|
if (states.Count == 0) sb.AppendLine("//I am retarded");
|
|
foreach (var state in states)
|
|
{
|
|
sb.AppendLine($" var state{state.Name} = new {state.Name}(StateMachineContext, eventBus);");
|
|
sb.AppendLine($" States.Add(state{state.Name}.myState, state{state.Name});");
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine($" protected override bool CheckTransitionAllowed({enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} nextState) => AllowedStates.HasFlag(nextState);");
|
|
sb.AppendLine(" protected override void ResetAllowedTransitions() => AllowedStates = 0;");
|
|
sb.AppendLine("}");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"public static class {sm.Name}Extensions");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine($" public static AbstractStateMachine<{enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}, {contextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}> AllowStateTransition(");
|
|
sb.AppendLine($" this AbstractStateMachine<{enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}, {contextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}> stateMachine, {enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} state)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" if (stateMachine == null) return null;");
|
|
sb.AppendLine(" stateMachine.AllowedStates |= state;");
|
|
sb.AppendLine(" return stateMachine;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
|
|
if (sm.ContainingNamespace != null && !sm.ContainingNamespace.IsGlobalNamespace)
|
|
{
|
|
sb.AppendLine("}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string GenerateStateClassCode(
|
|
INamedTypeSymbol state,
|
|
INamedTypeSymbol stateEnumType,
|
|
INamedTypeSymbol contextType,
|
|
bool isSilent,
|
|
bool hasRaisingEvent)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("// <auto-generated/>");
|
|
sb.AppendLine("using Code.Infrastructure.StateMachine;");
|
|
sb.AppendLine();
|
|
|
|
if (state.ContainingNamespace != null && !state.ContainingNamespace.IsGlobalNamespace)
|
|
{
|
|
sb.AppendLine($"namespace {state.ContainingNamespace.ToDisplayString()}");
|
|
sb.AppendLine("{");
|
|
}
|
|
|
|
var fireStatesCode = "";
|
|
if (hasRaisingEvent)
|
|
{
|
|
fireStatesCode = $@"
|
|
protected override void _fireEnterState()
|
|
{{
|
|
EventBus.RaiseEvent<IEnterStateHandle<{state.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>>(h => h.EnterState());
|
|
}}
|
|
|
|
protected override void _fireExitState()
|
|
{{
|
|
EventBus.RaiseEvent<IExitStateHandle<{state.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>>(h => h.ExitState());
|
|
}}
|
|
";
|
|
}
|
|
|
|
var genetics = "";
|
|
if (!isSilent)
|
|
{
|
|
// ИСПРАВЛЕНО: Поменяны местами аргументы шаблона - сначала контекст, потом enum
|
|
genetics = $@" : BaseState<{contextType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}, {stateEnumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>";
|
|
}
|
|
|
|
sb.AppendLine($"public partial class {state.Name}{genetics}");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine(fireStatesCode);
|
|
sb.AppendLine("}");
|
|
|
|
if (state.ContainingNamespace != null && !state.ContainingNamespace.IsGlobalNamespace)
|
|
{
|
|
sb.AppendLine("}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static INamedTypeSymbol? GetNamedTypeArgument(AttributeData attribute, string argumentName)
|
|
{
|
|
// Проверяем именованные аргументы
|
|
foreach (var namedArg in attribute.NamedArguments)
|
|
{
|
|
if (namedArg.Key == argumentName && namedArg.Value.Value is INamedTypeSymbol namedType)
|
|
return namedType;
|
|
}
|
|
|
|
// Проверяем конструкторные аргументы
|
|
var constructorArgs = attribute.ConstructorArguments;
|
|
if (constructorArgs.Length >= 2 && constructorArgs[0].Value is INamedTypeSymbol firstType &&
|
|
constructorArgs[1].Value is INamedTypeSymbol secondType)
|
|
{
|
|
// Для StateMachineAttribute и StateAttribute порядок параметров в конструкторе:
|
|
// [StateMachine(typeof(ContextType), typeof(EnumType))]
|
|
// [State(typeof(ContextType), typeof(EnumType))]
|
|
if (argumentName == "StateMachineContextType" || argumentName == "StateMachineContext")
|
|
return firstType;
|
|
if (argumentName == "StateMachineEnumType" || argumentName == "StateEnum")
|
|
return secondType;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool GetBooleanArgument(AttributeData attribute, string argumentName)
|
|
{
|
|
foreach (var namedArg in attribute.NamedArguments)
|
|
{
|
|
if (namedArg.Key == argumentName && namedArg.Value.Value is bool boolValue)
|
|
return boolValue;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|