~ruther/NosSmooth

9fe8a75a1c1d2c5327f8a4509699f957cd31e54e — František Boháček 3 years ago 9b14d09
feat: add source generator for basic packet converter
A Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/IParameterGenerator.cs +43 -0
@@ 0,0 1,43 @@
//
//  IParameterGenerator.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 Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Errors;

namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators
{
    /// <summary>
    /// Generate serializer and deserializer method parts for the given constructor parameter.
    /// </summary>
    public interface IParameterGenerator
    {
        /// <summary>
        /// Check whether this generator should handle parameter with this attribute.
        /// </summary>
        /// <param name="attribute">The parameters attribute.</param>
        /// <returns>Whether to handle this parameter.</returns>
        public bool ShouldHandle(AttributeSyntax attribute);

        /// <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>
        /// <returns>The generated source code.</returns>
        public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo);

        /// <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>
        /// <returns>The generated source code.</returns>
        public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo);
    }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketIndexAttributeGenerator.cs +73 -0
@@ 0,0 1,73 @@
//
//  PacketIndexAttributeGenerator.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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Errors;

namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators
{
    /// <inheritdoc />
    public class PacketIndexAttributeGenerator : IParameterGenerator
    {
        /// <inheritdoc />
        public bool ShouldHandle(AttributeSyntax attribute)
            => attribute.Name.NormalizeWhitespace().ToFullString() == "PacketIndex";

        /// <inheritdoc />
        public IError? GenerateSerializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo)
        {
            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);
}}
");

            return null;
        }

        /// <inheritdoc />
        public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, RecordDeclarationSyntax recordDeclarationSyntax, ParameterInfo parameterInfo)
        {
            bool pushedLevel = false;

            if (parameterInfo.NamedAttributeArguments.ContainsKey("AfterSeparator") && parameterInfo.NamedAttributeArguments["AfterSeparator"] is not null)
            {
                textWriter.WriteLine($"stringEnumerator.SetAfterSeparatorOnce('{parameterInfo.NamedAttributeArguments["AfterSeparator"]}');");
            }

            if (parameterInfo.NamedAttributeArguments.ContainsKey("InnerSeparator") && parameterInfo.NamedAttributeArguments["InnerSeparator"] is not null)
            {
                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()}>(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 {parameterInfo.Name} = {parameterInfo.Name}Result.Entity;
");
            if (pushedLevel)
            {
                textWriter.WriteLine("stringEnumerator.PopLevel();");
            }

            return null;
        }
    }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Errors/DiagnosticError.cs => Core/NosSmooth.PacketSerializersGenerator/Errors/DiagnosticError.cs +25 -0
@@ 0,0 1,25 @@
//
//  DiagnosticError.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.Text;

namespace NosSmooth.PacketSerializersGenerator.Errors;

/// <summary>
/// The diagnostic error.
/// </summary>
/// <param name="Id"></param>
/// <param name="Title"></param>
/// <param name="MessageFormat"></param>
/// <param name="Tree"></param>
/// <param name="Span"></param>
/// <param name="Parameters"></param>
public record DiagnosticError(string Id, string Title, string MessageFormat, SyntaxTree Tree, TextSpan Span, List<object?> Parameters) : IError
{
    /// <inheritdoc />
    public string Message => Title;
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Errors/IError.cs => Core/NosSmooth.PacketSerializersGenerator/Errors/IError.cs +18 -0
@@ 0,0 1,18 @@
//
//  IError.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.Errors;

/// <summary>
/// Base error type.
/// </summary>
public interface IError
{
    /// <summary>
    /// Gets the message.
    /// </summary>
    public string Message { get; }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/Extensions/SyntaxNodeExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/SyntaxNodeExtensions.cs +60 -0
@@ 0,0 1,60 @@
//
//  SyntaxNodeExtensions.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;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace NosSmooth.PacketSerializersGenerator.Extensions;

/// <summary>
/// Extensions for <see cref="SyntaxNode"/>.
/// </summary>
public static class SyntaxNodeExtensions
{
    /// <summary>
    /// Gets the prefix of the given member (class or namespace).
    /// </summary>
    /// <param name="member">The member to get prefix of.</param>
    /// <returns>The full name.</returns>
    public static string GetPrefix(this SyntaxNode? member)
    {
        if (member is null)
        {
            return string.Empty;
        }

        StringBuilder sb = new StringBuilder();
        SyntaxNode node = member;

        while (node.Parent != null)
        {
            node = node.Parent;

            if (node is NamespaceDeclarationSyntax)
            {
                var namespaceDeclaration = (NamespaceDeclarationSyntax)node;

                sb.Insert(0, ".");
                sb.Insert(0, namespaceDeclaration.Name.ToString());
            }
            else if (node is FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclarationSyntax)
            {
                sb.Insert(0, ".");
                sb.Insert(0, fileScopedNamespaceDeclarationSyntax.Name.ToString());
            }
            else if (node is ClassDeclarationSyntax)
            {
                var classDeclaration = (ClassDeclarationSyntax)node;

                sb.Insert(0, ".");
                sb.Insert(0, classDeclaration.Identifier.ToString());
            }
        }

        return sb.ToString().Trim('.');
    }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/IsExternalInit.cs => Core/NosSmooth.PacketSerializersGenerator/IsExternalInit.cs +15 -0
@@ 0,0 1,15 @@
//
//  IsExternalInit.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 System.Runtime.CompilerServices
{
    /// <summary>
    /// Dummy.
    /// </summary>
    public class IsExternalInit
    {
    }
}
\ No newline at end of file

A Core/NosSmooth.PacketSerializersGenerator/NosSmooth.PacketSerializersGenerator.csproj => Core/NosSmooth.PacketSerializersGenerator/NosSmooth.PacketSerializersGenerator.csproj +22 -0
@@ 0,0 1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <IsRoslynComponent>true</IsRoslynComponent>
        <IncludeCopyLocalFilesOutputGroup>true</IncludeCopyLocalFilesOutputGroup>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
        <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
        <EmbeddedResource Include="$(PkgRemora_Results)\lib\netstandard2.0\*.dll" Visible="false" />
    </ItemGroup>

</Project>

A Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs => Core/NosSmooth.PacketSerializersGenerator/PacketClassReceiver.cs +50 -0
@@ 0,0 1,50 @@
//
//  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/PacketSerializerGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketSerializerGenerator.cs +353 -0
@@ 0,0 1,353 @@
//
//  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;

/// <summary>
/// Generates ITypeGenerator for packets that are marked with NosSmooth.Packets.Attributes.GenerateSerializerAttribute.
/// </summary>
/// <remarks>
/// The packets to create serializer for have to be records that specify PacketIndices in the constructor.
/// </remarks>
[Generator]
public class PacketSerializerGenerator : ISourceGenerator
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSerializerGenerator"/> class.
    /// </summary>
    public PacketSerializerGenerator()
    {
        _generators = new List<IParameterGenerator>(new[] { new PacketIndexAttributeGenerator() });
    }

