// // 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.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using NosSmooth.PacketSerializersGenerator.AttributeGenerators; using NosSmooth.PacketSerializersGenerator.Data; using NosSmooth.PacketSerializersGenerator.Errors; using NosSmooth.PacketSerializersGenerator.Extensions; namespace NosSmooth.PacketSerializersGenerator; /// /// Generates ITypeGenerator for packets that are marked with NosSmooth.Packets.Attributes.GenerateSerializerAttribute. /// /// /// The packets to create serializer for have to be records that specify PacketIndices in the constructor. /// [Generator] public class SourceGenerator : ISourceGenerator { /// /// Initializes a new instance of the class. /// public SourceGenerator() { _generators = new List ( new IParameterGenerator[] { new PacketIndexAttributeGenerator(), new PacketListIndexAttributeGenerator(), new PacketContextListAttributeGenerator(), } ); } private readonly List _generators; /// public void Initialize(GeneratorInitializationContext context) { } private IEnumerable GetPacketRecords(Compilation compilation, SyntaxTree tree) { var semanticModel = compilation.GetSemanticModel(tree); var root = tree.GetRoot(); return root .DescendantNodes() .OfType() .Where ( x => x.AttributeLists.Any (y => y.ContainsAttribute(semanticModel, Constants.GenerateSourceAttributeClass)) ); } /// public void Execute(GeneratorExecutionContext context) { var packetRecords = context.Compilation.SyntaxTrees .SelectMany(x => GetPacketRecords(context.Compilation, x)); foreach (var packetRecord in packetRecords) { if (packetRecord is not null) { using var stringWriter = new StringWriter(); using var writer = new IndentedTextWriter(stringWriter, " "); var generatedResult = GeneratePacketSerializer(writer, context.Compilation, packetRecord); if (generatedResult is not null) { if (generatedResult is DiagnosticError diagnosticError) { context.ReportDiagnostic ( Diagnostic.Create ( new DiagnosticDescriptor ( diagnosticError.Id, diagnosticError.Title, diagnosticError.MessageFormat, "Serialization", DiagnosticSeverity.Error, true ), Location.Create(diagnosticError.Tree, diagnosticError.Span), diagnosticError.Parameters.ToArray() ) ); } else if (generatedResult is not null) { throw new Exception(generatedResult.Message); } continue; } context.AddSource ( $"{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs", stringWriter.GetStringBuilder().ToString() ); } } } private IError? GeneratePacketSerializer (IndentedTextWriter textWriter, Compilation compilation, RecordDeclarationSyntax packetClass) { var semanticModel = compilation.GetSemanticModel(packetClass.SyntaxTree); var name = packetClass.Identifier.NormalizeWhitespace().ToFullString(); var @namespace = packetClass.GetPrefix(); var constructor = (ParameterListSyntax?)packetClass.ChildNodes() .FirstOrDefault(x => x.IsKind(SyntaxKind.ParameterList)); if (constructor is null) { return new DiagnosticError ( "SG0001", "Packet without constructor", "The packet class {0} does not have any constructors to use for packet serializer.", packetClass.SyntaxTree, packetClass.FullSpan, new List ( new[] { packetClass.Identifier.NormalizeWhitespace().ToFullString() } ) ); } var parameters = constructor.Parameters; var orderedParameters = new List(); int constructorIndex = 0; foreach (var parameter in parameters) { var createError = CreateParameterInfo ( packetClass, parameter, semanticModel, constructorIndex, out var parameterInfo ); if (createError is not null) { return createError; } if (parameterInfo is not null) { orderedParameters.Add(parameterInfo); } constructorIndex++; } orderedParameters = orderedParameters.OrderBy(x => x.PacketIndex).ToList(); var packetInfo = new PacketInfo ( compilation, packetClass, semanticModel, new Parameters(orderedParameters), @namespace, name ); var generator = new PacketConverterGenerator(packetInfo, _generators); var generatorError = generator.Generate(textWriter); return generatorError; } private IError? CreateParameterInfo ( RecordDeclarationSyntax packet, ParameterSyntax parameter, SemanticModel semanticModel, int constructorIndex, out ParameterInfo? parameterInfo ) { var name = packet.Identifier.NormalizeWhitespace().ToFullString(); parameterInfo = null; var attributes = parameter.AttributeLists .Where(x => x.ContainsAttribute(semanticModel, Constants.PacketAttributesClassRegex)) .SelectMany ( x => x.Attributes.Where ( y => Regex.IsMatch (semanticModel.GetTypeInfo(y).Type?.ToString()!, Constants.PacketAttributesClassRegex) ) ) .ToList(); if (attributes.Count == 0) { return new DiagnosticError ( "SG0003", "Packet constructor parameter without packet attribute", "Could not find PacketIndexAttribute on {0} parameter in class {1}. Parameters without PacketIndexAttribute aren't allowed.", parameter.SyntaxTree, parameter.FullSpan, new List ( new[] { parameter.Identifier.NormalizeWhitespace().ToFullString(), name } ) ); } var attribute = attributes.First(); var index = ushort.Parse(attribute.ArgumentList!.Arguments[0].GetValue(semanticModel)!.ToString()); List attributeInfos = attributes .Select(x => CreateAttributeInfo(x, semanticModel)) .ToList(); parameterInfo = new ParameterInfo ( parameter, semanticModel.GetTypeInfo(parameter.Type!).Type!, parameter.Type is NullableTypeSyntax, attributeInfos, parameter.Identifier.NormalizeWhitespace().ToFullString(), constructorIndex, index ); return null; } private AttributeInfo CreateAttributeInfo(AttributeSyntax attribute, SemanticModel semanticModel) { var namedArguments = new Dictionary(); var arguments = new List(); foreach (var argument in attribute.ArgumentList!.Arguments) { var argumentName = argument.NameEquals?.Name.Identifier.NormalizeWhitespace().ToFullString(); var value = argument.GetValue(semanticModel); if (argumentName is not null) { namedArguments.Add(argumentName, value); } else { arguments.Add(value); } } return new AttributeInfo ( attribute, semanticModel.GetTypeInfo(attribute).Type?.ToString()!, arguments, namedArguments ); } }