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;
}
}