~ruther/NosSmooth

661f4e96df3c03e74668eaefbd71b142a7d54a25 — František Boháček 3 years ago 0e9c9ae
feat: rewrite packet serializer generator
19 files changed, 1116 insertions(+), 516 deletions(-)

M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs
M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs
A Core/NosSmooth.PacketSerializersGenerator/Constants.cs
A Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs
A Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs
A Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs
A Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs
R Core/NosSmooth.PacketSerializersGenerator/{ParameterInfo.cs => Data/ParameterInfo.cs}
A Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs
A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs
A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs
A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs
A Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs
A Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs
D Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs
A Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs
R Core/NosSmooth.PacketSerializersGenerator/{PacketSerializerGenerator.cs => SourceGenerator.cs}
M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs +7 -8
@@ 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
        /// <summary>
        /// Check whether this generator should handle parameter with this attribute.
        /// </summary>
        /// <param name="attribute">The parameters attribute.</param>
        /// <param name="parameter">The parameter.</param>
        /// <returns>Whether to handle this parameter.</returns>
        public bool ShouldHandle(AttributeSyntax attribute);
        public bool ShouldHandle(ParameterInfo parameter);

        /// <summary>
        /// Generate part for the Serializer method to serialize the given parameter.
        /// </summary>
        /// <param name="textWriter">The text writer to write the code to.</param>
        /// <param name="recordDeclarationSyntax">The packet record declaration syntax.</param>
        /// <param name="parameterInfo">The parameter info to generate for.</param>
        /// <param name="packetInfo">The packet info to generate for.</param>
        /// <returns>The generated source code.</returns>
        public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo);
        public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo);

        /// <summary>
        /// Generate part for the Deserializer method to deserialize the given parameter.
        /// </summary>
        /// <param name="textWriter">The text writer to write the code to.</param>
        /// <param name="recordDeclarationSyntax">The packet record declaration syntax.</param>
        /// <param name="parameterInfo">The parameter info to generate for.</param>
        /// <param name="packetInfo">The packet info to generate for.</param>
        /// <returns>The generated source code.</returns>
        public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo);
        public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo);
    }
}
\ No newline at end of file

M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketContextListAttributeGenerator.cs +41 -69
@@ 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;

/// <inheritdoc />
public class PacketContextListAttributeGenerator : IParameterGenerator
{
    /// <summary>
    /// Gets the full name of the packet index attribute.
    /// </summary>
    public static string PacketListIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketContextListAttribute";

    /// <inheritdoc />
    public bool ShouldHandle(AttributeSyntax attribute)
        => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketContextList";
    public bool ShouldHandle(ParameterInfo parameter)
        => parameter.Attributes.Any(x => x.FullName == PacketListIndexAttributeFullName);

