using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace EndiannessSourceGenerator; internal class DebugMeException(string message) : Exception(message); internal class InvalidUsageException(string message) : Exception(message); [Generator] public class StructEndiannessSourceGenerator : ISourceGenerator { private const string Namespace = "EndiannessSourceGenerator"; private const string AttributeName = "StructEndiannessAttribute"; private const string IsLittleEndianProperty = "IsLittleEndian"; private const string AttributeSourceCode = $$""" // namespace {{Namespace}} { [System.AttributeUsage(System.AttributeTargets.Struct)] public class {{AttributeName}}: System.Attribute { public required bool {{IsLittleEndianProperty}} { get; init; } } } """; private const string UsingDeclarations = """ using System; using System.Buffers.Binary; """; public void Initialize(GeneratorInitializationContext context) { // Register the attribute source context.RegisterForPostInitialization(i => { i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode); }); // context.RegisterForSyntaxNotifications(() => new SyntaxCon); } private readonly SymbolDisplayFormat _namespacedNameFormat = new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); // TODO: generate syntax tree with roslyn to get rid of string wrangling and so code is properly formatted public void Execute(GeneratorExecutionContext context) { var treesWithStructsWithAttributes = context.Compilation.SyntaxTrees .Where(st => st.GetRoot().DescendantNodes() .OfType() .Any(p => p.DescendantNodes() .OfType() .Any())) .ToList(); foreach (var tree in treesWithStructsWithAttributes) { var semanticModel = context.Compilation.GetSemanticModel(tree); var structsWithAttributes = tree.GetRoot().DescendantNodes() .OfType() .Where(cd => cd.DescendantNodes() .OfType() .Any()); foreach (var structDeclaration in structsWithAttributes) { var foundAttribute = GetEndiannessAttribute(structDeclaration, semanticModel); // not my type if (foundAttribute == null) continue; HandleStruct(context, structDeclaration, semanticModel, foundAttribute); } } } private static void HandleStruct(GeneratorExecutionContext context, StructDeclarationSyntax structDeclaration, SemanticModel semanticModel, AttributeSyntax foundAttribute) { var isPartial = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); if (!isPartial) throw new InvalidUsageException("struct is not marked partial"); var accessibilityModifier = structDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.InternalKeyword)) ? "internal" : "public"; var structType = semanticModel.GetDeclaredSymbol(structDeclaration); if (structType == null) throw new DebugMeException("struct type info is null"); var structNamespace = structType.ContainingNamespace?.ToDisplayString(); if (structNamespace == null) throw new InvalidUsageException("struct has to be contained in a namespace"); var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute); var generatedCode = new StringBuilder(); generatedCode.AppendLine(UsingDeclarations); generatedCode.AppendLine($"namespace {structNamespace};"); generatedCode.AppendLine($$"""{{accessibilityModifier}} partial struct {{structType.Name}} {"""); var hasProperties = structDeclaration.Members .Any(m => m.IsKind(SyntaxKind.PropertyDeclaration)); if (hasProperties) throw new InvalidUsageException("struct cannot have properties"); var fieldDeclarations = structDeclaration.Members .Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType(); GenerateStructProperties(generatedCode, fieldDeclarations, semanticModel, structIsLittleEndian); generatedCode.AppendLine("}"); // end of struct context.AddSource($"{structNamespace}.{structType.Name}.g.cs", SourceText.From(generatedCode.ToString(), Encoding.UTF8)); } private static void GenerateStructProperties(StringBuilder generatedCode, IEnumerable fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian) { foreach (var field in fieldDeclarations) { if (!field.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) throw new InvalidUsageException("fields have to be private"); var variableDeclaration = field.DescendantNodes() .OfType() .FirstOrDefault(); if (variableDeclaration == null) throw new DebugMeException("variable declaration of field declaration null"); var variableTypeInfo = semanticModel.GetTypeInfo(variableDeclaration.Type).Type; if (variableTypeInfo == null) throw new DebugMeException("variable type info of field declaration null"); var typeName = variableTypeInfo.ToDisplayString(); var fieldName = variableDeclaration.Variables.First().Identifier.ToString(); var propertyName = GeneratePropertyName(fieldName); GenerateProperty(generatedCode, typeName, propertyName, structIsLittleEndian, fieldName); } } private static void GenerateProperty(StringBuilder generatedCode, string typeName, string propertyName, bool structIsLittleEndian, string fieldName) { generatedCode.AppendLine($$"""public {{typeName}} {{propertyName}} {"""); var maybeNegator = structIsLittleEndian ? string.Empty : "!"; var sameEndiannessExpression = $"{maybeNegator}BitConverter.IsLittleEndian"; generatedCode.AppendLine($"get => {sameEndiannessExpression}"); generatedCode.AppendLine($" ? {fieldName}"); generatedCode.AppendLine($" : BinaryPrimitives.ReverseEndianness({fieldName});"); generatedCode.AppendLine($"set => {fieldName} = {sameEndiannessExpression}"); generatedCode.AppendLine(" ? value"); generatedCode.AppendLine(" : BinaryPrimitives.ReverseEndianness(value);"); generatedCode.AppendLine("}"); // end of property } private static string GeneratePropertyName(string fieldName) { var propertyName = fieldName; if (propertyName.StartsWith("_")) propertyName = propertyName.Substring(1); if (!char.IsLetter(propertyName, 0) || char.IsUpper(propertyName, 0)) throw new InvalidUsageException("field names have to start with a lower case letter"); propertyName = propertyName.Substring(0, 1).ToUpperInvariant() + propertyName.Substring(1); return propertyName; } private static AttributeSyntax? GetEndiannessAttribute(StructDeclarationSyntax structDeclaration, SemanticModel semanticModel) { AttributeSyntax? foundAttribute = null; foreach (var attributeSyntax in structDeclaration.DescendantNodes().OfType()) { var attributeTypeInfo = semanticModel.GetTypeInfo(attributeSyntax).Type; if (attributeTypeInfo == null) throw new DebugMeException("attribute type info is null"); if (attributeTypeInfo.ContainingNamespace?.Name != Namespace) continue; if (attributeTypeInfo.Name != AttributeName) continue; foundAttribute = attributeSyntax; break; } return foundAttribute; } private static bool GetStructIsLittleEndian(AttributeSyntax foundAttribute) { var endiannessArguments = foundAttribute.ArgumentList; if (endiannessArguments == null) throw new InvalidUsageException("endianness attribute has no arguments"); var isLittleEndianArgumentSyntax = endiannessArguments.Arguments .FirstOrDefault(argumentSyntax => argumentSyntax.NameEquals?.Name.Identifier.ToString() == IsLittleEndianProperty); if (isLittleEndianArgumentSyntax == null) throw new InvalidUsageException("endianness attribute argument not found"); bool? structIsLittleEndian = isLittleEndianArgumentSyntax.Expression.Kind() switch { SyntaxKind.FalseLiteralExpression => false, SyntaxKind.TrueLiteralExpression => true, SyntaxKind.DefaultLiteralExpression => false, _ => throw new InvalidUsageException($"{IsLittleEndianProperty} has to be set with a literal") }; return structIsLittleEndian.Value; } }