    private readonly List<IParameterGenerator> _generators;

    /// <inheritdoc />
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new PacketClassReceiver());
    }

    /// <inheritdoc />
    public void Execute(GeneratorExecutionContext context)
    {
        var syntaxReceiver = (PacketClassReceiver)context.SyntaxReceiver!;

        foreach (var packetClass in syntaxReceiver.PacketClasses)
        {
            if (packetClass is not null)
            {
                using var stringWriter = new StringWriter();
                var writer = new IndentedTextWriter(stringWriter, "    ");
                var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetClass);
                if (generatedResult is not null)
                {
                    if (generatedResult is DiagnosticError diagnosticError)
                    {
                        context.ReportDiagnostic(Diagnostic.Create
                            (
                                new DiagnosticDescriptor
                                (
                                    diagnosticError.Id,
                                    diagnosticError.Title,
                                    diagnosticError.MessageFormat,
                                    "Serialization",
                                    DiagnosticSeverity.Error,
                                    true
                                ),
                                Location.Create(diagnosticError.Tree, diagnosticError.Span),
                                diagnosticError.Parameters.ToArray()
                            )
                        );
                    }
                    else if (generatedResult is not null)
                    {
                        throw new Exception(generatedResult.Message);
                    }

                    continue;
                }

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

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

        var name = packetClass.Identifier.NormalizeWhitespace().ToFullString();
        var @namespace = packetClass.GetPrefix();

        var constructor = (ParameterListSyntax?)packetClass.ChildNodes()
            .FirstOrDefault(x => x.IsKind(SyntaxKind.ParameterList));

        if (constructor is null)
        {
            return new DiagnosticError(
                "SG0001",
                "Packet without constructor",
                "The packet class {0} does not have any constructors to use for packet serializer.",
                packetClass.SyntaxTree,
                packetClass.FullSpan,
                new List<object?>(new[]
                {
                    packetClass.Identifier.NormalizeWhitespace().ToFullString()
                })
            );
        }

        var parameters = constructor.Parameters;
        var orderedParameters = new List<ParameterInfo>();
        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<object?>(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<object?>(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<string, object?>();
            var arguments = new List<object?>();

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

    private IError? GenerateSerializer(IndentedTextWriter textWriter, RecordDeclarationSyntax packetClass, List<ParameterInfo> 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<ParameterInfo> parameters)
    {
        var lastIndex = parameters.FirstOrDefault()?.PacketIndex ?? 0;
        bool skipped = false;
        foreach (var parameter in parameters)
        {
            var skip = parameter.PacketIndex - lastIndex - 1;
            if (skip > 0)
            {
                if (!skipped)
                {
                    textWriter.WriteLine("Result<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);
}}");
            }

            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

A Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs => Core/NosSmooth.PacketSerializersGenerator/ParameterInfo.cs +39 -0
@@ 0,0 1,39 @@
//
//  ParameterInfo.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;

/// <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="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,
    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

Do not follow this link