    /// <inheritdoc />
    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<char?>("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<char>("ListSeparator", '|');
        generator.PushLevel(listSeparator);

        textWriter.WriteLine($"builder.PrepareLevel('{innerSeparator}')");
        var innerSeparator = attribute.GetNamedValue<char>("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;
    }

    /// <inheritdoc />
    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<char?>("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<char>("ListSeparator", '|');
        var lengthVariable = attribute.GetIndexedValue<string>(1);
        textWriter.WriteLine(@$"stringEnumerator.PushLevel(""{listSeparator}"", {lengthVariable});");

        textWriter.WriteLine($"stringEnumerator.PushLevel('{listSeparator}');");
        var innerSeparator = attribute.GetNamedValue<char>("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

M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs +38 -55
@@ 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;

/// <inheritdoc />
public class PacketIndexAttributeGenerator : IParameterGenerator
{
    /// <summary>
    /// Gets the full name of the packet index attribute.
    /// </summary>
    public static string PacketIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketIndexAttribute";

    /// <inheritdoc />
    public bool ShouldHandle(AttributeSyntax attribute)
        => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketIndex";
    public bool ShouldHandle(ParameterInfo parameter)
        => parameter.Attributes.Any(x => x.FullName == PacketIndexAttributeFullName);

    /// <inheritdoc />
    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<char?>("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<char?>("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;
    }

    /// <inheritdoc />
    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<char?>("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<char?>("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;

M Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs +42 -74
@@ 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;

/// <inheritdoc />
public class PacketListIndexAttributeGenerator : IParameterGenerator
{
    /// <summary>
    /// Gets the full name of the packet index attribute.
    /// </summary>
    public static string PacketListIndexAttributeFullName => "NosSmooth.Packets.Attributes.PacketListIndex";

    /// <inheritdoc />
    public bool ShouldHandle(AttributeSyntax attribute)
        => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketListIndex";
    public bool ShouldHandle(ParameterInfo parameter)
        => parameter.Attributes.Any(x => x.FullName == PacketListIndexAttributeFullName);

    /// <inheritdoc />
    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<char?>("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<char>("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<char>("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;
    }

    /// <inheritdoc />
    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<char?>("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<char>("ListSeparator", '|');
        var length = attribute.GetNamedValue<int>("Length", 0);
        generator.PushLevel(listSeparator, length != 0 ? (uint?)length : (uint?)null);

        var innerSeparator = attribute.GetNamedValue<char>("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;
    }

A Core/NosSmooth.PacketSerializersGenerator/Constants.cs => Core/NosSmooth.PacketSerializersGenerator/Constants.cs +23 -0
@@ 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;

/// <summary>
/// Contains constants needed for the generation.
/// </summary>
public class Constants
{
    /// <summary>
    /// Gets the full name of the generate source attribute class.
    /// </summary>
    public static string GenerateSourceAttributeClass => "NosSmooth.Packets.Attributes.GenerateSerializerAttribute";

    /// <summary>
    /// Gets the full name of the packet attribute classes that are used for the generation.
    /// </summary>
    public static string PacketAttributesClassRegex => @"^NosSmooth\.Packets\.Attributes\.Packet.*";
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/ConverterDeserializationGenerator.cs +135 -0
@@ 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;

/// <summary>
/// Various templates for converter deserialization.
/// </summary>
public class ConverterDeserializationGenerator
{
    private readonly string _stringEnumeratorVariable = "stringEnumerator";
    private readonly IndentedTextWriter _textWriter;

    /// <summary>
    /// Initializes a new instance of the <see cref="ConverterDeserializationGenerator"/> class.
    /// </summary>
    /// <param name="textWriter">The text writer.</param>
    public ConverterDeserializationGenerator(IndentedTextWriter textWriter)
    {
        _textWriter = textWriter;
    }

    /// <summary>
    /// Push level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _textWriter.WriteLine(@$"{_stringEnumeratorVariable}.SetAfterSeparatorOnce(""{separator}"");");
    }

    /// <summary>
    /// Push level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    public void PushLevel(char separator, uint? maxTokens = default)
    {
        _textWriter.WriteLine(@$"{_stringEnumeratorVariable}.PushLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});");
    }

    /// <summary>
    /// Pop level from the string enumerator.
    /// </summary>
    public void PopLevel()
    {
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.PopLevel();");
    }

    /// <summary>
    /// Prepare the level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    public void PrepareLevel(char separator, uint? maxTokens = default)
    {
        _textWriter.WriteLine($@"{_stringEnumeratorVariable}.PrepareLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});");
    }

    /// <summary>
    /// Prepare the level to the string enumerator.
    /// </summary>
    public void RemovePreparedLevel()
    {
        _textWriter.WriteLine($@"{_stringEnumeratorVariable}.RemovePreparedLevel();");
    }

    /// <summary>
    /// Try to read to the last token of the level.
    /// </summary>
    /// <remarks>
    /// 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.
    /// </remarks>
    public void ReadToLastToken()
    {
        _textWriter.WriteLine($@"while ({_stringEnumeratorVariable}.IsOnLastToken() == false)");
        _textWriter.WriteLine("{");
        _textWriter.Indent++;
        _textWriter.WriteLine($"{_stringEnumeratorVariable}.GetNextToken();");
        _textWriter.Indent--;
        _textWriter.WriteLine("}");
    }

    /// <summary>
    /// Deserialize the given parameter and check the result.
    /// </summary>
    /// <param name="packetFullName">The full name of the packet.</param>
    /// <param name="parameter">The parameter to deserialize.</param>
    /// <param name="isLast">Whether the token is the last one.</param>
    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()});
}}
");
    }

    /// <summary>
    /// Check taht the given variable is not null, if it is, return an error.
    /// </summary>
    /// <param name="resultVariableName">The result variable to check for null.</param>
    /// <param name="parameterName">The parameter that is being parsed.</param>
    /// <param name="reason">The reason for the error.</param>
    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}"");
}}
");
    }

    /// <summary>
    /// Assign local variable with the result of the parameter deserialization.
    /// </summary>
    /// <param name="parameter">The parameter.</param>
    public void AssignLocalVariable(ParameterInfo parameter)
    {
        _textWriter.WriteLine($"var {parameter.Name} = ({parameter.GetActualType()}){parameter.GetResultVariableName()}.Entity;");
    }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/ConverterSerializationGenerator.cs +93 -0
