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; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 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; } } } """; public void Initialize(GeneratorInitializationContext context) { // Register the attribute source context.RegisterForPostInitialization(i => i.AddSource($"{AttributeName}.g.cs", AttributeSourceCode)); } 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); if (foundAttribute == null) continue; // not my type var structIsLittleEndian = GetStructIsLittleEndian(foundAttribute); HandleStruct(context, structDeclaration, semanticModel, structIsLittleEndian); } } } private static void HandleStruct(GeneratorExecutionContext context, TypeDeclarationSyntax structDeclaration, SemanticModel semanticModel, bool structIsLittleEndian) { 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)) ? Token(SyntaxKind.InternalKeyword) : Token(SyntaxKind.PublicKeyword); 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"); if (structDeclaration.Members.Any(m => m.IsKind(SyntaxKind.PropertyDeclaration))) throw new InvalidUsageException("struct cannot have properties"); var fieldDeclarations = structDeclaration.Members .Where(m => m.IsKind(SyntaxKind.FieldDeclaration)).OfType(); var generatedCode = CompilationUnit() .WithUsings(List([ UsingDirective(IdentifierName("System")), UsingDirective(IdentifierName("System.Buffers.Binary")) ])) .WithMembers(List([ FileScopedNamespaceDeclaration(IdentifierName(structNamespace)), StructDeclaration(structType.Name) .WithModifiers(TokenList([accessibilityModifier, Token(SyntaxKind.PartialKeyword)])) .WithMembers(GenerateStructProperties(fieldDeclarations, semanticModel, structIsLittleEndian)) ])) .NormalizeWhitespace() .ToFullString(); context.AddSource( $"{structNamespace}.{structType.Name}.g.cs", SourceText.From(generatedCode, Encoding.UTF8) ); } private static SyntaxList GenerateStructProperties( IEnumerable fieldDeclarations, SemanticModel semanticModel, bool structIsLittleEndian) { var result = new List(); 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(); result.Add(GenerateProperty(typeName, structIsLittleEndian, fieldName)); } return new SyntaxList(result); } private static PropertyDeclarationSyntax GenerateProperty(string typeName, bool structIsLittleEndian, string fieldName) { var propertyName = GeneratePropertyName(fieldName); var fieldIdentifier = IdentifierName(fieldName); ExpressionSyntax condition = MemberAccessExpression( kind: SyntaxKind.SimpleMemberAccessExpression, expression: IdentifierName("BitConverter"), name: IdentifierName("IsLittleEndian") ); if (!structIsLittleEndian) condition = PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, condition); var reverseEndiannessMethod = MemberAccessExpression( kind: SyntaxKind.SimpleMemberAccessExpression, expression: IdentifierName("BinaryPrimitives"), name: IdentifierName("ReverseEndianness") ); var valueIdentifier = IdentifierName("value"); return PropertyDeclaration(ParseTypeName(typeName), propertyName) .WithModifiers(TokenList([Token(SyntaxKind.PublicKeyword)])) .WithAccessorList(AccessorList(List([ AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(ConditionalExpression( condition: condition, whenTrue: fieldIdentifier, whenFalse: InvocationExpression( expression: reverseEndiannessMethod, argumentList: ArgumentList(SingletonSeparatedList( Argument(fieldIdentifier) )) ) ))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(AssignmentExpression( kind: SyntaxKind.SimpleAssignmentExpression, left: fieldIdentifier, right: ConditionalExpression( condition: condition, whenTrue: valueIdentifier, whenFalse: InvocationExpression( expression: reverseEndiannessMethod, argumentList: ArgumentList(SingletonSeparatedList( Argument(valueIdentifier) )) ) ) ))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) ]))); } private static SyntaxToken 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 Identifier(propertyName); } private static AttributeSyntax? GetEndiannessAttribute(SyntaxNode 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; } }