AyCode.Core/AyCode.Core.Serializers.Sou.../AcBinarySourceGenerator.cs

416 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace AyCode.Core.Serializers.SourceGenerator;
/// <summary>
/// Generates IGeneratedBinaryWriter implementations for [AcBinarySerializable] types.
/// Also generates a ModuleInitializer that auto-registers all writers at startup.
/// </summary>
[Generator]
public class AcBinarySourceGenerator : IIncrementalGenerator
{
private const string AttributeName = "AyCode.Core.Serializers.Attributes.AcBinarySerializableAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeName,
predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax,
transform: static (ctx, _) => GetClassInfo(ctx))
.Where(static info => info != null);
context.RegisterSourceOutput(classDeclarations.Collect(),
static (spc, classes) => Execute(classes!, spc));
}
private static SerializableClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context)
{
if (!(context.TargetSymbol is INamedTypeSymbol typeSymbol))
return null;
// Skip nested types — generated writer class can't be placed inside containing type
if (typeSymbol.ContainingType != null)
return null;
var namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
? string.Empty
: typeSymbol.ContainingNamespace.ToDisplayString();
var properties = new List<PropInfo>();
foreach (var member in typeSymbol.GetMembers())
{
if (member is IPropertySymbol p &&
p.DeclaredAccessibility == Accessibility.Public &&
p.GetMethod != null && p.SetMethod != null &&
!p.IsIndexer && !p.IsStatic)
{
var hasIgnore = p.GetAttributes().Any(a =>
{
var name = a.AttributeClass?.Name ?? "";
return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute";
});
if (hasIgnore) continue;
properties.Add(new PropInfo(
p.Name,
p.Type.ToDisplayString(),
GetKind(p.Type),
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type)));
}
}
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
var isIId = typeSymbol.AllInterfaces.Any(i =>
i.IsGenericType &&
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
if (isIId)
properties.Sort((a, b) =>
{
var aIsId = a.Name == "Id" ? 0 : 1;
var bIsId = b.Name == "Id" ? 0 : 1;
if (aIsId != bIsId) return aIsId.CompareTo(bIsId);
return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
});
else
properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
return new SerializableClassInfo(namespaceName, typeSymbol.Name, typeSymbol.ToDisplayString(), properties);
}
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
{
if (classes.IsDefaultOrEmpty) return;
var valid = classes.Where(c => c != null).Cast<SerializableClassInfo>().ToList();
if (valid.Count == 0) return;
foreach (var ci in valid)
context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8));
context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8));
}
private static string GenWriter(SerializableClassInfo ci)
{
var sb = new StringBuilder(2048);
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.CompilerServices;");
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
sb.AppendLine();
if (!string.IsNullOrEmpty(ci.Namespace))
sb.AppendLine($"namespace {ci.Namespace};");
sb.AppendLine();
sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter");
sb.AppendLine("{");
sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();");
sb.AppendLine();
sb.AppendLine(" public void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
sb.AppendLine(" {");
sb.AppendLine($" var obj = Unsafe.As<{ci.FullTypeName}>(value);");
foreach (var p in ci.Properties)
{
sb.AppendLine();
EmitProp(sb, p, " ");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static void EmitProp(StringBuilder sb, PropInfo p, string i)
{
var a = $"obj.{p.Name}";
// Nullable value types always use markered path (need Null marker)
if (IsNullableVTKind(p.TypeKind))
{
sb.AppendLine($"{i}if ({a}.HasValue)");
sb.AppendLine($"{i}{{");
EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", i + " ");
sb.AppendLine($"{i}}}");
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);");
return;
}
// Markerless types: write raw value only, no type marker, no PropertySkip
// Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode
if (IsMarkerless(p.TypeKind))
{
EmitMarkerless(sb, p.TypeKind, a, i);
return;
}
// Non-markerless types: write WITH type marker byte (markered path)
switch (p.TypeKind)
{
case PropertyTypeKind.String:
sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);");
break;
case PropertyTypeKind.Complex:
case PropertyTypeKind.Collection:
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);");
}
else
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);");
break;
default:
EmitSkip(sb, p.TypeKind, a, i);
break;
}
}
/// <summary>
/// Returns true for property types that use markerless serialization in FastMode.
/// These types have ExpectedTypeCode at runtime — no type marker byte, no PropertySkip for defaults.
/// </summary>
private static bool IsMarkerless(PropertyTypeKind k) => k switch
{
PropertyTypeKind.Int32 or PropertyTypeKind.Int64 or PropertyTypeKind.Int16 or
PropertyTypeKind.Byte or PropertyTypeKind.UInt16 or PropertyTypeKind.UInt32 or PropertyTypeKind.UInt64 or
PropertyTypeKind.Double or PropertyTypeKind.Single or PropertyTypeKind.Decimal or
PropertyTypeKind.DateTime or PropertyTypeKind.Guid or
PropertyTypeKind.TimeSpan or PropertyTypeKind.DateTimeOffset => true,
_ => false
};
/// <summary>
/// Emits raw value only — no type marker, no PropertySkip.
/// Matches runtime WritePropertyMarkerless exactly.
/// </summary>
private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i)
{
switch (k)
{
case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteVarInt({a});"); break;
case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteVarLong({a});"); break;
case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break;
case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break;
case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteGuidBits({a});"); break;
case PropertyTypeKind.Byte: sb.AppendLine($"{i}context.WriteByte({a});"); break;
case PropertyTypeKind.Int16: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
case PropertyTypeKind.UInt16: sb.AppendLine($"{i}context.WriteRaw({a});"); break;
case PropertyTypeKind.UInt32: sb.AppendLine($"{i}context.WriteVarUInt({a});"); break;
case PropertyTypeKind.UInt64: sb.AppendLine($"{i}context.WriteVarULong({a});"); break;
case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break;
case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break;
}
}
private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string i)
{
switch (k)
{
case PropertyTypeKind.Int32:
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}");
break;
case PropertyTypeKind.Int64:
sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}");
break;
case PropertyTypeKind.Boolean:
sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);");
break;
case PropertyTypeKind.Double:
sb.AppendLine($"{i}if ({a} == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a}); }}");
break;
case PropertyTypeKind.Single:
sb.AppendLine($"{i}if ({a} == 0f) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a}); }}");
break;
case PropertyTypeKind.Decimal:
sb.AppendLine($"{i}if ({a} == 0m) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a}); }}");
break;
case PropertyTypeKind.DateTime:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});");
break;
case PropertyTypeKind.Guid:
sb.AppendLine($"{i}if ({a} == System.Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a}); }}");
break;
case PropertyTypeKind.Byte:
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a}); }}");
break;
case PropertyTypeKind.Int16:
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a}); }}");
break;
case PropertyTypeKind.UInt16:
sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a}); }}");
break;
case PropertyTypeKind.UInt32:
sb.AppendLine($"{i}if ({a} == 0U) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a}); }}");
break;
case PropertyTypeKind.UInt64:
sb.AppendLine($"{i}if ({a} == 0UL) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a}); }}");
break;
case PropertyTypeKind.Enum:
var s = a.Replace(".", "_");
sb.AppendLine($"{i}var ev_{s} = (int){a};");
sb.AppendLine($"{i}if (ev_{s} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt(ev_{s}, out var te_{s})) {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(te_{s}); }}");
sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{s}); }}");
break;
case PropertyTypeKind.TimeSpan:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);");
break;
case PropertyTypeKind.DateTimeOffset:
sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});");
break;
default:
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context, depth);");
break;
}
}
private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string i)
{
switch (k)
{
case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a});"); break;
case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a});"); break;
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);"); break;
case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});"); break;
case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});"); break;
case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break;
case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break;
case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break;
default: EmitSkip(sb, k, a, i); break;
}
}
private static string GenInit(List<SerializableClassInfo> classes)
{
var sb = new StringBuilder(512);
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("using System.Runtime.CompilerServices;");
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
sb.AppendLine();
sb.AppendLine("namespace AyCode.Core.Serializers.Generated;");
sb.AppendLine();
sb.AppendLine("internal static class AcBinaryGeneratedWritersInit");
sb.AppendLine("{");
sb.AppendLine(" [ModuleInitializer]");
sb.AppendLine(" internal static void Register()");
sb.AppendLine(" {");
foreach (var ci in classes)
sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {ci.FullTypeName}_GeneratedWriter.Instance);");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
#region Type analysis
private static bool IsNullableVT(ITypeSymbol t) =>
t is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T;
private static PropertyTypeKind GetKind(ITypeSymbol type)
{
if (type is INamedTypeSymbol n && n.IsGenericType && n.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
return GetKindCore(n.TypeArguments[0], true);
return GetKindCore(type, false);
}
private static PropertyTypeKind GetKindCore(ITypeSymbol type, bool nullable)
{
switch (type.SpecialType)
{
case SpecialType.System_String: return PropertyTypeKind.String;
case SpecialType.System_Int32: return nullable ? PropertyTypeKind.NullableInt32 : PropertyTypeKind.Int32;
case SpecialType.System_Int64: return nullable ? PropertyTypeKind.NullableInt64 : PropertyTypeKind.Int64;
case SpecialType.System_Int16: return nullable ? PropertyTypeKind.NullableInt16 : PropertyTypeKind.Int16;
case SpecialType.System_Byte: return nullable ? PropertyTypeKind.NullableByte : PropertyTypeKind.Byte;
case SpecialType.System_UInt16: return nullable ? PropertyTypeKind.NullableUInt16 : PropertyTypeKind.UInt16;
case SpecialType.System_UInt32: return nullable ? PropertyTypeKind.NullableUInt32 : PropertyTypeKind.UInt32;
case SpecialType.System_UInt64: return nullable ? PropertyTypeKind.NullableUInt64 : PropertyTypeKind.UInt64;
case SpecialType.System_Boolean: return nullable ? PropertyTypeKind.NullableBoolean : PropertyTypeKind.Boolean;
case SpecialType.System_Single: return nullable ? PropertyTypeKind.NullableSingle : PropertyTypeKind.Single;
case SpecialType.System_Double: return nullable ? PropertyTypeKind.NullableDouble : PropertyTypeKind.Double;
case SpecialType.System_Decimal: return nullable ? PropertyTypeKind.NullableDecimal : PropertyTypeKind.Decimal;
case SpecialType.System_DateTime: return nullable ? PropertyTypeKind.NullableDateTime : PropertyTypeKind.DateTime;
default: break;
}
var fn = type.ToDisplayString();
if (fn == "System.Guid") return nullable ? PropertyTypeKind.NullableGuid : PropertyTypeKind.Guid;
if (fn == "System.TimeSpan") return nullable ? PropertyTypeKind.NullableTimeSpan : PropertyTypeKind.TimeSpan;
if (fn == "System.DateTimeOffset") return nullable ? PropertyTypeKind.NullableDateTimeOffset : PropertyTypeKind.DateTimeOffset;
if (type.TypeKind == TypeKind.Enum) return nullable ? PropertyTypeKind.NullableEnum : PropertyTypeKind.Enum;
if (type is IArrayTypeSymbol) return PropertyTypeKind.Collection;
if (type is INamedTypeSymbol nt && nt.AllInterfaces.Any(iface => iface.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T))
return PropertyTypeKind.Collection;
if (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct) return PropertyTypeKind.Complex;
return PropertyTypeKind.Unknown;
}
private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32;
private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch
{
PropertyTypeKind.NullableInt32 => PropertyTypeKind.Int32, PropertyTypeKind.NullableInt64 => PropertyTypeKind.Int64,
PropertyTypeKind.NullableInt16 => PropertyTypeKind.Int16, PropertyTypeKind.NullableByte => PropertyTypeKind.Byte,
PropertyTypeKind.NullableUInt16 => PropertyTypeKind.UInt16, PropertyTypeKind.NullableUInt32 => PropertyTypeKind.UInt32,
PropertyTypeKind.NullableUInt64 => PropertyTypeKind.UInt64, PropertyTypeKind.NullableBoolean => PropertyTypeKind.Boolean,
PropertyTypeKind.NullableSingle => PropertyTypeKind.Single, PropertyTypeKind.NullableDouble => PropertyTypeKind.Double,
PropertyTypeKind.NullableDecimal => PropertyTypeKind.Decimal, PropertyTypeKind.NullableDateTime => PropertyTypeKind.DateTime,
PropertyTypeKind.NullableDateTimeOffset => PropertyTypeKind.DateTimeOffset, PropertyTypeKind.NullableTimeSpan => PropertyTypeKind.TimeSpan,
PropertyTypeKind.NullableGuid => PropertyTypeKind.Guid, PropertyTypeKind.NullableEnum => PropertyTypeKind.Enum,
_ => PropertyTypeKind.Unknown
};
#endregion
}
internal sealed class SerializableClassInfo
{
public string Namespace { get; }
public string ClassName { get; }
public string FullTypeName { get; }
public List<PropInfo> Properties { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; }
}
internal sealed class PropInfo
{
public string Name { get; }
public string TypeName { get; }
public PropertyTypeKind TypeKind { get; }
public bool IsNullable { get; }
public PropInfo(string n, string tn, PropertyTypeKind tk, bool nullable)
{ Name = n; TypeName = tn; TypeKind = tk; IsNullable = nullable; }
}
internal enum PropertyTypeKind
{
Unknown, String, Int32, Int64, Int16, Byte, UInt16, UInt32, UInt64,
Boolean, Single, Double, Decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Enum,
Collection, Complex,
NullableInt32, NullableInt64, NullableInt16, NullableByte, NullableUInt16, NullableUInt32, NullableUInt64,
NullableBoolean, NullableSingle, NullableDouble, NullableDecimal, NullableDateTime,
NullableDateTimeOffset, NullableTimeSpan, NullableGuid, NullableEnum
}