From 661f4e96df3c03e74668eaefbd71b142a7d54a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Fri, 31 Dec 2021 00:31:19 +0100 Subject: [PATCH] feat: rewrite packet serializer generator --- .../IParameterGenerator.cs | 15 +- .../PacketContextListAttributeGenerator.cs | 110 ++--- .../PacketIndexAttributeGenerator.cs | 93 ++--- .../PacketListIndexAttributeGenerator.cs | 116 ++---- .../Constants.cs | 23 ++ .../ConverterDeserializationGenerator.cs | 135 +++++++ .../ConverterSerializationGenerator.cs | 93 +++++ .../Data/AttributeInfo.cs | 24 ++ .../Data/PacketInfo.cs | 29 ++ .../{ => Data}/ParameterInfo.cs | 26 +- .../Data/Parameters.cs | 57 +++ .../AttributeArgumentSyntaxExtensions.cs | 33 ++ .../Extensions/AttributeInfoExtensions.cs | 58 +++ .../AttributeListSyntaxExtensions.cs | 29 ++ .../IndentedTextWriterExtensions.cs | 28 ++ .../Extensions/ParameterInfoExtensions.cs | 79 ++++ .../PacketClassReceiver.cs | 50 --- .../PacketConverterGenerator.cs | 245 ++++++++++++ .../PacketSerializerGenerator.cs | 375 ------------------ .../SourceGenerator.cs | 278 +++++++++++++ 20 files changed, 1248 insertions(+), 648 deletions(-) create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Constants.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs rename Core/NosSmooth.PacketSerializersGenerator/{ => Data}/ParameterInfo.cs (50%) create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs delete mode 100644 Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs delete mode 100644 Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs create mode 100644 Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs diff --git a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs index 7fb1048c7d5accdf824f54e1a894434b1dd54654..ffefb513b36c361676d2ebfc24bf004d4357a522 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs @@ -6,6 +6,7 @@ using System.CodeDom.Compiler; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators @@ -18,26 +19,24 @@ namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators /// /// Check whether this generator should handle parameter with this attribute. /// - /// The parameters attribute. + /// The parameter. /// Whether to handle this parameter. - public bool ShouldHandle(AttributeSyntax attribute); + public bool ShouldHandle(ParameterInfo parameter); /// /// Generate part for the Serializer method to serialize the given parameter. /// /// The text writer to write the code to. - /// The packet record declaration syntax. - /// The parameter info to generate for. + /// The packet info to generate for. /// The generated source code. - public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo); + public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo); /// /// Generate part for the Deserializer method to deserialize the given parameter. /// /// The text writer to write the code to. - /// The packet record declaration syntax. - /// The parameter info to generate for. + /// The packet info to generate for. /// The generated source code. - public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo); + public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo); } } \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs index c5425d91f74dc1920eff1c5808d8742d9df1ce92..59ed39d23aeace35bab9ef20c7c81b53bc2eaf78 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs @@ -8,108 +8,80 @@ using System.CodeDom.Compiler; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; +using NosSmooth.PacketSerializersGenerator.Extensions; namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators; /// public class PacketContextListAttributeGenerator : IParameterGenerator { + /// + /// Gets the full name of the packet index attribute. + /// + public static string PacketListIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketContextListAttribute"; + /// - public bool ShouldHandle(AttributeSyntax attribute) - => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketContextList"; + public bool ShouldHandle(ParameterInfo parameter) + => parameter.Attributes.Any(x => x.FullName == PacketListIndexAttributeFullName); /// - public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) - { - textWriter.WriteLine($"builder.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); - } + var generator = new ConverterSerializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(x => x.FullName == PacketListIndexAttributeFullName); - var listSeparator = '|'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("ListSeparator") && parameterInfo.NamedAttributeArguments["ListSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - listSeparator = parameterInfo.NamedAttributeArguments["ListSeparator"]!.ToString()[0]; + generator.SetAfterSeparatorOnce((char)afterSeparator); } - textWriter.WriteLine($"builder.PushLevel('{listSeparator}')"); - - var innerSeparator = '.'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) - { - innerSeparator = parameterInfo.NamedAttributeArguments["InnerSeparator"]!.ToString()[0]; - textWriter.WriteLine($"builder.PushLevel('{parameterInfo.NamedAttributeArguments["InnerSeparator"]}');"); - } + var listSeparator = attribute.GetNamedValue("ListSeparator", '|'); + generator.PushLevel(listSeparator); - textWriter.WriteLine($"builder.PrepareLevel('{innerSeparator}')"); + var innerSeparator = attribute.GetNamedValue("InnerSeparator", '.'); + generator.PrepareLevel(innerSeparator); - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Serialize(obj.{parameterInfo.Name}, builder); -if (!{parameterInfo.Name}Result.IsSuccess) -{{ - return Result.FromError(new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result), {parameterInfo.Name}Result); -}} - -builder.RemovePreparedLevel(); -builder.PopLevel(); -"); + generator.SerializeAndCheck(parameter); + generator.RemovePreparedLevel(); + generator.PopLevel(); return null; } /// - public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { - bool nullable = parameterInfo.Parameter.Type is NullableTypeSyntax; + var generator = new ConverterDeserializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(x => x.FullName == PacketListIndexAttributeFullName); - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - textWriter.WriteLine($"stringEnumerator.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); + generator.SetAfterSeparatorOnce((char)afterSeparator); } - var listSeparator = '|'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("ListSeparator") && parameterInfo.NamedAttributeArguments["ListSeparator"] is not null) - { - listSeparator = parameterInfo.NamedAttributeArguments["ListSeparator"]!.ToString()[0]; - } + var listSeparator = attribute.GetNamedValue("ListSeparator", '|'); + var lengthVariable = attribute.GetIndexedValue(1); + textWriter.WriteLine(@$"stringEnumerator.PushLevel(""{listSeparator}"", {lengthVariable});"); - textWriter.WriteLine($"stringEnumerator.PushLevel('{listSeparator}');"); + var innerSeparator = attribute.GetNamedValue("InnerSeparator", '.'); + generator.PrepareLevel(innerSeparator); - var innerSeparator = '.'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) + generator.DeserializeAndCheck($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast); + generator.RemovePreparedLevel(); + generator.PopLevel(); + if (!parameter.Nullable) { - innerSeparator = parameterInfo.NamedAttributeArguments["InnerSeparator"]!.ToString()[0]; + generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name); } - var maxTokensVariable = parameterInfo.IndexedAttributeArguments[1]!.ToString(); - - textWriter.WriteLine($"stringEnumerator.PrepareLevel('{innerSeparator}', {maxTokensVariable});"); - - var semanticModel = parameterInfo.Compilation.GetSemanticModel(recordDeclarationSyntax.SyntaxTree); - var type = semanticModel.GetTypeInfo(parameterInfo.Parameter.Type!).Type; - string last = parameterInfo.IsLast ? "true" : "false"; - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Deserialize<{type!.ToString()}{(nullable ? string.Empty : "?")}>(stringEnumerator); -var {parameterInfo.Name}Error = CheckDeserializationResult({parameterInfo.Name}Result, ""{parameterInfo.Name}"", stringEnumerator, {last}); -if ({parameterInfo.Name}Error is not null) -{{ - return Result<{recordDeclarationSyntax.Identifier.NormalizeWhitespace().ToFullString()}?>.FromError({parameterInfo.Name}Error, {parameterInfo.Name}Result); -}} - -stringEnumerator.RemovePreparedLevel(); -stringEnumerator.PopLevel(); -"); - if (!nullable) - { - textWriter.WriteLine($@" -if ({parameterInfo.Name}Result.Entity is null) {{ - return new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result, ""The converter has returned null even though it was not expected.""); -}} -"); - } + generator.AssignLocalVariable(parameter); - textWriter.WriteLine($"var {parameterInfo.Name} = ({type.ToString()}){parameterInfo.Name}Result.Entity;"); return null; } } \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs index 05d7771104ee4ad0123ceeda17ea39988feca474..92003c84aa7163b334a5d98240f1c19af21cba5e 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs @@ -5,106 +5,89 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CodeDom.Compiler; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; +using NosSmooth.PacketSerializersGenerator.Extensions; namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators; /// public class PacketIndexAttributeGenerator : IParameterGenerator { + /// + /// Gets the full name of the packet index attribute. + /// + public static string PacketIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketIndexAttribute"; + /// - public bool ShouldHandle(AttributeSyntax attribute) - => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketIndex"; + public bool ShouldHandle(ParameterInfo parameter) + => parameter.Attributes.Any(x => x.FullName == PacketIndexAttributeFullName); /// - public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { bool pushedLevel = false; + var generator = new ConverterSerializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(x => x.FullName == PacketIndexAttributeFullName); - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - textWriter.WriteLine($"builder.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); + generator.SetAfterSeparatorOnce((char)afterSeparator); } - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) + var innerSeparator = attribute.GetNamedValue("InnerSeparator", null); + if (innerSeparator is not null) { + generator.PushLevel((char)innerSeparator); pushedLevel = true; - textWriter.WriteLine($"builder.PushLevel('{parameterInfo.NamedAttributeArguments["InnerSeparator"]}');"); } - var semanticModel = parameterInfo.Compilation.GetSemanticModel(recordDeclarationSyntax.SyntaxTree); - var type = semanticModel.GetTypeInfo(parameterInfo.Parameter.Type!).Type; - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Serialize<{type}>(obj.{parameterInfo.Name}, builder); -if (!{parameterInfo.Name}Result.IsSuccess) -{{ - return Result.FromError(new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result), {parameterInfo.Name}Result); -}} -"); + generator.SerializeAndCheck(parameter); if (pushedLevel) { - textWriter.WriteLine("builder.PopLevel();"); + generator.PopLevel(); } return null; } /// - public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { bool pushedLevel = false; - bool nullable = parameterInfo.Parameter.Type is NullableTypeSyntax; + var generator = new ConverterDeserializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(); - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - textWriter.WriteLine($"stringEnumerator.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); + generator.SetAfterSeparatorOnce((char)afterSeparator); } - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) + var innerSeparator = attribute.GetNamedValue("InnerSeparator", null); + if (innerSeparator is not null) { + generator.PushLevel((char)innerSeparator); pushedLevel = true; - textWriter.WriteLine($"stringEnumerator.PushLevel('{parameterInfo.NamedAttributeArguments["InnerSeparator"]}');"); } - var semanticModel = parameterInfo.Compilation.GetSemanticModel(recordDeclarationSyntax.SyntaxTree); - var type = semanticModel.GetTypeInfo(parameterInfo.Parameter.Type!).Type; - string last = parameterInfo.IsLast ? "true" : "false"; - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Deserialize<{type!.ToString().TrimEnd('?')}?>(stringEnumerator); -var {parameterInfo.Name}Error = CheckDeserializationResult({parameterInfo.Name}Result, ""{parameterInfo.Name}"", stringEnumerator, {last}); -if ({parameterInfo.Name}Error is not null) -{{ - return Result<{recordDeclarationSyntax.Identifier.NormalizeWhitespace().ToFullString()}?>.FromError({parameterInfo.Name}Error, {parameterInfo.Name}Result); -}} -"); - - if (!nullable) + generator.DeserializeAndCheck($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast); + + if (!parameter.Nullable) { - textWriter.WriteLine($@" -if ({parameterInfo.Name}Result.Entity is null) {{ - return new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result, ""The converter has returned null even though it was not expected.""); -}} -"); + generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name); } - textWriter.WriteLine($"var {parameterInfo.Name} = ({type!.ToString().TrimEnd('?')}{(nullable ? "?" : string.Empty)}){parameterInfo.Name}Result.Entity;"); + generator.AssignLocalVariable(parameter); if (pushedLevel) { - // If we know that we are not on the last token in the item level, just skip to the end of the item. - // Note that if this is the case, then that means the converter is either corrupted - // or the packet has more fields. - textWriter.WriteLine($@" -while (stringEnumerator.IsOnLastToken() == false) -{{ - stringEnumerator.GetNextToken(); -}} -"); - textWriter.WriteLine("stringEnumerator.PopLevel();"); + generator.ReadToLastToken(); + generator.PopLevel(); } return null; diff --git a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs index 847f1e74a9062b625d5aabcc90637bc96e4f35fc..ca6254d10029b107b6e245fc61365596053b11d8 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs @@ -9,112 +9,80 @@ using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; +using NosSmooth.PacketSerializersGenerator.Extensions; +using ParameterInfo = NosSmooth.PacketSerializersGenerator.Data.ParameterInfo; namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators; /// public class PacketListIndexAttributeGenerator : IParameterGenerator { + /// + /// Gets the full name of the packet index attribute. + /// + public static string PacketListIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketListIndex"; + /// - public bool ShouldHandle(AttributeSyntax attribute) - => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketListIndex"; + public bool ShouldHandle(ParameterInfo parameter) + => parameter.Attributes.Any(x => x.FullName == PacketListIndexAttributeFullName); /// - public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) - { - textWriter.WriteLine($"builder.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); - } + var generator = new ConverterSerializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(x => x.FullName == PacketListIndexAttributeFullName); - var listSeparator = '|'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("ListSeparator") && parameterInfo.NamedAttributeArguments["ListSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - listSeparator = parameterInfo.NamedAttributeArguments["ListSeparator"]!.ToString()[0]; + generator.SetAfterSeparatorOnce((char)afterSeparator); } - textWriter.WriteLine($"builder.PushLevel('{listSeparator}');"); + var listSeparator = attribute.GetNamedValue("ListSeparator", '|'); + generator.PushLevel(listSeparator); - var innerSeparator = '.'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) - { - innerSeparator = parameterInfo.NamedAttributeArguments["InnerSeparator"]!.ToString()[0]; - } + var innerSeparator = attribute.GetNamedValue("InnerSeparator", '.'); + generator.PrepareLevel(innerSeparator); - textWriter.WriteLine($"builder.PrepareLevel('{innerSeparator}');"); - - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Serialize(obj.{parameterInfo.Name}, builder); -if (!{parameterInfo.Name}Result.IsSuccess) -{{ - return Result.FromError(new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result), {parameterInfo.Name}Result); -}} - -builder.RemovePreparedLevel(); -builder.PopLevel(); -"); + generator.SerializeAndCheck(parameter); + generator.RemovePreparedLevel(); + generator.PopLevel(); return null; } /// - public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo) + public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo) { - bool nullable = parameterInfo.Parameter.Type is NullableTypeSyntax; - - if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null) - { - textWriter.WriteLine($"stringEnumerator.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');"); - } - - var listSeparator = '|'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("ListSeparator") && parameterInfo.NamedAttributeArguments["ListSeparator"] is not null) - { - listSeparator = parameterInfo.NamedAttributeArguments["ListSeparator"]!.ToString()[0]; - } - - textWriter.WriteLine($"stringEnumerator.PushLevel('{listSeparator}');"); + var generator = new ConverterDeserializationGenerator(textWriter); + var parameter = packetInfo.Parameters.Current; + var attribute = parameter.Attributes.First(x => x.FullName == PacketListIndexAttributeFullName); - var innerSeparator = '.'; - if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null) + var afterSeparator = attribute.GetNamedValue("AfterSeparator", null); + if (afterSeparator is not null) { - innerSeparator = parameterInfo.NamedAttributeArguments["InnerSeparator"]!.ToString()[0]; + generator.SetAfterSeparatorOnce((char)afterSeparator); } - var maxTokens = "null"; - if (parameterInfo.NamedAttributeArguments.ContainsKey("Length") && parameterInfo.NamedAttributeArguments["Length"] is not null) - { - maxTokens = parameterInfo.NamedAttributeArguments["Length"]!.ToString(); - } - - textWriter.WriteLine($"stringEnumerator.PrepareLevel('{innerSeparator}', {maxTokens ?? "null"});"); - - var semanticModel = parameterInfo.Compilation.GetSemanticModel(recordDeclarationSyntax.SyntaxTree); - var type = semanticModel.GetTypeInfo(parameterInfo.Parameter.Type!).Type; - string last = parameterInfo.IsLast ? "true" : "false"; - textWriter.WriteLine($@" -var {parameterInfo.Name}Result = _typeConverterRepository.Deserialize<{type!.ToString()}{(nullable ? string.Empty : "?")}>(stringEnumerator); -var {parameterInfo.Name}Error = CheckDeserializationResult({parameterInfo.Name}Result, ""{parameterInfo.Name}"", stringEnumerator, {last}); -if ({parameterInfo.Name}Error is not null) -{{ - return Result<{recordDeclarationSyntax.Identifier.NormalizeWhitespace().ToFullString()}?>.FromError({parameterInfo.Name}Error, {parameterInfo.Name}Result); -}} + var listSeparator = attribute.GetNamedValue("ListSeparator", '|'); + var length = attribute.GetNamedValue("Length", 0); + generator.PushLevel(listSeparator, length != 0 ? (uint?)length : (uint?)null); + var innerSeparator = attribute.GetNamedValue("InnerSeparator", '.'); + generator.PrepareLevel(innerSeparator); -stringEnumerator.RemovePreparedLevel(); -stringEnumerator.PopLevel(); -"); - if (!nullable) + generator.DeserializeAndCheck($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast); + generator.RemovePreparedLevel(); + generator.PopLevel(); + if (!parameter.Nullable) { - textWriter.WriteLine($@" -if ({parameterInfo.Name}Result.Entity is null) {{ - return new PacketParameterSerializerError(this, ""{parameterInfo.Name}"", {parameterInfo.Name}Result, ""The converter has returned null even though it was not expected.""); -}} -"); + generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name); } - textWriter.WriteLine($"var {parameterInfo.Name} = ({type.ToString()}){parameterInfo.Name}Result.Entity;"); + generator.AssignLocalVariable(parameter); return null; } diff --git a/Core/NosSmooth.PacketSerializersGenerator/Constants.cs b/Core/NosSmooth.PacketSerializersGenerator/Constants.cs new file mode 100644 index 0000000000000000000000000000000000000000..cb4960fe3b0ff7da1e04b22899e9368fcd4184df --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Constants.cs @@ -0,0 +1,23 @@ +// +// Constants.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. + +namespace NosSmooth.PacketSerializersGenerator; + +/// +/// Contains constants needed for the generation. +/// +public class Constants +{ + /// + /// Gets the full name of the generate source attribute class. + /// + public static string GenerateSourceAttributeClass => "NosSmooth.Packets.Attributes.GenerateSerializerAttribute"; + + /// + /// Gets the full name of the packet attribute classes that are used for the generation. + /// + public static string PacketAttributesClassRegex => @"^NosSmooth\.Packets\.Attributes\.Packet.*"; +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..9dff5ad083a267e20974458dd2f9109904395e1e --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs @@ -0,0 +1,135 @@ +// +// ConverterDeserializationGenerator.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 NosSmooth.PacketSerializersGenerator.Data; +using NosSmooth.PacketSerializersGenerator.Extensions; + +namespace NosSmooth.PacketSerializersGenerator; + +/// +/// Various templates for converter deserialization. +/// +public class ConverterDeserializationGenerator +{ + private readonly string _stringEnumeratorVariable = "stringEnumerator"; + private readonly IndentedTextWriter _textWriter; + + /// + /// Initializes a new instance of the class. + /// + /// The text writer. + public ConverterDeserializationGenerator(IndentedTextWriter textWriter) + { + _textWriter = textWriter; + } + + /// + /// Push level to the string enumerator. + /// + /// The separator. + public void SetAfterSeparatorOnce(char separator) + { + _textWriter.WriteLine(@$"{_stringEnumeratorVariable}.SetAfterSeparatorOnce(""{separator}"");"); + } + + /// + /// Push level to the string enumerator. + /// + /// The separator. + /// The maximum number of tokens to read. + public void PushLevel(char separator, uint? maxTokens = default) + { + _textWriter.WriteLine(@$"{_stringEnumeratorVariable}.PushLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});"); + } + + /// + /// Pop level from the string enumerator. + /// + public void PopLevel() + { + _textWriter.WriteLine($"{_stringEnumeratorVariable}.PopLevel();"); + } + + /// + /// Prepare the level to the string enumerator. + /// + /// The separator. + /// The maximum number of tokens to read. + public void PrepareLevel(char separator, uint? maxTokens = default) + { + _textWriter.WriteLine($@"{_stringEnumeratorVariable}.PrepareLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});"); + } + + /// + /// Prepare the level to the string enumerator. + /// + public void RemovePreparedLevel() + { + _textWriter.WriteLine($@"{_stringEnumeratorVariable}.RemovePreparedLevel();"); + } + + /// + /// Try to read to the last token of the level. + /// + /// + /// If we know that we are not on the last token in the item level, just skip to the end of the item. + /// Note that if this is the case, then that means the converter is either corrupted + /// or the packet has more fields. + /// + public void ReadToLastToken() + { + _textWriter.WriteLine($@"while ({_stringEnumeratorVariable}.IsOnLastToken() == false)"); + _textWriter.WriteLine("{"); + _textWriter.Indent++; + _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken();"); + _textWriter.Indent--; + _textWriter.WriteLine("}"); + } + + /// + /// Deserialize the given parameter and check the result. + /// + /// The full name of the packet. + /// The parameter to deserialize. + /// Whether the token is the last one. + public void DeserializeAndCheck(string packetFullName, ParameterInfo parameter, bool isLast) + { + string isLastString = isLast ? "true" : "false"; + _textWriter.WriteMultiline($@" +var {parameter.GetResultVariableName()} = _typeConverterRepository.Deserialize<{parameter.GetNullableType()}>({_stringEnumeratorVariable}); +var {parameter.GetErrorVariableName()} = CheckDeserializationResult({parameter.GetResultVariableName()}, ""{parameter.Name}"", {_stringEnumeratorVariable}, {isLastString}); +if ({parameter.GetErrorVariableName()} is not null) +{{ + return Result<{packetFullName}?>.FromError({parameter.GetErrorVariableName()}, {parameter.GetResultVariableName()}); +}} +"); + } + + /// + /// Check taht the given variable is not null, if it is, return an error. + /// + /// The result variable to check for null. + /// The parameter that is being parsed. + /// The reason for the error. + public void CheckNullError(string resultVariableName, string parameterName, string reason = "The converter has returned null even though it was not expected.") + { + _textWriter.WriteMultiline($@" +if ({resultVariableName}.Entity is null) {{ + return new PacketParameterSerializerError(this, ""{parameterName}"", {resultVariableName}, ""{reason}""); +}} +"); + } + + /// + /// Assign local variable with the result of the parameter deserialization. + /// + /// The parameter. + public void AssignLocalVariable(ParameterInfo parameter) + { + _textWriter.WriteLine($"var {parameter.Name} = ({parameter.GetActualType()}){parameter.GetResultVariableName()}.Entity;"); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..a3cab6406ecfe17358a992304abc502a081f03e2 --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs @@ -0,0 +1,93 @@ +// +// ConverterSerializationGenerator.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 NosSmooth.PacketSerializersGenerator.Data; +using NosSmooth.PacketSerializersGenerator.Extensions; + +namespace NosSmooth.PacketSerializersGenerator; + +/// +/// Various templates for converter serialization. +/// +public class ConverterSerializationGenerator +{ + private readonly string _builderVariable = "builder"; + private readonly IndentedTextWriter _textWriter; + + /// + /// Initializes a new instance of the class. + /// + /// The text writer. + public ConverterSerializationGenerator(IndentedTextWriter textWriter) + { + _textWriter = textWriter; + } + + /// + /// Push level to the string enumerator. + /// + /// The separator. + public void SetAfterSeparatorOnce(char separator) + { + _textWriter.WriteLine(@$"{_builderVariable}.SetAfterSeparatorOnce(""{separator}"");"); + } + + /// + /// Push level to the string enumerator. + /// + /// The separator. + public void PushLevel(char separator) + { + _textWriter.WriteLine + (@$"{_builderVariable}.PushLevel(""{separator}"");"); + } + + /// + /// Pop level from the string enumerator. + /// + public void PopLevel() + { + _textWriter.WriteLine($"{_builderVariable}.PopLevel();"); + } + + /// + /// Prepare the level to the string enumerator. + /// + /// The separator. + /// The maximum number of tokens to read. + public void PrepareLevel(char separator, uint? maxTokens = default) + { + _textWriter.WriteLine + ($@"{_builderVariable}.PrepareLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});"); + } + + /// + /// Prepare the level to the string enumerator. + /// + public void RemovePreparedLevel() + { + _textWriter.WriteLine($@"{_builderVariable}.RemovePreparedLevel();"); + } + + /// + /// Deserialize the given parameter and check the result. + /// + /// The parameter to deserialize. + public void SerializeAndCheck(ParameterInfo parameter) + { + _textWriter.WriteMultiline + ( + $@" +var {parameter.GetResultVariableName()} = _typeConverterRepository.Serialize<{parameter.GetActualType()}>(obj.{parameter.Name}, {_builderVariable}); +if (!{parameter.GetResultVariableName()}.IsSuccess) +{{ + return Result.FromError(new PacketParameterSerializerError(this, ""{parameter.Name}"", {parameter.GetResultVariableName()}), {parameter.GetResultVariableName()}); +}} +" + ); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..edba10e19acb294b120353c229485be9050fa4b2 --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs @@ -0,0 +1,24 @@ +// +// AttributeInfo.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 Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NosSmooth.PacketSerializersGenerator.Data; + +/// +/// The attribute info. +/// +/// The attribute syntax. +/// The full name of the attribute containing namespace. +/// The indexed arguments passed to the attribute. +/// The named arguments passed to the attribute. +public record AttributeInfo +( + AttributeSyntax Attribute, + string FullName, + IReadOnlyList IndexedAttributeArguments, + IReadOnlyDictionary NamedAttributeArguments +); \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d0269d4e46b83a2c8b261227efa21acc4b5609e --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs @@ -0,0 +1,29 @@ +// +// PacketInfo.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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NosSmooth.PacketSerializersGenerator.Data; + +/// +/// Contains information about a packet record syntax. +/// +/// The compilation of the generator. +/// The packet record declaration. +/// The semantic model the packet is in. +/// The parsed parameters of the packet. +/// The namespace of the packet record. +/// The name of the packet. +public record PacketInfo +( + Compilation Compilation, + RecordDeclarationSyntax PacketRecord, + SemanticModel SemanticModel, + Parameters Parameters, + string Namespace, + string Name +); \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/ParameterInfo.cs similarity index 50% rename from Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs rename to Core/NosSmooth.PacketSerializersGenerator/Data/ParameterInfo.cs index 6e93cc86975f56bce66510ba80e72b05c9774ff2..b0a7e5ebc66f521819f72a9e204726add0384016 100644 --- a/Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/ParameterInfo.cs @@ -7,33 +7,25 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace NosSmooth.PacketSerializersGenerator; +namespace NosSmooth.PacketSerializersGenerator.Data; /// /// Information about a parameter of a packet constructor. /// -/// -/// -/// -/// -/// +/// The parameter's syntax. +/// The type of the parameter. +/// Whether the parameter type is nullable. +/// The list of all of the attribute on the parameter that are used for the generation of serializers. /// /// /// public record ParameterInfo ( - Compilation Compilation, ParameterSyntax Parameter, - AttributeSyntax Attribute, - IReadOnlyList IndexedAttributeArguments, - IReadOnlyDictionary NamedAttributeArguments, + ITypeSymbol Type, + bool Nullable, + IReadOnlyList Attributes, string Name, int ConstructorIndex, int PacketIndex -) -{ - /// - /// Gets or sets if this parameter is the last one. - /// - public bool IsLast { get; set; } -} \ No newline at end of file +); \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs b/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs new file mode 100644 index 0000000000000000000000000000000000000000..32bf98a1371dbba60fb2535fc9253265b67e2dec --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs @@ -0,0 +1,57 @@ +// +// Parameters.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. + +namespace NosSmooth.PacketSerializersGenerator.Data; + +/// +/// Contains set of parameters of a packet. +/// +public class Parameters +{ + /// + /// Initializes a new instance of the class. + /// + /// The list of the parameters. + public Parameters(IReadOnlyList parameters) + { + List = parameters; + } + + /// + /// Gets the list of the parameters. + /// + public IReadOnlyList List { get; } + + /// + /// Gets the current index of the parameter. + /// + public int CurrentIndex { get; set; } + + /// + /// Gets the currently processing parameter. + /// + public ParameterInfo Current => List[CurrentIndex]; + + /// + /// Gets the next processing parameter. + /// + public ParameterInfo Next => List[CurrentIndex]; + + /// + /// Gets the previous processing parameter. + /// + public ParameterInfo Previous => List[CurrentIndex]; + + /// + /// Gets whether the current parameter is the last one. + /// + public bool IsLast => CurrentIndex == List.Count - 1; + + /// + /// Gets whether the current parameter is the first one. + /// + public bool IsFirst => CurrentIndex == 0; +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..f2e843370f07a5208aec8fd84902f105a6a6b71e --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs @@ -0,0 +1,33 @@ +// +// AttributeArgumentSyntaxExtensions.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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NosSmooth.PacketSerializersGenerator.Extensions; + +/// +/// Extension methods for . +/// +public static class AttributeArgumentSyntaxExtensions +{ + /// + /// Get the value of the argument. + /// + /// The attribute argument. + /// The semantic model containing the attribute argument info. + /// The value. + public static object? GetValue(this AttributeArgumentSyntax attributeArgument, SemanticModel semanticModel) + { + var value = semanticModel.GetConstantValue(attributeArgument.Expression); + if (!value.HasValue) + { + return null; + } + + return value; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..d7b9386e40126b4d37664330c264468fda73df96 --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs @@ -0,0 +1,58 @@ +// +// AttributeInfoExtensions.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.Diagnostics; +using NosSmooth.PacketSerializersGenerator.Data; + +namespace NosSmooth.PacketSerializersGenerator.Extensions; + +/// +/// Extension methods for . +/// +public static class AttributeInfoExtensions +{ + /// + /// Get value of a named parameter. + /// + /// The attribute information. + /// The name of the parameter. + /// The default value to return if not found. + /// The value type. + /// The value of the attribute. + public static TValue? GetNamedValue(this AttributeInfo attributeInfo, string name, TValue? @default) + { + if (attributeInfo.NamedAttributeArguments.TryGetValue(name, out var value)) + { + if (typeof(TValue) == typeof(string)) + { + return (TValue?)(object?)value?.ToString(); + } + + return (TValue?)value; + } + + return @default; + } + + /// + /// Get value of a named parameter. + /// + /// The attribute information. + /// The index of the parameter. + /// The value type. + /// The value of the attribute. + public static TValue? GetIndexedValue(this AttributeInfo attributeInfo, int index) + { + var value = attributeInfo.IndexedAttributeArguments[index]; + + if (typeof(TValue) == typeof(string)) + { + return (TValue?)(object?)value?.ToString(); + } + + return (TValue?)value; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..2c12e29e1ef4fcae9cb14e765c43ad6d11e5447f --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs @@ -0,0 +1,29 @@ +// +// AttributeListSyntaxExtensions.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.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NosSmooth.PacketSerializersGenerator.Extensions; + +/// +/// Extension methods for . +/// +public static class AttributeListSyntaxExtensions +{ + /// + /// Whether the attribute list contains the attribute with the given full name. + /// + /// The list of the attributes. + /// The semantic model. + /// The full name of the attribute. + /// Whether the attribute is present. + public static bool ContainsAttribute(this AttributeListSyntax attributeList, SemanticModel semanticModel, string attributeFullName) + { + return attributeList.Attributes.Any(x => Regex.IsMatch(attributeFullName, semanticModel.GetTypeInfo(x).Type?.ToString()!)); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..620f30e7928c778fe15f84b63791081f00665939 --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs @@ -0,0 +1,28 @@ +// +// IndentedTextWriterExtensions.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; + +namespace NosSmooth.PacketSerializersGenerator.Extensions; + +/// +/// Extension methods for . +/// +public static class IndentedTextWriterExtensions +{ + /// + /// Append multiline text with correct indentation. + /// + /// The text writer to write to. + /// The text to write. + public static void WriteMultiline(this IndentedTextWriter textWriter, string text) + { + foreach (var line in text.Split('\n')) + { + textWriter.WriteLine(line); + } + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs b/Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e61aa10b5863a29ffcd466a7fe2d9a450ec39ba --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs @@ -0,0 +1,79 @@ +// +// ParameterInfoExtensions.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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.Data; +using NosSmooth.PacketSerializersGenerator.Errors; + +namespace NosSmooth.PacketSerializersGenerator.Extensions; + +/// +/// Extensions for . +/// +public static class ParameterInfoExtensions +{ + /// + /// Gets the name of the error variable. + /// + /// The parameter. + /// The name of the error variable. + public static string GetErrorVariableName(this ParameterInfo parameterInfo) + { + return $"{parameterInfo.Name}Error"; + } + + /// + /// Gets the name of the error variable. + /// + /// The parameter. + /// The name of the error variable. + public static string GetResultVariableName(this ParameterInfo parameterInfo) + { + return $"{parameterInfo.Name}Result"; + } + + /// + /// Gets the name of the error variable. + /// + /// The parameter. + /// The name of the error variable. + public static string GetVariableName(this ParameterInfo parameterInfo) + { + return parameterInfo.Name; + } + + /// + /// Gets the type of the parameter as nullable. + /// + /// The parameter. + /// The nullable type. + public static string GetNullableType(this ParameterInfo parameterInfo) + { + return parameterInfo.Type.ToString().TrimEnd('?') + "?"; + } + + /// + /// Gets the type of the parameter with ? if the parameter is nullable.. + /// + /// The parameter. + /// The type. + public static string GetActualType(this ParameterInfo parameterInfo) + { + return parameterInfo.Type.ToString().TrimEnd('?') + (parameterInfo.Nullable ? "?" : string.Empty); + } + + /// + /// Gets whether the parameter is marked as optional. + /// + /// The parameter info. + /// Whether the parameter is optional. + public static bool IsOptional(this ParameterInfo parameterInfo) + { + return parameterInfo.Attributes.First().GetNamedValue("IsOptional", false); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs b/Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs deleted file mode 100644 index e0d4a11be58845a2330f19cb265a5e6badff43cd..0000000000000000000000000000000000000000 --- a/Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs +++ /dev/null @@ -1,50 +0,0 @@ -// -// PacketClassReceiver.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 Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace NosSmooth.PacketSerializersGenerator -{ - /// - /// Syntax receiver of classes with generate attribute. - /// - public class PacketClassReceiver : ISyntaxReceiver - { - private readonly List _packetClasses; - - /// - /// Gets the name of the attribute that indicates the packet should have a serializer generated. - /// - public static string AttributeFullName => "GenerateSerializer"; - - /// - /// Initializes a new instance of the class. - /// - public PacketClassReceiver() - { - _packetClasses = new List(); - } - - /// - /// Gets the classes that should have serializers generated. - /// - public IReadOnlyList PacketClasses => _packetClasses; - - /// - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode.IsKind(SyntaxKind.RecordDeclaration) && syntaxNode is RecordDeclarationSyntax classDeclarationSyntax) - { - if (classDeclarationSyntax.AttributeLists.Any(x => x.Attributes.Any(x => x.Name.NormalizeWhitespace().ToFullString() == AttributeFullName))) - { - _packetClasses.Add(classDeclarationSyntax); - } - } - } - } -} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..a9c1413142d964fac7fb1301ce17cd635ea4e08d --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs @@ -0,0 +1,245 @@ +// +// PacketConverterGenerator.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 NosSmooth.PacketSerializersGenerator.AttributeGenerators; +using NosSmooth.PacketSerializersGenerator.Data; +using NosSmooth.PacketSerializersGenerator.Errors; +using NosSmooth.PacketSerializersGenerator.Extensions; + +namespace NosSmooth.PacketSerializersGenerator; + +/// +/// Code generator of a packet converter. +/// +public class PacketConverterGenerator +{ + private readonly PacketInfo _packetInfo; + private readonly IReadOnlyList _parameterGenerators; + + /// + /// Initializes a new instance of the class. + /// + /// The packet type information. + /// The converter parameter generators. + public PacketConverterGenerator(PacketInfo packetInfo, IReadOnlyList parameterGenerators) + { + _packetInfo = packetInfo; + _parameterGenerators = parameterGenerators; + } + + /// + /// Generate the converter class. + /// + /// The text writer to write the class to. + /// An error, if any. + public IError? Generate(IndentedTextWriter textWriter) + { + textWriter.WriteLine + ( + @$"// +#nullable enable +#pragma warning disable 1591 + +using {_packetInfo.Namespace}; +using NosSmooth.Packets.Converters; +using NosSmooth.Packets.Errors; +using NosSmooth.Packets; +using Remora.Results; + +namespace {_packetInfo.Namespace}.Generated; + +public class {_packetInfo.Name}Converter : BaseTypeConverter<{_packetInfo.Name}> +{{" + ); + textWriter.Indent++; + textWriter.WriteLine + ( + $@" +private readonly ITypeConverterRepository _typeConverterRepository; + +public {_packetInfo.Name}Converter(ITypeConverterRepository typeConverterRepository) +{{ + _typeConverterRepository = typeConverterRepository; +}} + +/// +public override Result Serialize({_packetInfo.Name}? obj, PacketStringBuilder builder) +{{ + if (obj is null) + {{ + return new ArgumentNullError(nameof(obj)); + }} +" + ); + textWriter.Indent++; + var serializerError = GenerateSerializer(textWriter); + if (serializerError is not null) + { + return serializerError; + } + + textWriter.Indent--; + textWriter.WriteLine + ( + $@" +}} + +/// +public override Result<{_packetInfo.Name}?> Deserialize(PacketStringEnumerator stringEnumerator) +{{ +" + ); + + textWriter.Indent++; + var deserializerError = GenerateDeserializer(textWriter); + 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) + { + _packetInfo.Parameters.CurrentIndex = 0; + foreach (var parameter in _packetInfo.Parameters.List) + { + bool handled = false; + foreach (var generator in _parameterGenerators) + { + if (generator.ShouldHandle(parameter)) + { + var result = generator.GenerateSerializerPart(textWriter, _packetInfo); + if (result is not null) + { + return result; + } + + handled = true; + break; + } + } + + if (!handled) + { + throw new InvalidOperationException + ( + $"Could not handle {_packetInfo.Namespace}.{_packetInfo.Name}.{parameter.Name}" + ); + } + _packetInfo.Parameters.CurrentIndex++; + } + + textWriter.WriteLine("return Result.FromSuccess();"); + return null; + } + + private IError? GenerateDeserializer + (IndentedTextWriter textWriter) + { + _packetInfo.Parameters.CurrentIndex = 0; + var lastIndex = _packetInfo.Parameters.Current.PacketIndex - 1; + bool skipped = false; + foreach (var parameter in _packetInfo.Parameters.List) + { + 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.WriteMultiline + ( + $@"if (skipError is not null) {{ + return Result<{_packetInfo.Name}>.FromError(skipError, skipResult); +}}" + ); + } + else if (skip < 0) + { + return new DiagnosticError + ( + "SG0004", + "Same packet index", + "There were two parameters of the same packet index {0} on property {1} in packet {2}, that is not supported.", + parameter.Attributes.First().Attribute.SyntaxTree, + parameter.Attributes.First().Attribute.FullSpan, + new List + ( + new[] + { + parameter.PacketIndex.ToString(), + parameter.Name, + _packetInfo.Name + } + ) + ); + } + + bool handled = false; + foreach (var generator in _parameterGenerators) + { + if (generator.ShouldHandle(parameter)) + { + var result = generator.GenerateDeserializerPart(textWriter, _packetInfo); + if (result is not null) + { + return result; + } + + handled = true; + break; + } + } + + if (!handled) + { + throw new InvalidOperationException + ( + $"Could not handle {_packetInfo.Name}.{parameter.Name}" + ); + } + lastIndex = parameter.PacketIndex; + _packetInfo.Parameters.CurrentIndex++; + } + + string parametersString = string.Join(", ", _packetInfo.Parameters.List.OrderBy(x => x.ConstructorIndex).Select(x => x.GetVariableName())); + textWriter.WriteLine + ($"return new {_packetInfo.Name}({parametersString});"); + + return null; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs deleted file mode 100644 index 809475431e2e628e530990b738ab1e7440622d18..0000000000000000000000000000000000000000 --- a/Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs +++ /dev/null @@ -1,375 +0,0 @@ -// -// 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 IParameterGenerator[] - { - new PacketIndexAttributeGenerator(), - new PacketListIndexAttributeGenerator(), - new PacketContextListAttributeGenerator(), - }); - } - - 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(); - using 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) - 1; - 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); -}}"); - } - else if (skip < 0) - { - return new DiagnosticError - ( - "SG0004", - "Same packet index", - "There were two parameters of the same packet index {0} on property {1} in packet {2}, that is not supported.", - parameter.Attribute.SyntaxTree, - parameter.Attribute.FullSpan, - new List(new[] - { - parameter.PacketIndex.ToString(), - parameter.Name, - packetClass.Identifier.NormalizeWhitespace().ToFullString() - }) - ); - } - - 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; - } -} \ No newline at end of file diff --git a/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs b/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..7afac6d100c16381e774768dd450e2c80f180b64 --- /dev/null +++ b/Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs @@ -0,0 +1,278 @@ +// +// SourceGenerator.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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NosSmooth.PacketSerializersGenerator.AttributeGenerators; +using NosSmooth.PacketSerializersGenerator.Data; +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 SourceGenerator : ISourceGenerator +{ + /// + /// Initializes a new instance of the class. + /// + public SourceGenerator() + { + _generators = new List + ( + new IParameterGenerator[] + { + new PacketIndexAttributeGenerator(), + new PacketListIndexAttributeGenerator(), + new PacketContextListAttributeGenerator(), + } + ); + } + + private readonly List _generators; + + /// + public void Initialize(GeneratorInitializationContext context) + { + SpinWait.SpinUntil(() => Debugger.IsAttached); + } + + private IEnumerable GetPacketRecords(Compilation compilation, SyntaxTree tree) + { + var semanticModel = compilation.GetSemanticModel(tree); + var root = tree.GetRoot(); + + return root + .DescendantNodes() + .OfType() + .Where + ( + x => x.AttributeLists.Any + (y => y.ContainsAttribute(semanticModel, Constants.GenerateSourceAttributeClass)) + ); + } + + /// + public void Execute(GeneratorExecutionContext context) + { + var packetRecords = context.Compilation.SyntaxTrees + .SelectMany(x => GetPacketRecords(context.Compilation, x)); + + foreach (var packetRecord in packetRecords) + { + if (packetRecord is not null) + { + using var stringWriter = new StringWriter(); + using var writer = new IndentedTextWriter(stringWriter, " "); + var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetRecord); + 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 + ( + $"{packetRecord.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 createError = CreateParameterInfo + ( + packetClass, + parameter, + semanticModel, + constructorIndex, + out var parameterInfo + ); + + if (createError is not null) + { + return createError; + } + + if (parameterInfo is not null) + { + orderedParameters.Add(parameterInfo); + } + + constructorIndex++; + } + + orderedParameters = orderedParameters.OrderBy(x => x.PacketIndex).ToList(); + var packetInfo = new PacketInfo + ( + compilation, + packetClass, + semanticModel, + new Parameters(orderedParameters), + @namespace, + name + ); + + var generator = new PacketConverterGenerator(packetInfo, _generators); + var generatorError = generator.Generate(textWriter); + + return generatorError; + } + + private IError? CreateParameterInfo + ( + RecordDeclarationSyntax packet, + ParameterSyntax parameter, + SemanticModel semanticModel, + int constructorIndex, + out ParameterInfo? parameterInfo + ) + { + var name = packet.Identifier.NormalizeWhitespace().ToFullString(); + + parameterInfo = null; + var attributes = parameter.AttributeLists + .Where(x => x.ContainsAttribute(semanticModel, Constants.PacketAttributesClassRegex)) + .SelectMany(x => x.Attributes) + .ToList(); + + if (attributes.Count == 0) + { + 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 index = ushort.Parse(attribute.ArgumentList!.Arguments[0].GetValue(semanticModel)!.ToString()); + + List attributeInfos = attributes + .Select(x => CreateAttributeInfo(x, semanticModel)) + .ToList(); + + parameterInfo = new ParameterInfo + ( + parameter, + semanticModel.GetTypeInfo(parameter.Type!).Type!, + parameter.Type is NullableTypeSyntax, + attributeInfos, + parameter.Identifier.NormalizeWhitespace().ToFullString(), + constructorIndex, + index + ); + return null; + } + + private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel) + { + var namedArguments = new Dictionary(); + var arguments = new List(); + + foreach (var argument in attribute.ArgumentList!.Arguments) + { + var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString(); + var value = argument.GetValue(semanticModel); + + if (argumentName is not null) + { + namedArguments.Add(argumentName, value); + } + else + { + arguments.Add(value); + } + } + + return new AttributeInfo + ( + attribute, + semanticModel.GetTypeInfo(attribute).Type?.ToString()!, + arguments, + namedArguments + ); + } +} \ No newline at end of file