@@ 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;

/// <summary>
/// Various templates for converter serialization.
/// </summary>
public class ConverterSerializationGenerator
{
    private readonly string _builderVariable = "builder";
    private readonly IndentedTextWriter _textWriter;

    /// <summary>
    /// Initializes a new instance of the <see cref="ConverterSerializationGenerator"/> class.
    /// </summary>
    /// <param name="textWriter">The text writer.</param>
    public ConverterSerializationGenerator(IndentedTextWriter textWriter)
    {
        _textWriter = textWriter;
    }

    /// <summary>
    /// Push level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _textWriter.WriteLine(@$"{_builderVariable}.SetAfterSeparatorOnce(""{separator}"");");
    }

    /// <summary>
    /// Push level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    public void PushLevel(char separator)
    {
        _textWriter.WriteLine
            (@$"{_builderVariable}.PushLevel(""{separator}"");");
    }

    /// <summary>
    /// Pop level from the string enumerator.
    /// </summary>
    public void PopLevel()
    {
        _textWriter.WriteLine($"{_builderVariable}.PopLevel();");
    }

    /// <summary>
    /// Prepare the level to the string enumerator.
    /// </summary>
    /// <param name="separator">The separator.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    public void PrepareLevel(char separator, uint? maxTokens = default)
    {
        _textWriter.WriteLine
            ($@"{_builderVariable}.PrepareLevel(""{separator}"", {maxTokens?.ToString() ?? "null"});");
    }

    /// <summary>
    /// Prepare the level to the string enumerator.
    /// </summary>
    public void RemovePreparedLevel()
    {
        _textWriter.WriteLine($@"{_builderVariable}.RemovePreparedLevel();");
    }

    /// <summary>
    /// Deserialize the given parameter and check the result.
    /// </summary>
    /// <param name="parameter">The parameter to deserialize.</param>
    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

A Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs => Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs +24 -0
@@ 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;

/// <summary>
/// The attribute info.
/// </summary>
/// <param name="Attribute">The attribute syntax.</param>
/// <param name="FullName">The full name of the attribute containing namespace.</param>
/// <param name="IndexedAttributeArguments">The indexed arguments passed to the attribute.</param>
/// <param name="NamedAttributeArguments">The named arguments passed to the attribute.</param>
public record AttributeInfo
(
    AttributeSyntax Attribute,
    string FullName,
    IReadOnlyList<object?> IndexedAttributeArguments,
    IReadOnlyDictionary<string, object?> NamedAttributeArguments
);
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs => Core/NosSmooth.PacketSerializersGenerator/Data/PacketInfo.cs +29 -0
@@ 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;

/// <summary>
/// Contains information about a packet record syntax.
/// </summary>
/// <param name="Compilation">The compilation of the generator.</param>
/// <param name="PacketRecord">The packet record declaration.</param>
/// <param name="SemanticModel">The semantic model the packet is in.</param>
/// <param name="Parameters">The parsed parameters of the packet.</param>
/// <param name="Namespace">The namespace of the packet record.</param>
/// <param name="Name">The name of the packet.</param>
public record PacketInfo
(
    Compilation Compilation,
    RecordDeclarationSyntax PacketRecord,
    SemanticModel SemanticModel,
    Parameters Parameters,
    string Namespace,
    string Name
);
\ No newline at end of file

R Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs => Core/NosSmooth.PacketSerializersGenerator/Data/ParameterInfo.cs +9 -17
@@ 7,33 7,25 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace NosSmooth.PacketSerializersGenerator;
namespace NosSmooth.PacketSerializersGenerator.Data;

