847 lines
46 KiB
C#
847 lines
46 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;
|
|
|
|
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;
|
|
|
|
// String interning attribútum detektálás (null = no attr, true/false = explicit)
|
|
bool? stringInternAttr = null;
|
|
if (GetKind(p.Type) == PropertyTypeKind.String)
|
|
{
|
|
var attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute");
|
|
if (attr != null && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Kind == TypedConstantKind.Primitive)
|
|
{
|
|
stringInternAttr = (bool)attr.ConstructorArguments[0].Value!;
|
|
}
|
|
}
|
|
|
|
// For typeof(): strip trailing '?' from nullable reference types (typeof(T?) is invalid for ref types)
|
|
// Nullable value types (int?, Guid?) keep '?' because typeof(int?) == typeof(Nullable<int>) is valid
|
|
var typeDisplayName = p.Type.ToDisplayString();
|
|
var typeNameForTypeof = (p.Type.NullableAnnotation == NullableAnnotation.Annotated && !p.Type.IsValueType)
|
|
? typeDisplayName.TrimEnd('?')
|
|
: typeDisplayName;
|
|
|
|
// Direct object write detection for Complex property types:
|
|
// Check if the property type has [AcBinarySerializable] (→ has generated writer)
|
|
// and if it implements IId<T> (→ needs ref tracking in generated code)
|
|
var kind = GetKind(p.Type);
|
|
bool hasGenWriter = false;
|
|
bool propTypeIsIId = false;
|
|
string? writerClassName = null;
|
|
if (kind == PropertyTypeKind.Complex)
|
|
{
|
|
// Resolve to the actual type symbol (strip nullable annotation for ref types)
|
|
// For SharedTag? → SharedTag. OriginalDefinition handles generic types.
|
|
var resolvedType = p.Type is INamedTypeSymbol namedPropType
|
|
? namedPropType.OriginalDefinition
|
|
: p.Type;
|
|
|
|
hasGenWriter = resolvedType.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
|
|
if (hasGenWriter)
|
|
{
|
|
propTypeIsIId = resolvedType.AllInterfaces.Any(i =>
|
|
i.IsGenericType &&
|
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
// Writer class: {Namespace}.{FlatName}_GeneratedWriter
|
|
var flatName = BuildFlatName((INamedTypeSymbol)resolvedType);
|
|
var ns = resolvedType.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty
|
|
: resolvedType.ContainingNamespace.ToDisplayString();
|
|
writerClassName = string.IsNullOrEmpty(ns)
|
|
? $"{flatName}_GeneratedWriter"
|
|
: $"{ns}.{flatName}_GeneratedWriter";
|
|
}
|
|
}
|
|
|
|
// Collection element type analysis for inline collection write
|
|
PropertyTypeKind elemKind = PropertyTypeKind.Unknown;
|
|
bool elemHasGenWriter = false;
|
|
bool elemIsIId = false;
|
|
string? elemWriterClassName = null;
|
|
string? collKind = null;
|
|
string? elemFullTypeName = null;
|
|
if (kind == PropertyTypeKind.Collection)
|
|
{
|
|
var elemType = GetCollectionElementType(p.Type);
|
|
if (elemType != null)
|
|
{
|
|
elemKind = GetKind(elemType);
|
|
elemFullTypeName = elemType.ToDisplayString();
|
|
|
|
// Detect collection shape for inline write
|
|
if (p.Type is IArrayTypeSymbol)
|
|
collKind = "Array";
|
|
else if (p.Type is INamedTypeSymbol collNamedType)
|
|
{
|
|
var origDef = collNamedType.OriginalDefinition.ToDisplayString();
|
|
collKind = origDef switch
|
|
{
|
|
"System.Collections.Generic.List<T>" => "List",
|
|
"System.Collections.Generic.IList<T>" => "List", // has Count + indexer
|
|
"System.Collections.Generic.IReadOnlyList<T>" => "List", // has Count + indexer
|
|
"System.Collections.Generic.HashSet<T>" => "Counted", // has Count, no indexer
|
|
"System.Collections.Generic.Queue<T>" => "Counted",
|
|
"System.Collections.Generic.ICollection<T>" => "Counted",
|
|
"System.Collections.Generic.IReadOnlyCollection<T>" => "Counted",
|
|
"System.Collections.Generic.SortedSet<T>" => "Counted",
|
|
"System.Collections.Generic.LinkedList<T>" => "Counted",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
// For Complex element types, check for generated writer
|
|
if (elemKind == PropertyTypeKind.Complex)
|
|
{
|
|
var resolvedElem = elemType is INamedTypeSymbol namedElem
|
|
? namedElem.OriginalDefinition : elemType;
|
|
elemHasGenWriter = resolvedElem.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (elemHasGenWriter)
|
|
{
|
|
elemIsIId = resolvedElem.AllInterfaces.Any(ifc =>
|
|
ifc.IsGenericType &&
|
|
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
|
|
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
|
elemWriterClassName = string.IsNullOrEmpty(ens)
|
|
? $"{elemFlatName}_GeneratedWriter"
|
|
: $"{ens}.{elemFlatName}_GeneratedWriter";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
properties.Add(new PropInfo(
|
|
p.Name,
|
|
typeDisplayName,
|
|
typeNameForTypeof,
|
|
kind,
|
|
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
|
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName,
|
|
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName));
|
|
}
|
|
}
|
|
|
|
// 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, BuildFlatName(typeSymbol), 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;");
|
|
// ReferenceHandlingMode is needed when any Complex/Collection property has direct object write
|
|
if (ci.Properties.Any(p => p.HasGeneratedWriter || p.ElementHasGeneratedWriter))
|
|
sb.AppendLine("using AyCode.Core.Serializers;");
|
|
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, " ", ci.FullTypeName);
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName)
|
|
{
|
|
var a = $"obj.{p.Name}";
|
|
|
|
// Markerless types: write raw value only, no type marker, no PropertySkip
|
|
// Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode
|
|
// NEVER filtered (runtime doesn't filter markerless properties either)
|
|
// UseMetadata=true: markerless path NOT available — must use markered path (EmitSkip)
|
|
// to match runtime WritePropertyOrSkip behavior (every property gets a type marker byte)
|
|
if (IsMarkerless(p.TypeKind))
|
|
{
|
|
sb.AppendLine($"{i}if (context.UseMetadata)");
|
|
sb.AppendLine($"{i}{{");
|
|
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " ");
|
|
sb.AppendLine($"{i}}}");
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i}{{");
|
|
EmitMarkerless(sb, p.TypeKind, a, i + " ");
|
|
sb.AppendLine($"{i}}}");
|
|
return;
|
|
}
|
|
|
|
// All non-markerless properties: emit PropertyFilter guard
|
|
// When filter returns false, write PropertySkip and skip the property write
|
|
sb.AppendLine($"{i}if (context.HasPropertyFilter)");
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});");
|
|
sb.AppendLine($"{i} if (!context.PropertyFilter!(in fc_{p.Name}))");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i} goto skip_{p.Name};");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
|
|
// 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", p.TypeNameForTypeof, i + " ");
|
|
sb.AppendLine($"{i}}}");
|
|
sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}skip_{p.Name}:;");
|
|
return;
|
|
}
|
|
|
|
// Non-markerless types: write WITH type marker byte (markered path)
|
|
switch (p.TypeKind)
|
|
{
|
|
case PropertyTypeKind.String:
|
|
if (p.InterningFlags == 0)
|
|
sb.AppendLine($"{i}context.StringInternEligible = false;");
|
|
else
|
|
sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;");
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);");
|
|
break;
|
|
case PropertyTypeKind.Complex:
|
|
// Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely
|
|
// when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise.
|
|
if (p.HasGeneratedWriter)
|
|
EmitDirectObjectWrite(sb, p, a, i);
|
|
else if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
}
|
|
else
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
break;
|
|
case PropertyTypeKind.Collection:
|
|
// Direct collection write for List<T>/T[] with Complex element types that have generated writers
|
|
if (p.ElementHasGeneratedWriter && p.CollectionKind != null)
|
|
EmitDirectCollectionWrite(sb, p, a, i);
|
|
else if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
}
|
|
else
|
|
sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
break;
|
|
default:
|
|
EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i);
|
|
break;
|
|
}
|
|
|
|
sb.AppendLine($"{i}skip_{p.Name}:;");
|
|
}
|
|
|
|
/// <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 or
|
|
PropertyTypeKind.Boolean or PropertyTypeKind.Enum => 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;
|
|
case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? (byte)1 : (byte)0);"); break;
|
|
case PropertyTypeKind.Enum: sb.AppendLine($"{i}context.WriteVarInt((int){a});"); break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
|
|
/// Writes marker bytes + calls child GeneratedWriter.WriteProperties inline.
|
|
/// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
|
|
/// Non-IId types: guard ReferenceHandling == All (tracked only in All mode).
|
|
/// Falls back to WriteObjectGenerated when context.IsDirectObjectWrite is false (UseMetadata).
|
|
/// </summary>
|
|
private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.WriterClassName;
|
|
var nextDepth = "depth + 1";
|
|
|
|
if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}if (context.IsDirectObjectWrite)");
|
|
}
|
|
|
|
sb.AppendLine($"{i}{{");
|
|
|
|
// MaxDepth check — matches WriteObjectGenerated
|
|
sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
|
|
// Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior.
|
|
// IId types: tracked in OnlyId + All → guard: ReferenceHandling != None
|
|
// Non-IId types: tracked only in All → guard: ReferenceHandling == All
|
|
// This matches UseTypeReferenceHandling: (IsIId || ReferenceHandling == All) && ReferenceHandling != None
|
|
var refGuard = p.IsIId
|
|
? "context.ReferenceHandling != ReferenceHandlingMode.None"
|
|
: "context.ReferenceHandling == ReferenceHandlingMode.All";
|
|
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
|
|
sb.AppendLine($"{i} }}");
|
|
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
|
|
// Fallback for non-direct mode (UseMetadata=true or HasPropertyFilter=true)
|
|
if (p.IsNullable)
|
|
sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits inline collection write for List<T> / T[] where T is a Complex type with generated writer.
|
|
/// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely.
|
|
/// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]...
|
|
/// Falls back to WriteValueGenerated when context.IsDirectObjectWrite is false.
|
|
/// </summary>
|
|
private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i)
|
|
{
|
|
var writer = p.ElementWriterClassName;
|
|
var elemType = p.ElementFullTypeName;
|
|
|
|
if (p.IsNullable)
|
|
{
|
|
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
|
sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}if (context.IsDirectObjectWrite)");
|
|
}
|
|
|
|
sb.AppendLine($"{i}{{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);");
|
|
|
|
// Get count and iteration based on collection kind
|
|
if (p.CollectionKind == "Array")
|
|
{
|
|
sb.AppendLine($"{i} var arr_{p.Name} = {a};");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);");
|
|
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
|
|
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];");
|
|
}
|
|
else if (p.CollectionKind == "Counted")
|
|
{
|
|
// HashSet<T>, Queue<T>, ICollection<T>, IReadOnlyCollection<T>, etc. — Count + foreach
|
|
sb.AppendLine($"{i} var col_{p.Name} = {a};");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
|
|
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
|
|
sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})");
|
|
sb.AppendLine($"{i} {{");
|
|
}
|
|
else // List, IList<T>, IReadOnlyList<T> — Count + indexer
|
|
{
|
|
sb.AppendLine($"{i} var list_{p.Name} = {a};");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);");
|
|
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
|
|
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < list_{p.Name}.Count; i_{p.Name}++)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];");
|
|
}
|
|
|
|
// Per-element write — same logic as EmitDirectObjectWrite but for each element
|
|
var e = $"elem_{p.Name}";
|
|
// Elements in a collection can be null (runtime writes Null marker)
|
|
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
|
|
sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
|
|
|
|
// Inline ref tracking: guard depends on IId vs non-IId element type to match scan pass.
|
|
// IId elements: tracked in OnlyId + All → guard: ReferenceHandling != None
|
|
// Non-IId elements: tracked only in All → guard: ReferenceHandling == All
|
|
var elemRefGuard = p.ElementIsIId
|
|
? "context.ReferenceHandling != ReferenceHandlingMode.None"
|
|
: "context.ReferenceHandling == ReferenceHandlingMode.All";
|
|
sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
|
|
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i} else");
|
|
sb.AppendLine($"{i} {{");
|
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
|
|
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
|
|
sb.AppendLine($"{i} }}");
|
|
|
|
sb.AppendLine($"{i} }}");
|
|
sb.AppendLine($"{i}}}");
|
|
|
|
// Fallback for non-direct mode
|
|
if (p.IsNullable)
|
|
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
else
|
|
{
|
|
sb.AppendLine($"{i}else");
|
|
sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
|
|
}
|
|
}
|
|
|
|
private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, 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}, typeof({typeName}), context, depth);");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string typeName, 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, typeName, 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)
|
|
{
|
|
var writerRef = string.IsNullOrEmpty(ci.Namespace)
|
|
? $"{ci.ClassName}_GeneratedWriter"
|
|
: $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter";
|
|
sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);");
|
|
}
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a flat class name for nested types: Outer_Inner_Leaf.
|
|
/// For top-level types returns the simple name unchanged.
|
|
/// </summary>
|
|
private static string BuildFlatName(INamedTypeSymbol typeSymbol)
|
|
{
|
|
if (typeSymbol.ContainingType == null)
|
|
return typeSymbol.Name;
|
|
|
|
var parts = new List<string>();
|
|
var current = typeSymbol;
|
|
while (current != null)
|
|
{
|
|
parts.Add(current.Name);
|
|
current = current.ContainingType;
|
|
}
|
|
parts.Reverse();
|
|
return string.Join("_", parts);
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the element type T from List<T>, T[], IList<T>, IEnumerable<T>.
|
|
/// Returns null if the element type cannot be determined.
|
|
/// </summary>
|
|
private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type)
|
|
{
|
|
// T[] → element type
|
|
if (type is IArrayTypeSymbol arrayType)
|
|
return arrayType.ElementType;
|
|
|
|
// Generic collections: List<T>, IList<T>, ICollection<T>, IEnumerable<T>
|
|
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
|
|
{
|
|
// Direct: List<T>, HashSet<T>, etc. — first type argument
|
|
var iface = namedType.AllInterfaces
|
|
.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
|
|
if (iface != null)
|
|
return iface.TypeArguments[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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; }
|
|
/// <summary>
|
|
/// Type name safe for typeof() — nullable ref type annotation stripped (typeof(T?) invalid for ref types).
|
|
/// </summary>
|
|
public string TypeNameForTypeof { get; }
|
|
public PropertyTypeKind TypeKind { get; }
|
|
public bool IsNullable { get; }
|
|
/// <summary>
|
|
/// Pre-computed interning flags matching runtime BinaryPropertyAccessorBase._interningFlags.
|
|
/// Bit layout: bit N = eligible when StringInterningMode == N.
|
|
/// None=0 → bit 0 never set. Attribute=1 → bit 1. All=2 → bit 2.
|
|
/// No attr: 0b100 (4), [AcStringIntern(true)]: 0b110 (6), [AcStringIntern(false)]: 0b000 (0).
|
|
/// </summary>
|
|
public int InterningFlags { get; }
|
|
|
|
/// <summary>True if the Complex property type has [AcBinarySerializable] → has a generated writer.</summary>
|
|
public bool HasGeneratedWriter { get; }
|
|
/// <summary>True if the Complex property type implements IId<T> → needs ref tracking in write pass.</summary>
|
|
public bool IsIId { get; }
|
|
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
|
|
public string? WriterClassName { get; }
|
|
|
|
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
|
|
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
|
|
public PropertyTypeKind ElementKind { get; }
|
|
/// <summary>True if collection element type has [AcBinarySerializable].</summary>
|
|
public bool ElementHasGeneratedWriter { get; }
|
|
/// <summary>True if collection element type implements IId<T>.</summary>
|
|
public bool ElementIsIId { get; }
|
|
/// <summary>Generated writer class name for collection element type.</summary>
|
|
public string? ElementWriterClassName { get; }
|
|
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary>
|
|
public string? CollectionKind { get; }
|
|
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
|
|
public string? ElementFullTypeName { get; }
|
|
|
|
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
|
|
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null,
|
|
PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
|
|
string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null)
|
|
{
|
|
Name = n;
|
|
TypeName = tn;
|
|
TypeNameForTypeof = tnForTypeof;
|
|
TypeKind = tk;
|
|
IsNullable = nullable;
|
|
HasGeneratedWriter = hasGeneratedWriter;
|
|
IsIId = isIId;
|
|
WriterClassName = writerClassName;
|
|
ElementKind = elementKind;
|
|
ElementHasGeneratedWriter = elementHasGenWriter;
|
|
ElementIsIId = elementIsIId;
|
|
ElementWriterClassName = elementWriterClassName;
|
|
CollectionKind = collectionKind;
|
|
ElementFullTypeName = elementFullTypeName;
|
|
// Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase
|
|
int flags = 0;
|
|
if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit
|
|
if (stringInternAttr != false) flags |= (1 << 2); // All bit
|
|
InterningFlags = flags;
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|