364 lines
21 KiB
C#
364 lines
21 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.CodeAnalysis;
|
|
|
|
namespace AyCode.Core.Serializers.SourceGenerator;
|
|
|
|
/// <summary>
|
|
/// Class-info extraction pass — transforms a Roslyn <see cref="GeneratorAttributeSyntaxContext"/>
|
|
/// (a class/struct annotated with <c>[AcBinarySerializable]</c>) into the <see cref="SerializableClassInfo"/>
|
|
/// model consumed by the emit passes (writer / reader / scan / init).
|
|
///
|
|
/// <para>Reads the attribute's feature flags (1-, 4-, 5-, 6-bool ctor variants), walks the inheritance
|
|
/// hierarchy via <c>GetAllSerializablePropertySymbols</c>, and computes per-property metadata: kind,
|
|
/// nullability, intern eligibility, complex / collection / dictionary element types, generated-writer
|
|
/// pointers, FNV hashes for inline-metadata, and recursive scan-need flags.</para>
|
|
/// </summary>
|
|
public partial class AcBinarySourceGenerator
|
|
{
|
|
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>();
|
|
|
|
// Read feature flags from [AcBinarySerializable] — disabled features eliminate
|
|
// corresponding code blocks from generated ScanObject/WriteProperties.
|
|
var enableIdTracking = true;
|
|
var enableRefHandling = true;
|
|
var enableInternString = true;
|
|
var enableMetadata = true;
|
|
var enablePropertyFilter = true;
|
|
var enablePolymorphDetect = true;
|
|
var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (binarySerializableAttr != null)
|
|
{
|
|
if (binarySerializableAttr.ConstructorArguments.Length == 1)
|
|
{
|
|
// Single bool ctor: AcBinarySerializable(enableAllFeatures)
|
|
var all = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = all;
|
|
enableRefHandling = all;
|
|
enableInternString = all;
|
|
enableMetadata = all;
|
|
enablePropertyFilter = all;
|
|
enablePolymorphDetect = all;
|
|
}
|
|
else if (binarySerializableAttr.ConstructorArguments.Length == 4)
|
|
{
|
|
// Four bool ctor: (metadata, idTracking, refHandling, internString) — filter + polymorph default to true
|
|
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
|
|
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
|
|
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
|
|
}
|
|
else if (binarySerializableAttr.ConstructorArguments.Length == 5)
|
|
{
|
|
// Five bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter) — polymorph defaults to true
|
|
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
|
|
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
|
|
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
|
|
enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
|
|
}
|
|
else if (binarySerializableAttr.ConstructorArguments.Length == 6)
|
|
{
|
|
// Six bool ctor: (metadata, idTracking, refHandling, internString, propertyFilter, polymorphDetect)
|
|
enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!;
|
|
enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!;
|
|
enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!;
|
|
enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!;
|
|
enablePropertyFilter = (bool)binarySerializableAttr.ConstructorArguments[4].Value!;
|
|
enablePolymorphDetect = (bool)binarySerializableAttr.ConstructorArguments[5].Value!;
|
|
}
|
|
}
|
|
|
|
foreach (var p in GetAllSerializablePropertySymbols(typeSymbol))
|
|
{
|
|
// String interning attribútum detektálás (null = no attr, true/false = explicit)
|
|
bool? stringInternAttr = null;
|
|
if (!enableInternString)
|
|
{
|
|
stringInternAttr = false;
|
|
}
|
|
else 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;
|
|
bool propEnableMetadata = true;
|
|
bool childNeedsIdScan = true;
|
|
bool childNeedsAllRefScan = true;
|
|
bool childNeedsInternScan = true;
|
|
string? writerClassName = null;
|
|
string? propIdTypeName = null;
|
|
int childTypeNameHash = 0;
|
|
int[]? childPropertyHashes = 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.Locations.Any(l => l.IsInSource)
|
|
&& resolvedType.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
|
|
if (hasGenWriter)
|
|
{
|
|
// Read child type's EnableMetadataFeature
|
|
propEnableMetadata = ReadEnableMetadata(resolvedType);
|
|
var childScanFlags = ComputeNeedsScan(resolvedType);
|
|
childNeedsIdScan = childScanFlags.needsIdScan;
|
|
childNeedsAllRefScan = childScanFlags.needsAllRefScan;
|
|
childNeedsInternScan = childScanFlags.needsInternScan;
|
|
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i =>
|
|
i.IsGenericType &&
|
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
propTypeIsIId = iidIface != null;
|
|
if (iidIface != null)
|
|
propIdTypeName = iidIface.TypeArguments[0].ToDisplayString();
|
|
|
|
// 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";
|
|
|
|
// UseMetadata: compute child type hash-es for inline metadata
|
|
childTypeNameHash = ComputeFnvHash(resolvedType.Name);
|
|
childPropertyHashes = ComputeChildPropertyHashes(resolvedType);
|
|
}
|
|
}
|
|
|
|
// Collection element type analysis for inline collection write
|
|
PropertyTypeKind elemKind = PropertyTypeKind.Unknown;
|
|
bool elemHasGenWriter = false;
|
|
bool elemIsIId = false;
|
|
bool elemEnableMetadata = true;
|
|
bool elemNeedsIdScan = true;
|
|
bool elemNeedsAllRefScan = true;
|
|
bool elemNeedsInternScan = true;
|
|
string? elemWriterClassName = null;
|
|
string? elemIdTypeName = null;
|
|
string? collKind = null;
|
|
string? collAddMethod = null;
|
|
bool collHasCapacityCtor = false;
|
|
string? elemFullTypeName = null;
|
|
int elementTypeNameHash = 0;
|
|
int[]? elementPropertyHashes = 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>" => "IndexedCollection",
|
|
"System.Collections.Generic.IReadOnlyList<T>" => "IndexedCollection",
|
|
"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
|
|
};
|
|
|
|
// Determine add method + capacity ctor for Counted concrete types
|
|
if (collKind == "Counted")
|
|
{
|
|
collAddMethod = origDef switch
|
|
{
|
|
"System.Collections.Generic.HashSet<T>" => "Add",
|
|
"System.Collections.Generic.SortedSet<T>" => "Add",
|
|
"System.Collections.Generic.Queue<T>" => "Enqueue",
|
|
"System.Collections.Generic.LinkedList<T>" => "AddLast",
|
|
_ => null // ICollection<T>, IReadOnlyCollection<T> → backed by List<T>
|
|
};
|
|
collHasCapacityCtor = origDef is
|
|
"System.Collections.Generic.HashSet<T>" or
|
|
"System.Collections.Generic.Queue<T>";
|
|
}
|
|
}
|
|
|
|
// For Complex element types, check for generated writer
|
|
if (elemKind == PropertyTypeKind.Complex)
|
|
{
|
|
var resolvedElem = elemType is INamedTypeSymbol namedElem
|
|
? namedElem.OriginalDefinition : elemType;
|
|
elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource)
|
|
&& resolvedElem.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (elemHasGenWriter)
|
|
{
|
|
// Read element type's EnableMetadataFeature
|
|
elemEnableMetadata = ReadEnableMetadata(resolvedElem);
|
|
var elemScanFlags = ComputeNeedsScan(resolvedElem);
|
|
elemNeedsIdScan = elemScanFlags.needsIdScan;
|
|
elemNeedsAllRefScan = elemScanFlags.needsAllRefScan;
|
|
elemNeedsInternScan = elemScanFlags.needsInternScan;
|
|
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc =>
|
|
ifc.IsGenericType &&
|
|
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
elemIsIId = elemIidIface != null;
|
|
if (elemIidIface != null)
|
|
elemIdTypeName = elemIidIface.TypeArguments[0].ToDisplayString();
|
|
|
|
var elemFlatName = BuildFlatName((INamedTypeSymbol)resolvedElem);
|
|
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
|
|
elemWriterClassName = string.IsNullOrEmpty(ens)
|
|
? $"{elemFlatName}_GeneratedWriter"
|
|
: $"{ens}.{elemFlatName}_GeneratedWriter";
|
|
|
|
// UseMetadata: compute element type hash-es for inline metadata
|
|
elementTypeNameHash = ComputeFnvHash(resolvedElem.Name);
|
|
elementPropertyHashes = ComputeChildPropertyHashes(resolvedElem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dictionary key/value type analysis for inline dictionary read
|
|
PropertyTypeKind dictKeyKind = PropertyTypeKind.Unknown;
|
|
PropertyTypeKind dictValueKind = PropertyTypeKind.Unknown;
|
|
string? dictKeyTypeName = null;
|
|
string? dictValueTypeName = null;
|
|
bool dictValueHasGenWriter = false;
|
|
string? dictValueWriterClassName = null;
|
|
bool dictValueIsIId = false;
|
|
bool dictValueEnableMetadata = true;
|
|
bool dictValueNeedsIdScan = true;
|
|
bool dictValueNeedsAllRefScan = true;
|
|
bool dictValueNeedsInternScan = true;
|
|
int dictValueTypeNameHash = 0;
|
|
int[]? dictValuePropertyHashes = null;
|
|
if (kind == PropertyTypeKind.Dictionary)
|
|
{
|
|
var (keyType, valueType) = GetDictionaryKeyValueTypes(p.Type);
|
|
if (keyType != null)
|
|
{
|
|
dictKeyKind = GetKind(keyType);
|
|
dictKeyTypeName = keyType.ToDisplayString();
|
|
}
|
|
if (valueType != null)
|
|
{
|
|
dictValueKind = GetKind(valueType);
|
|
dictValueTypeName = valueType.ToDisplayString();
|
|
if (dictValueKind == PropertyTypeKind.Complex)
|
|
{
|
|
var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType;
|
|
dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource)
|
|
&& resolvedValue.GetAttributes().Any(a =>
|
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
|
if (dictValueHasGenWriter)
|
|
{
|
|
var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue);
|
|
var vns = resolvedValue.ContainingNamespace.IsGlobalNamespace
|
|
? string.Empty : resolvedValue.ContainingNamespace.ToDisplayString();
|
|
dictValueWriterClassName = string.IsNullOrEmpty(vns)
|
|
? $"{vfn}_GeneratedWriter"
|
|
: $"{vns}.{vfn}_GeneratedWriter";
|
|
|
|
dictValueEnableMetadata = ReadEnableMetadata(resolvedValue);
|
|
var dvScanFlags = ComputeNeedsScan(resolvedValue);
|
|
dictValueNeedsIdScan = dvScanFlags.needsIdScan;
|
|
dictValueNeedsAllRefScan = dvScanFlags.needsAllRefScan;
|
|
dictValueNeedsInternScan = dvScanFlags.needsInternScan;
|
|
var dvIidIface = resolvedValue.AllInterfaces.FirstOrDefault(ifc =>
|
|
ifc.IsGenericType &&
|
|
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
dictValueIsIId = dvIidIface != null;
|
|
dictValueTypeNameHash = ComputeFnvHash(resolvedValue.Name);
|
|
dictValuePropertyHashes = ComputeChildPropertyHashes(resolvedValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
properties.Add(new PropInfo(
|
|
p.Name,
|
|
typeDisplayName,
|
|
typeNameForTypeof,
|
|
kind,
|
|
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
|
|
p.Type.SpecialType == SpecialType.System_Object,
|
|
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName,
|
|
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
|
|
collAddMethod, collHasCapacityCtor,
|
|
dictKeyKind, dictValueKind, dictKeyTypeName, dictValueTypeName, dictValueHasGenWriter, dictValueWriterClassName,
|
|
dictValueIsIId, dictValueEnableMetadata, dictValueTypeNameHash, dictValuePropertyHashes,
|
|
dictValueNeedsIdScan, dictValueNeedsAllRefScan, dictValueNeedsInternScan,
|
|
childTypeNameHash, childPropertyHashes,
|
|
elementTypeNameHash, elementPropertyHashes,
|
|
propEnableMetadata, elemEnableMetadata,
|
|
childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan,
|
|
elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan));
|
|
}
|
|
|
|
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
|
|
// If EnableIdTrackingFeature == false, skip IId detection entirely → isIId = false
|
|
var isIId = false;
|
|
string? idTypeName = null;
|
|
if (enableIdTracking)
|
|
{
|
|
var iidInterface = typeSymbol.AllInterfaces.FirstOrDefault(i =>
|
|
i.IsGenericType &&
|
|
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
|
|
if (iidInterface != null)
|
|
{
|
|
isIId = true;
|
|
idTypeName = iidInterface.TypeArguments[0].ToDisplayString();
|
|
}
|
|
}
|
|
|
|
// Properties are already in runtime-matching order from GetAllSerializablePropertySymbols:
|
|
// derived → base, each level sorted alphabetically (matches TypeMetadataBase.GetUnfilteredProperties).
|
|
|
|
var className = BuildFlatName(typeSymbol);
|
|
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
|
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
|
|
var selfScanFlags = ComputeNeedsScan(typeSymbol);
|
|
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, enablePropertyFilter, enablePolymorphDetect, enableInternString, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
|
|
}
|
|
}
|