/// <summary>
/// Information about a parameter of a packet constructor.
/// </summary>
/// <param name="Compilation"></param>
/// <param name="Parameter"></param>
/// <param name="Attribute"></param>
/// <param name="IndexedAttributeArguments"></param>
/// <param name="NamedAttributeArguments"></param>
/// <param name="Parameter">The parameter's syntax.</param>
/// <param name="Type">The type of the parameter.</param>
/// <param name="Nullable">Whether the parameter type is nullable.</param>
/// <param name="Attributes">The list of all of the attribute on the parameter that are used for the generation of serializers.</param>
/// <param name="Name"></param>
/// <param name="ConstructorIndex"></param>
/// <param name="PacketIndex"></param>
public record ParameterInfo
(
    Compilation Compilation,
    ParameterSyntax Parameter,
    AttributeSyntax Attribute,
    IReadOnlyList<object?> IndexedAttributeArguments,
    IReadOnlyDictionary<string, object?> NamedAttributeArguments,
    ITypeSymbol Type,
    bool Nullable,
    IReadOnlyList<AttributeInfo> Attributes,
    string Name,
    int ConstructorIndex,
    int PacketIndex
)
{
    /// <summary>
    /// Gets or sets if this parameter is the last one.
    /// </summary>
    public bool IsLast { get; set; }
}
\ No newline at end of file
);
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs => Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs +57 -0
@@ 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;

/// <summary>
/// Contains set of parameters of a packet.
/// </summary>
public class Parameters
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Parameters"/> class.
    /// </summary>
    /// <param name="parameters">The list of the parameters.</param>
    public Parameters(IReadOnlyList<ParameterInfo> parameters)
    {
        List = parameters;
    }

    /// <summary>
    /// Gets the list of the parameters.
    /// </summary>
    public IReadOnlyList<ParameterInfo> List { get; }

    /// <summary>
    /// Gets the current index of the parameter.
    /// </summary>
    public int CurrentIndex { get; set; }

    /// <summary>
    /// Gets the currently processing parameter.
    /// </summary>
    public ParameterInfo Current => List[CurrentIndex];

    /// <summary>
    /// Gets the next processing parameter.
    /// </summary>
    public ParameterInfo Next => List[CurrentIndex];

    /// <summary>
    /// Gets the previous processing parameter.
    /// </summary>
    public ParameterInfo Previous => List[CurrentIndex];

    /// <summary>
    /// Gets whether the current parameter is the last one.
    /// </summary>
    public bool IsLast => CurrentIndex == List.Count - 1;

    /// <summary>
    /// Gets whether the current parameter is the first one.
    /// </summary>
    public bool IsFirst => CurrentIndex == 0;
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeArgumentSyntaxExtensions.cs +33 -0
@@ 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;

/// <summary>
/// Extension methods for <see cref="AttributeArgumentSyntax"/>.
/// </summary>
public static class AttributeArgumentSyntaxExtensions
{
    /// <summary>
    /// Get the value of the argument.
    /// </summary>
    /// <param name="attributeArgument">The attribute argument.</param>
    /// <param name="semanticModel">The semantic model containing the attribute argument info.</param>
    /// <returns>The value.</returns>
    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

A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs +58 -0
@@ 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;

/// <summary>
/// Extension methods for <see cref="AttributeInfo"/>.
/// </summary>
public static class AttributeInfoExtensions
{
    /// <summary>
    /// Get value of a named parameter.
    /// </summary>
    /// <param name="attributeInfo">The attribute information.</param>
    /// <param name="name">The name of the parameter.</param>
    /// <param name="default">The default value to return if not found.</param>
    /// <typeparam name="TValue">The value type.</typeparam>
    /// <returns>The value of the attribute.</returns>
    public static TValue? GetNamedValue<TValue>(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;
    }

