// // PacketSerializerGenerator.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CodeDom.Compiler; using System.Diagnostics; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using NosSmooth.PacketSerializersGenerator.AttributeGenerators; using NosSmooth.PacketSerializersGenerator.Errors; using NosSmooth.PacketSerializersGenerator.Extensions; namespace NosSmooth.PacketSerializersGenerator; /// /// Generates ITypeGenerator for packets that are marked with NosSmooth.Packets.Attributes.GenerateSerializerAttribute. /// /// /// The packets to create serializer for have to be records that specify PacketIndices in the constructor. /// [Generator] public class PacketSerializerGenerator : ISourceGenerator { /// /// Initializes a new instance of the class. /// public PacketSerializerGenerator() { _generators = new List(new[] { new PacketIndexAttributeGenerator() }); } private readonly List _generators; /// public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new PacketClassReceiver()); } /// public void Execute(GeneratorExecutionContext context) { var syntaxReceiver = (PacketClassReceiver)context.SyntaxReceiver!; foreach (var packetClass in syntaxReceiver.PacketClasses) { if (packetClass is not null) { using var stringWriter = new StringWriter(); var writer = new IndentedTextWriter(stringWriter, " "); var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetClass); if (generatedResult is not null) { if (generatedResult is DiagnosticError diagnosticError) { context.ReportDiagnostic(Diagnostic.Create ( new DiagnosticDescriptor ( diagnosticError.Id, diagnosticError.Title, diagnosticError.MessageFormat, "Serialization", DiagnosticSeverity.Error, true ), Location.Create(diagnosticError.Tree, diagnosticError.Span), diagnosticError.Parameters.ToArray() ) ); } else if (generatedResult is not null) { throw new Exception(generatedResult.Message); } continue; } context.AddSource($"{packetClass.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs", stringWriter.GetStringBuilder().ToString()); } } } private IError? GeneratePacketSerializer(IndentedTextWriter textWriter, Compilation compilation, RecordDeclarationSyntax packetClass) { var semanticModel = compilation.GetSemanticModel(packetClass.SyntaxTree); var name = packetClass.Identifier.NormalizeWhitespace().ToFullString(); var @namespace = packetClass.GetPrefix(); var constructor = (ParameterListSyntax?)packetClass.ChildNodes() .FirstOrDefault(x => x.IsKind(SyntaxKind.ParameterList)); if (constructor is null) { return new DiagnosticError( "SG0001", "Packet without constructor", "The packet class {0} does not have any constructors to use for packet serializer.", packetClass.SyntaxTree, packetClass.FullSpan, new List(new[] { packetClass.Identifier.NormalizeWhitespace().ToFullString() }) ); } var parameters = constructor.Parameters; var orderedParameters = new List(); int constructorIndex = 0; foreach (var parameter in parameters) { var attributeLists = parameter.AttributeLists .Where(x => x.Attributes.Any(x => x.Name.NormalizeWhitespace().ToFullString().StartsWith("Packet"))).ToList(); var attributes = attributeLists.FirstOrDefault()?.Attributes.Where(x => x.Name.NormalizeWhitespace().ToFullString().StartsWith("Packet")) .ToList(); if (attributeLists.Count > 1 || (attributes is not null && attributes?.Count > 1)) { return new DiagnosticError ( "SG0002", "Packet constructor parameter with multiple packet attributes", "There are multiple PacketIndexAttributes on {0} parameter in class {1}. Only one may be specified.", parameter.SyntaxTree, parameter.FullSpan, new List(new[] { parameter.Identifier.NormalizeWhitespace().ToFullString(), name }) ); } else if (attributeLists.Count == 0 || attributes is null || attributes.Count < 1) { return new DiagnosticError( "SG0003", "Packet constructor parameter without packet attribute", "Could not find PacketIndexAttribute on {0} parameter in class {1}. Parameters without PacketIndexAttribute aren't allowed.", parameter.SyntaxTree, parameter.FullSpan, new List(new[] { parameter.Identifier.NormalizeWhitespace().ToFullString(), name }) ); } var attribute = attributes.First(); var indexArg = attribute.ArgumentList!.Arguments[0]; var indexExp = indexArg.Expression; var index = ushort.Parse(semanticModel.GetConstantValue(indexExp).ToString()); var namedArguments = new Dictionary(); var arguments = new List(); foreach (var argument in attribute.ArgumentList.Arguments) { var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString(); var expression = argument.Expression; var value = semanticModel.GetConstantValue(expression).Value; if (argumentName is not null) { namedArguments.Add(argumentName, value); } else { arguments.Add(value); } } orderedParameters.Add(new ParameterInfo ( compilation, parameter, attribute, arguments, namedArguments, parameter.Identifier.NormalizeWhitespace().ToFullString(), constructorIndex, index ) ); constructorIndex++; } orderedParameters = orderedParameters.OrderBy(x => x.PacketIndex).ToList(); orderedParameters.Last().IsLast = true; textWriter.WriteLine(@$"// #nullable enable #pragma warning disable 1591 using {@namespace}; using NosSmooth.Packets.Converters; using NosSmooth.Packets.Errors; using NosSmooth.Packets; using Remora.Results; namespace {@namespace}.Generated; public class {name}Converter : BaseTypeConverter<{name}> {{"); textWriter.Indent++; textWriter.WriteLine($@" private readonly ITypeConverterRepository _typeConverterRepository; public {name}Converter(ITypeConverterRepository typeConverterRepository) {{ _typeConverterRepository = typeConverterRepository; }} /// public override Result Serialize({name}? obj, PacketStringBuilder builder) {{ if (obj is null) {{ return new ArgumentNullError(nameof(obj)); }} "); textWriter.Indent++; var serializerError = GenerateSerializer(textWriter, packetClass, orderedParameters); if (serializerError is not null) { return serializerError; } textWriter.Indent--; textWriter.WriteLine($@" }} /// public override Result<{name}?> Deserialize(PacketStringEnumerator stringEnumerator) {{ "); textWriter.Indent++; var deserializerError = GenerateDeserializer(textWriter, packetClass, orderedParameters); if (deserializerError is not null) { return deserializerError; } textWriter.Indent--; textWriter.WriteLine($@" }} private IResultError? CheckDeserializationResult(Result result, string property, PacketStringEnumerator stringEnumerator, bool last = false) {{ if (!result.IsSuccess) {{ return new PacketParameterSerializerError(this, property, result); }} if (!last && (stringEnumerator.IsOnLastToken() ?? false)) {{ return new PacketEndNotExpectedError(this, property); }} return null; }} }}"); return null; } private IError? GenerateSerializer(IndentedTextWriter textWriter, RecordDeclarationSyntax packetClass, List parameters) { foreach (var parameter in parameters) { bool handled = false; foreach (var generator in _generators) { if (generator.ShouldHandle(parameter.Attribute)) { var result = generator.GenerateSerializerPart(textWriter, packetClass, parameter); if (result is not null) { return result; } handled = true; break; } } if (!handled) { throw new InvalidOperationException($"Could not handle {packetClass.Identifier.NormalizeWhitespace().ToFullString()}.{parameter.Name}"); } } textWriter.WriteLine("return Result.FromSuccess();"); return null; } private IError? GenerateDeserializer(IndentedTextWriter textWriter, RecordDeclarationSyntax packetClass, List parameters) { var lastIndex = parameters.FirstOrDefault()?.PacketIndex ?? 0; bool skipped = false; foreach (var parameter in parameters) { var skip = parameter.PacketIndex - lastIndex - 1; if (skip > 0) { if (!skipped) { textWriter.WriteLine("Result skipResult;"); textWriter.WriteLine("IResultError? skipError;"); skipped = true; } textWriter.WriteLine($@"skipResult = stringEnumerator.GetNextToken();"); textWriter.WriteLine($@"skipError = CheckDeserializationResult(result, ""None"", stringEnumerator, false);"); textWriter.WriteLine($@"if (skipError is not null) {{ return Result<{packetClass.Identifier.NormalizeWhitespace().ToFullString()}>.FromError(skipError, skipResult); }}"); } bool handled = false; foreach (var generator in _generators) { if (generator.ShouldHandle(parameter.Attribute)) { var result = generator.GenerateDeserializerPart(textWriter, packetClass, parameter); if (result is not null) { return result; } handled = true; break; } } if (!handled) { throw new InvalidOperationException($"Could not handle {packetClass.Identifier.NormalizeWhitespace().ToFullString()}.{parameter.Name}"); } lastIndex = parameter.PacketIndex; } string parametersString = string.Join(", ", parameters.OrderBy(x => x.ConstructorIndex).Select(x => x.Name)); textWriter.WriteLine($"return new {packetClass.Identifier.NormalizeWhitespace().ToFullString()}({parametersString});"); return null; } }