~ruther/NosSmooth

6f8822252833b20c112019b294c7f8b3e3cd4b7a — František Boháček 3 years ago 046116f
feat: add support for conditional packet attribute
A Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalIndexAttributeGenerator.cs +299 -0
@@ 0,0 1,299 @@
//
//  PacketConditionalIndexAttributeGenerator.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;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Data;
using NosSmooth.PacketSerializersGenerator.Errors;
using NosSmooth.PacketSerializersGenerator.Extensions;

namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators;

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

    /// <inheritdoc />
    public bool ShouldHandle(ParameterInfo parameter)
        => parameter.Attributes.Any(x => x.FullName == PacketConditionalIndexAttributeFullName);

    /// <inheritdoc />
    public IError? CheckParameter(PacketInfo packet, ParameterInfo parameter)
    {
        if (!parameter.Nullable)
        {
            return new DiagnosticError
            (
                "SGNull",
                "Conditional parameters must be nullable",
                "The parameter {0} in {1} has to be nullable, because it is conditional.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>(new[] { parameter.Name, packet.Name })
            );
        }

        if (parameter.Attributes.Any(x => x.FullName != PacketConditionalIndexAttributeFullName))
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet constructor parameter with multiple packet attributes",
                "Found multiple packet attributes of multiple types on parameter {0} in {1}. PacketConditionalIndexAttribute supports multiple attributes of the same type only.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        // Check that all attributes have the same data. (where the same data need to be)
        var firstAttribute = parameter.Attributes.First();
        if (parameter.Attributes.Any
            (
                x =>
                {
                    var index = x.GetIndexedValue<int>(0);
                    if (index != parameter.PacketIndex)
                    {
                        return true;
                    }

                    foreach (var keyValue in x.NamedAttributeArguments)
                    {
                        if (!firstAttribute.NamedAttributeArguments.ContainsKey(keyValue.Key))
                        {
                            return true;
                        }

                        if (firstAttribute.NamedAttributeArguments[keyValue.Key] != keyValue.Value)
                        {
                            return true;
                        }
                    }

                    return false;
                }
            ))
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet constructor parameter with multiple conflicting attribute data.",
                "Found multiple packet attributes of multiple types on parameter {0} in {1} with conflicting data. Index, IsOptional, InnerSeparator, AfterSeparator all have to be the same for each attribute.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        var mismatchedAttribute = parameter.Attributes.FirstOrDefault
        (
            x => x.IndexedAttributeArguments.Count < 4 ||
                (x.IndexedAttributeArguments[3].IsArray &&
                    (
                        (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
                        ?.Initializer is null ||
                        (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
                        ?.Initializer?.Expressions.Count == 0
                    )
                )
        );
        if (mismatchedAttribute is not null)
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet conditional attribute without matching values",
                "Found PacketConditionalIndexAttribute without matchValues parameters set on {0} in {1}. At least one parameter has to be specified.",
                mismatchedAttribute.Attribute.SyntaxTree,
                mismatchedAttribute.Attribute.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        return ParameterChecker.CheckOptionalIsNullable(packet, parameter);
    }

    private string BuildAttributeIfPart(AttributeInfo attribute, string prefix)
    {
        var conditionParameterName = attribute.GetIndexedValue<string>(1);
        var negate = attribute.GetIndexedValue<bool>(2);
        var values = attribute.GetParamsVisualValues(3);
        if (conditionParameterName is null || values is null)
        {
            throw new ArgumentException();
        }

        var inner = string.Join
            (" || ", values.Select(x => $"{prefix}{conditionParameterName.Trim('"')} == {x?.ToString() ?? "null"}"));
        return (negate ? "!(" : string.Empty) + inner + (negate ? ")" : string.Empty);
    }

    private string BuildParameterIfStatement(ParameterInfo parameter, string prefix)
    {
        var ifInside = string.Empty;
        foreach (var attribute in parameter.Attributes)
        {
            ifInside += BuildAttributeIfPart(attribute, prefix);
        }

        return $"if ({ifInside})";
    }

    /// <inheritdoc />
    public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
    {
        bool pushedLevel = false;
        var generator = new ConverterSerializationGenerator(textWriter);
        var parameter = packetInfo.Parameters.Current;
        var attributes = parameter.Attributes;
        var attribute = attributes.First();

        // begin conditional if
        textWriter.WriteLine(BuildParameterIfStatement(parameter, "obj."));
        textWriter.WriteLine("{");
        textWriter.Indent++;

        if (parameter.IsOptional())
        {
            textWriter.WriteLine($"if (obj.{parameter.GetVariableName()} is not null)");
            textWriter.WriteLine("{");
            textWriter.Indent++;
        }

        // register after separator
        var afterSeparator = attribute.GetNamedValue<char?>("AfterSeparator", null);
        if (afterSeparator is not null)
        {
            generator.SetAfterSeparatorOnce((char)afterSeparator);
        }

        // push inner separator level
        var innerSeparator = attribute.GetNamedValue<char?>("InnerSeparator", null);
        if (innerSeparator is not null)
        {
            generator.PushLevel((char)innerSeparator);
            pushedLevel = true;
        }

        // serialize, check the error.
        generator.SerializeAndCheck(parameter);

        // pop inner separator level
        if (pushedLevel)
        {
            generator.PopLevel();
        }

        // end optional if
        if (parameter.IsOptional())
        {
            textWriter.Indent--;
            textWriter.WriteLine("}");
        }

        // end conditional if
        textWriter.Indent--;
        textWriter.WriteLine("}");

        return null;
    }

    /// <inheritdoc />
    public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
    {
        bool pushedLevel = false;
        var generator = new ConverterDeserializationGenerator(textWriter);
        var parameter = packetInfo.Parameters.Current;
        var attribute = parameter.Attributes.First();

        generator.DeclareLocalVariable(parameter);

        // begin conditional if
        textWriter.WriteLine(BuildParameterIfStatement(parameter, string.Empty));
        textWriter.WriteLine("{");
        textWriter.Indent++;

        // add optional if
        if (parameter.IsOptional())
        {
            generator.StartOptionalCheck(parameter, packetInfo.Name);
        }

        var afterSeparator = attribute.GetNamedValue<char?>("AfterSeparator", null);
        if (afterSeparator is not null)
        {
            generator.SetAfterSeparatorOnce((char)afterSeparator);
        }

        var innerSeparator = attribute.GetNamedValue<char?>("InnerSeparator", null);
        if (innerSeparator is not null)
        {
            generator.PushLevel((char)innerSeparator);
            pushedLevel = true;
        }

        generator.DeserializeAndCheck
            ($"{packetInfo.Namespace}.{packetInfo.Name}", parameter, packetInfo.Parameters.IsLast);

        if (!parameter.Nullable)
        {
            generator.CheckNullError(parameter.GetResultVariableName(), parameter.Name);
        }

        generator.AssignLocalVariable(parameter, false);
        if (pushedLevel)
        {
            generator.ReadToLastToken();
            generator.PopLevel();
        }

        // end is last token if body
        if (parameter.IsOptional())
        {
            generator.EndOptionalCheck(parameter);
        }

        // end conditional if
        textWriter.Indent--;
        textWriter.WriteLine("}");
        textWriter.WriteLine("else");
        textWriter.WriteLine("{");
        textWriter.Indent++;
        textWriter.WriteLine($"{parameter.GetVariableName()} = null;");
        textWriter.Indent--;
        textWriter.WriteLine("}");

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

M Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs => Core/NosSmooth.PacketSerializersGenerator/Data/AttributeInfo.cs +12 -3
@@ 19,6 19,15 @@ public record AttributeInfo
(
    AttributeSyntax Attribute,
    string FullName,
    IReadOnlyList<object?> IndexedAttributeArguments,
    IReadOnlyDictionary<string, object?> NamedAttributeArguments
);
\ No newline at end of file
    IReadOnlyList<AttributeArgumentInfo> IndexedAttributeArguments,
    IReadOnlyDictionary<string, AttributeArgumentInfo> NamedAttributeArguments
);

/// <summary>
/// The attribute argument information.
/// </summary>
/// <param name="Argument">The argument syntax.</param>
/// <param name="IsArray">Whether the attribute argument is an array.</param>
/// <param name="RealValue">The real parsed value of the argument.</param>
/// <param name="VisualValue">The visual value of the argument.</param>
public record AttributeArgumentInfo(AttributeArgumentSyntax Argument, bool IsArray, object? RealValue, string VisualValue);
\ No newline at end of file

M Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs => Core/NosSmooth.PacketSerializersGenerator/Data/Parameters.cs +2 -1
@@ 4,6 4,7 @@
//  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 NosSmooth.PacketSerializersGenerator.AttributeGenerators;
using NosSmooth.PacketSerializersGenerator.Extensions;

namespace NosSmooth.PacketSerializersGenerator.Data;


@@ 61,7 62,7 @@ public class Parameters
    {
        for (int i = CurrentIndex + 1; i < List.Count; i++)
        {
            if (!List[i].IsOptional())
            if (!List[i].IsOptional() && List[i].Attributes.All(x => x.FullName != PacketConditionalIndexAttributeGenerator.PacketConditionalIndexAttributeFullName))
            {
                return false;
            }

M Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs => Core/NosSmooth.PacketSerializersGenerator/Extensions/AttributeInfoExtensions.cs +52 -4
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Data;

namespace NosSmooth.PacketSerializersGenerator.Extensions;


@@ 28,10 29,10 @@ public static class AttributeInfoExtensions
        {
            if (typeof(TValue) == typeof(string))
            {
                return (TValue?)(object?)value?.ToString();
                return (TValue?)(object?)value.VisualValue;
            }

            return (TValue?)value;
            return (TValue?)value.RealValue;
        }

        return @default;


@@ 50,9 51,56 @@ public static class AttributeInfoExtensions

        if (typeof(TValue) == typeof(string))
        {
            return (TValue?)(object?)value?.ToString();
            return (TValue?)(object?)value?.VisualValue;
        }

        return (TValue?)value;
        return (TValue?)value.RealValue;
    }

    /// <summary>
    /// Gets visual values of params parameters in the constructor.
    /// </summary>
    /// <param name="attributeInfo">The attribute info.</param>
    /// <param name="startingIndex">The index the values start at.</param>
    /// <returns>A list containing all the values.</returns>
    public static IReadOnlyList<string> GetParamsVisualValues(this AttributeInfo attributeInfo, int startingIndex)
    {
        if (attributeInfo.IndexedAttributeArguments.Count - 1 < startingIndex)
        {
            return Array.Empty<string>();
        }

        if (attributeInfo.IndexedAttributeArguments[startingIndex].IsArray)
        {
            return attributeInfo.IndexedAttributeArguments[startingIndex].GetArrayVisualValues();
        }

        return attributeInfo.IndexedAttributeArguments
            .Skip(startingIndex)
            .Select(x => x.VisualValue)
            .ToArray();
    }

    /// <summary>
    /// Gets the visual values of the array.
    /// </summary>
    /// <param name="attributeArgumentInfo">The attribute argument.</param>
    /// <returns>The list of the elements.</returns>
    public static IReadOnlyList<string> GetArrayVisualValues(this AttributeArgumentInfo attributeArgumentInfo)
    {
        var arrayCreation = attributeArgumentInfo.Argument.Expression as ArrayCreationExpressionSyntax;
        if (arrayCreation is null)
        {
            throw new ArgumentException($"The given attribute argument is not an array creation.", nameof(attributeArgumentInfo));
        }

        if (arrayCreation.Initializer is null)
        {
            return Array.Empty<string>();
        }

        return arrayCreation.Initializer.Expressions
            .Select(x => x.ToString())
            .ToArray();
    }
}
\ No newline at end of file

M Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/PacketConverterGenerator.cs +6 -0
@@ 5,6 5,7 @@
//  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.AttributeGenerators;
using NosSmooth.PacketSerializersGenerator.Data;
using NosSmooth.PacketSerializersGenerator.Errors;


@@ 38,6 39,10 @@ public class PacketConverterGenerator
    /// <returns>An error, if any.</returns>
    public IError? Generate(IndentedTextWriter textWriter)
    {
        var usings = _packetInfo.PacketRecord.SyntaxTree.GetRoot()
            .DescendantNodes()
            .OfType<UsingDirectiveSyntax>();
        var usingsString = string.Join("\n", usings.Select(x => x.ToString()));
        textWriter.WriteLine
        (
            @$"// <auto-generated/>


@@ 49,6 54,7 @@ using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Errors;
using NosSmooth.Packets;
using Remora.Results;
{usingsString}

namespace {_packetInfo.Namespace}.Generated;


M Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Core/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +8 -4
@@ 36,8 36,10 @@ public class SourceGenerator : ISourceGenerator
            new IParameterGenerator[]
            {
                new PacketIndexAttributeGenerator(),
                new PacketGreedyIndexAttributeGenerator(),
                new PacketListIndexAttributeGenerator(),
                new PacketContextListAttributeGenerator(),
                new PacketConditionalIndexAttributeGenerator(),
            }
        );
    }


@@ 47,6 49,7 @@ public class SourceGenerator : ISourceGenerator
    /// <inheritdoc />
    public void Initialize(GeneratorInitializationContext context)
    {
        // SpinWait.SpinUntil(() => Debugger.IsAttached);
    }

    private IEnumerable<RecordDeclarationSyntax> GetPacketRecords(Compilation compilation, SyntaxTree tree)


@@ 257,21 260,22 @@ public class SourceGenerator : ISourceGenerator

    private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel)
    {
        var namedArguments = new Dictionary<string, object?>();
        var arguments = new List<object?>();
        var namedArguments = new Dictionary<string, AttributeArgumentInfo>();
        var arguments = new List<AttributeArgumentInfo>();

        foreach (var argument in attribute.ArgumentList!.Arguments)
        {
            var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString();
            var value = argument.GetValue(semanticModel);
            bool isArray = argument.Expression is ArrayCreationExpressionSyntax;

            if (argumentName is not null)
            {
                namedArguments.Add(argumentName, value);
                namedArguments.Add(argumentName, new AttributeArgumentInfo(argument, isArray, value, argument.ToString()));
            }
            else
            {
                arguments.Add(value);
                arguments.Add(new AttributeArgumentInfo(argument, isArray, value, argument.ToString()));
            }
        }


A Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketConditionalIndexAttribute.cs +26 -0
@@ 0,0 1,26 @@
//
//  PacketConditionalIndexAttribute.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.Packets.Attributes;

/// <summary>
/// Attribute for marking packet properties that may appear only in specific conditions.
/// </summary>
public class PacketConditionalIndexAttribute : PacketIndexAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketConditionalIndexAttribute"/> class.
    /// You can use this attribute multiple times on one parameter.
    /// </summary>
    /// <param name="index">The position in the packet.</param>
    /// <param name="conditionParameter">What parameter to check. (it has to precede this one).</param>
    /// <param name="negate">Whether to negate the match values (not equals).</param>
    /// <param name="matchValues">The values that mean this parameter is present.</param>
    public PacketConditionalIndexAttribute(ushort index, string conditionParameter, bool negate = false, params object?[] matchValues)
        : base(index)
    {
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Common/NameString.cs => Core/NosSmooth.Packets/Common/NameString.cs +103 -0
@@ 0,0 1,103 @@
//
//  NameString.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.Net.NetworkInformation;
using System.Reflection;

namespace NosSmooth.Packets.Common;

/// <summary>
/// Represents name in the game replacing "^" for " ".
/// </summary>
public class NameString
{
    /// <summary>
    /// Gets the character used to separate words.
    /// </summary>
    public static char WordSeparator => '^';

    /// <summary>
    /// Creates <see cref="NameString"/> instance from the given name retrieved from a packet.
    /// </summary>
    /// <param name="packetName">The name from the packet.</param>
    /// <returns>A name string instance.</returns>
    public static NameString FromPacket(string packetName)
    {
        return new NameString(packetName, true);
    }

    /// <summary>
    /// Creates <see cref="NameString"/> instance from the given name retrieved from a packet.
    /// </summary>
    /// <param name="packetName">The name from the packet.</param>
    /// <returns>A name string instance.</returns>
    public static NameString FromString(string packetName)
    {
        return new NameString(packetName, true);
    }

    private NameString(string name, bool packet)
    {
        if (packet)
        {
            PacketName = name;
            Name = name.Replace(WordSeparator, ' ');
        }
        else
        {
            Name = name;
            PacketName = name.Replace(' ', WordSeparator);
        }
    }

    /// <summary>
    /// The real name.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// The original name in the packet.
    /// </summary>
    public string PacketName { get; }

    /// <inheritdoc />
    public override string ToString()
    {
        return Name;
    }

    /// <summary>
    /// Converts name string to regular string.
    /// Returns the real name.
    /// </summary>
    /// <param name="nameString">The name string to convert.</param>
    /// <returns>The real name.</returns>
    public static implicit operator string(NameString nameString)
    {
        return nameString.Name;
    }

    /// <summary>
    /// Converts regular string to name string.
    /// </summary>
    /// <param name="name">The string to convert.</param>
    /// <returns>The name string.</returns>
    public static implicit operator NameString(string name)
    {
        return FromString(name);
    }

    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        if (!(obj is NameString nameString))
        {
            return false;
        }

        return Name.Equals(nameString.Name);
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs => Core/NosSmooth.Packets/Converters/Common/NameStringConverter.cs +42 -0
@@ 0,0 1,42 @@
//
//  NameStringConverter.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 NosSmooth.Packets.Common;
using NosSmooth.Packets.Converters.Basic;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Common;

/// <summary>
/// Converter of <see cref="NameString"/>.
/// </summary>
public class NameStringConverter : BaseTypeConverter<NameString>
{
    /// <inheritdoc />
    public override Result Serialize(NameString? obj, PacketStringBuilder builder)
    {
        if (obj is null)
        {
            builder.Append("-");
            return Result.FromSuccess();
        }

        builder.Append(obj.PacketName);
        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Result<NameString?> Deserialize(PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        if (!tokenResult.IsSuccess)
        {
            return Result<NameString?>.FromError(tokenResult);
        }

        return NameString.FromPacket(tokenResult.Entity.Token);
    }
}
\ No newline at end of file

M Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs +2 -0
@@ 10,6 10,7 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Converters.Basic;
using NosSmooth.Packets.Converters.Common;
using NosSmooth.Packets.Converters.Special;
using NosSmooth.Packets.Packets;



@@ 98,6 99,7 @@ public static class ServiceCollectionExtensions
            .AddTypeConverter<ULongTypeConverter>()
            .AddTypeConverter<LongTypeConverter>()
            .AddTypeConverter<StringTypeConverter>()
            .AddTypeConverter<NameStringConverter>()
            .AddTypeConverter<CharTypeConverter>();
    }


Do not follow this link