    /// <summary>
    /// Get value of a named parameter.
    /// </summary>
    /// <param name="attributeInfo">The attribute information.</param>
    /// <param name="index">The index of the parameter.</param>
    /// <typeparam name="TValue">The value type.</typeparam>
    /// <returns>The value of the attribute.</returns>
    public static TValue? GetIndexedValue<TValue>(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

A Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeListSyntaxExtensions.cs +29 -0
@@ 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;

/// <summary>
/// Extension methods for <see cref="AttributeListSyntax"/>.
/// </summary>
public static class AttributeListSyntaxExtensions
{
    /// <summary>
    /// Whether the attribute list contains the attribute with the given full name.
    /// </summary>
    /// <param name="attributeList">The list of the attributes.</param>
    /// <param name="semanticModel">The semantic model.</param>
    /// <param name="attributeFullName">The full name of the attribute.</param>
    /// <returns>Whether the attribute is present.</returns>
    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

A Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/IndentedTextWriterExtensions.cs +28 -0
@@ 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;

/// <summary>
/// Extension methods for <see cref="IndentedTextWriter"/>.
/// </summary>
public static class IndentedTextWriterExtensions
{
    /// <summary>
    /// Append multiline text with correct indentation.
    /// </summary>
    /// <param name="textWriter">The text writer to write to.</param>
    /// <param name="text">The text to write.</param>
    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

A Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/ParameterInfoExtensions.cs +79 -0
@@ 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;

/// <summary>
/// Extensions for <see cref="ParameterInfo"/>.
/// </summary>
public static class ParameterInfoExtensions
{
    /// <summary>
    /// Gets the name of the error variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The name of the error variable.</returns>
    public static string GetErrorVariableName(this ParameterInfo parameterInfo)
    {
        return $"{parameterInfo.Name}Error";
    }

    /// <summary>
    /// Gets the name of the error variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The name of the error variable.</returns>
    public static string GetResultVariableName(this ParameterInfo parameterInfo)
    {
        return $"{parameterInfo.Name}Result";
    }

    /// <summary>
    /// Gets the name of the error variable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The name of the error variable.</returns>
    public static string GetVariableName(this ParameterInfo parameterInfo)
    {
        return parameterInfo.Name;
    }

    /// <summary>
    /// Gets the type of the parameter as nullable.
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The nullable type.</returns>
    public static string GetNullableType(this ParameterInfo parameterInfo)
    {
        return parameterInfo.Type.ToString().TrimEnd('?') + "?";
    }

    /// <summary>
    /// Gets the type of the parameter with ? if the parameter is nullable..
    /// </summary>
    /// <param name="parameterInfo">The parameter.</param>
    /// <returns>The type.</returns>
    public static string GetActualType(this ParameterInfo parameterInfo)
    {
        return parameterInfo.Type.ToString().TrimEnd('?') + (parameterInfo.Nullable ? "?" : string.Empty);
    }

    /// <summary>
    /// Gets whether the parameter is marked as optional.
    /// </summary>
    /// <param name="parameterInfo">The parameter info.</param>
    /// <returns>Whether the parameter is optional.</returns>
    public static bool IsOptional(this ParameterInfo parameterInfo)
    {
        return parameterInfo.Attributes.First().GetNamedValue("IsOptional", false);
    }
}
\ No newline at end of file

D Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs => Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs +0 -50
@@ 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
{
    /// <summary>
    /// Syntax receiver of classes with generate attribute.
    /// </summary>
    public class PacketClassReceiver : ISyntaxReceiver
    {
        private readonly List<RecordDeclarationSyntax> _packetClasses;

        /// <summary>
        /// Gets the name of the attribute that indicates the packet should have a serializer generated.
        /// </summary>
        public static string AttributeFullName => "GenerateSerializer";

        /// <summary>
        /// Initializes a new instance of the <see cref="PacketClassReceiver"/> class.
        /// </summary>
        public PacketClassReceiver()
        {
            _packetClasses = new List<RecordDeclarationSyntax>();
        }

        /// <summary>
        /// Gets the classes that should have serializers generated.
        /// </summary>
        public IReadOnlyList<RecordDeclarationSyntax> PacketClasses => _packetClasses;

        /// <inheritdoc />
        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

A Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +245 -0
@@ 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;

/// <summary>
/// Code generator of a packet converter.
/// </summary>
public class PacketConverterGenerator
{
    private readonly PacketInfo _packetInfo;
    private readonly IReadOnlyList<IParameterGenerator> _parameterGenerators;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketConverterGenerator"/> class.
    /// </summary>
    /// <param name="packetInfo">The packet type information.</param>
    /// <param name="parameterGenerators">The converter parameter generators.</param>
    public PacketConverterGenerator(PacketInfo packetInfo, IReadOnlyList<IParameterGenerator> parameterGenerators)
    {
        _packetInfo = packetInfo;
        _parameterGenerators = parameterGenerators;
    }

    /// <summary>
    /// Generate the converter class.
    /// </summary>
    /// <param name="textWriter">The text writer to write the class to.</param>
    /// <returns>An error, if any.</returns>
    public IError? Generate(IndentedTextWriter textWriter)
    {
        textWriter.WriteLine
        (
            @$"// <auto-generated/>
#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;
}}

/// <inheritdoc />
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
        (
            $@"
}}

/// <inheritdoc />
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<T>(Result<T> 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<PacketToken> 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<object?>
                    (
                        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

R Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +146 -243
@@ 1,16 1,16 @@
//
//  PacketSerializerGenerator.cs
//  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 System.Text;
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;



@@ 23,19 23,22 @@ namespace NosSmooth.PacketSerializersGenerator;
/// The packets to create serializer for have to be records that specify PacketIndices in the constructor.
/// </remarks>
[Generator]
public class PacketSerializerGenerator : ISourceGenerator
public class SourceGenerator : ISourceGenerator
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSerializerGenerator"/> class.
    /// Initializes a new instance of the <see cref="SourceGenerator"/> class.
    /// </summary>
    public PacketSerializerGenerator()
    public SourceGenerator()
    {
        _generators = new List<IParameterGenerator>(new IParameterGenerator[]
        {
            new PacketIndexAttributeGenerator(),
            new PacketListIndexAttributeGenerator(),
            new PacketContextListAttributeGenerator(),
        });
        _generators = new List<IParameterGenerator>
        (
            new IParameterGenerator[]
            {
                new PacketIndexAttributeGenerator(),
                new PacketListIndexAttributeGenerator(),
                new PacketContextListAttributeGenerator(),
            }
        );
    }

    private readonly List<IParameterGenerator> _generators;


@@ 43,26 46,44 @@ public class PacketSerializerGenerator : ISourceGenerator
    /// <inheritdoc />
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new PacketClassReceiver());
        SpinWait.SpinUntil(() => Debugger.IsAttached);
    }

    private IEnumerable<RecordDeclarationSyntax> GetPacketRecords(Compilation compilation, SyntaxTree tree)
    {
        var semanticModel = compilation.GetSemanticModel(tree);
        var root = tree.GetRoot();

        return root
            .DescendantNodes()
            .OfType<RecordDeclarationSyntax>()
            .Where
            (
                x => x.AttributeLists.Any
                    (y => y.ContainsAttribute(semanticModel, Constants.GenerateSourceAttributeClass))
            );
    }

    /// <inheritdoc />
    public void Execute(GeneratorExecutionContext context)
    {
        var syntaxReceiver = (PacketClassReceiver)context.SyntaxReceiver!;
        var packetRecords = context.Compilation.SyntaxTrees
            .SelectMany(x => GetPacketRecords(context.Compilation, x));

        foreach (var packetClass in syntaxReceiver.PacketClasses)
        foreach (var packetRecord in packetRecords)
        {
            if (packetClass is not null)
            if (packetRecord is not null)
            {
                using var stringWriter = new StringWriter();
                using var writer = new IndentedTextWriter(stringWriter, "    ");
                var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetClass);
                var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetRecord);
                if (generatedResult is not null)
                {
                    if (generatedResult is DiagnosticError diagnosticError)
                    {
                        context.ReportDiagnostic(Diagnostic.Create
                        context.ReportDiagnostic
                        (
                            Diagnostic.Create
                            (
                                new DiagnosticDescriptor
                                (


@@ 86,12 107,17 @@ public class PacketSerializerGenerator : ISourceGenerator
                    continue;
                }

                context.AddSource($"{packetClass.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs", stringWriter.GetStringBuilder().ToString());
                context.AddSource
                (
                    $"{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );
            }
        }
    }

    private IError? GeneratePacketSerializer(IndentedTextWriter textWriter, Compilation compilation, RecordDeclarationSyntax packetClass)
    private IError? GeneratePacketSerializer
        (IndentedTextWriter textWriter, Compilation compilation, RecordDeclarationSyntax packetClass)
    {
        var semanticModel = compilation.GetSemanticModel(packetClass.SyntaxTree);



@@ 103,16 129,20 @@ public class PacketSerializerGenerator : ISourceGenerator

        if (constructor is null)
        {
            return new DiagnosticError(
            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<object?>(new[]
                {
                    packetClass.Identifier.NormalizeWhitespace().ToFullString()
                })
                new List<object?>
                (
                    new[]
                    {
                        packetClass.Identifier.NormalizeWhitespace().ToFullString()
                    }
                )
            );
        }



@@ 121,255 151,128 @@ public class PacketSerializerGenerator : ISourceGenerator
        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();
            var createError = CreateParameterInfo
            (
                packetClass,
                parameter,
                semanticModel,
                constructorIndex,
                out var parameterInfo
            );

            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<object?>(new[]
                    {
                        parameter.Identifier.NormalizeWhitespace().ToFullString(),
                        name
                    })
                );
            }
            else if (attributeLists.Count == 0 || attributes is null || attributes.Count < 1)
            if (createError is not null)
            {
                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<object?>(new[]
                    {
                        parameter.Identifier.NormalizeWhitespace().ToFullString(),
                        name
                    })
                );
                return createError;
            }

            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<string, object?>();
            var arguments = new List<object?>();

            foreach (var argument in attribute.ArgumentList.Arguments)
            if (parameterInfo is not null)
            {
                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(parameterInfo);
            }

            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(@$"// <auto-generated/>
#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;
}}

/// <inheritdoc />
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($@"
}}

/// <inheritdoc />
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<T>(Result<T> 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;
        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? GenerateSerializer(IndentedTextWriter textWriter, RecordDeclarationSyntax packetClass, List<ParameterInfo> parameters)
    private IError? CreateParameterInfo
    (
        RecordDeclarationSyntax packet,
        ParameterSyntax parameter,
        SemanticModel semanticModel,
        int constructorIndex,
        out ParameterInfo? parameterInfo
    )
    {
        foreach (var parameter in parameters)
        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)
        {
            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 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<object?>
                (
                    new[]
                    {
                        return result;
                        parameter.Identifier.NormalizeWhitespace().ToFullString(),
                        name
                    }

                    handled = true;
                    break;
                }
            }

            if (!handled)
            {
                throw new InvalidOperationException($"Could not handle {packetClass.Identifier.NormalizeWhitespace().ToFullString()}.{parameter.Name}");
            }
                )
            );
        }

        textWriter.WriteLine("return Result.FromSuccess();");
        var attribute = attributes.First();
        var index = ushort.Parse(attribute.ArgumentList!.Arguments[0].GetValue(semanticModel)!.ToString());

        List<AttributeInfo> 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 IError? GenerateDeserializer(IndentedTextWriter textWriter, RecordDeclarationSyntax packetClass, List<ParameterInfo> parameters)
    private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel)
    {
        var lastIndex = (parameters.FirstOrDefault()?.PacketIndex ?? 0) - 1;
        bool skipped = false;
        foreach (var parameter in parameters)
        var namedArguments = new Dictionary<string, object?>();
        var arguments = new List<object?>();

        foreach (var argument in attribute.ArgumentList!.Arguments)
        {
            var skip = parameter.PacketIndex - lastIndex - 1;
            if (skip > 0)
            {
                if (!skipped)
                {
                    textWriter.WriteLine("Result<PacketToken> 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<object?>(new[]
                    {
                        parameter.PacketIndex.ToString(),
                        parameter.Name,
                        packetClass.Identifier.NormalizeWhitespace().ToFullString()
                    })
                );
            }
            var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString();
            var value = argument.GetValue(semanticModel);

            bool handled = false;
            foreach (var generator in _generators)
            if (argumentName is not null)
            {
                if (generator.ShouldHandle(parameter.Attribute))
                {
                    var result = generator.GenerateDeserializerPart(textWriter, packetClass, parameter);
                    if (result is not null)
                    {
                        return result;
                    }

                    handled = true;
                    break;
                }
                namedArguments.Add(argumentName, value);
            }

            if (!handled)
            else
            {
                throw new InvalidOperationException($"Could not handle {packetClass.Identifier.NormalizeWhitespace().ToFullString()}.{parameter.Name}");
                arguments.Add(value);
            }
            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;
        return new AttributeInfo
        (
            attribute,
            semanticModel.GetTypeInfo(attribute).Type?.ToString()!,
            arguments,
            namedArguments
        );
    }
}
\ No newline at end of file

Do not follow this link