diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index dd3af7e..87199b1 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -79,21 +79,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } - foreach (var member in typeSymbol.GetMembers()) + foreach (var p in GetAllSerializablePropertySymbols(typeSymbol)) { - 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) + // String interning attribútum detektálás (null = no attr, true/false = explicit) bool? stringInternAttr = null; if (!enableInternString) { @@ -137,8 +125,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator ? namedPropType.OriginalDefinition : p.Type; - hasGenWriter = resolvedType.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); + hasGenWriter = resolvedType.Locations.Any(l => l.IsInSource) + && resolvedType.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); if (hasGenWriter) { @@ -236,8 +225,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { var resolvedElem = elemType is INamedTypeSymbol namedElem ? namedElem.OriginalDefinition : elemType; - elemHasGenWriter = resolvedElem.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); + elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource) + && resolvedElem.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); if (elemHasGenWriter) { // Read element type's EnableMetadataFeature @@ -297,8 +287,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (dictValueKind == PropertyTypeKind.Complex) { var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType; - dictValueHasGenWriter = resolvedValue.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == AttributeName); + dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource) + && resolvedValue.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == AttributeName); if (dictValueHasGenWriter) { var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue); @@ -342,7 +333,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator propEnableMetadata, elemEnableMetadata, childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan, elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan)); - } } // IId: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering @@ -361,7 +351,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator } } - properties.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + // 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); @@ -1214,16 +1205,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator 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"); - sb.AppendLine($"{i}{{"); - } - else - { - sb.AppendLine($"{i}{{"); - } + // Reference type properties can always be null at runtime regardless of nullable annotation + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); // MaxDepth check — matches WriteObjectGenerated sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); @@ -1351,19 +1336,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { var writer = p.ElementWriterClassName; - if (p.IsNullable) - { - sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - } - else - { - sb.AppendLine($"{i}if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - } + // Reference type collections can always be null at runtime regardless of nullable annotation + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);"); @@ -1587,19 +1564,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var keyType = p.DictKeyTypeName ?? "object"; var valType = p.DictValueTypeName ?? "object"; - if (p.IsNullable) - { - sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); - sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - } - else - { - sb.AppendLine($"{i}if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - } + // Reference type dictionaries can always be null at runtime regardless of nullable annotation + sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);"); + sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Dictionary);"); sb.AppendLine($"{i} context.WriteVarUInt((uint){a}.Count);"); @@ -2749,21 +2718,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var needsInternScan = false; // Check properties for string interning or complex children - foreach (var member in type.GetMembers()) + foreach (var p in GetAllSerializablePropertySymbols(type)) { - if (member is not IPropertySymbol p || - p.DeclaredAccessibility != Accessibility.Public || - p.GetMethod == null || p.SetMethod == null || - p.IsIndexer || p.IsStatic) - continue; - - var hasIgnore = p.GetAttributes().Any(a => - { - var name = a.AttributeClass?.Name ?? ""; - return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; - }); - if (hasIgnore) continue; - // Early exit: if all flags are already true, no need to check more properties if (needsIdScan && needsAllRefScan && needsInternScan) break; @@ -2850,35 +2806,62 @@ public class AcBinarySourceGenerator : IIncrementalGenerator /// /// Computes FNV-1a hashes for all serializable properties of a child type. /// Property filtering and ordering matches runtime TypeMetadataBase exactly: - /// public get+set, non-indexer, non-static, no ignore attributes, sorted alphabetically. + /// derived → base, each level sorted alphabetically, with ignore attribute filtering. /// private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType) { - var propNames = new List(); - foreach (var member in resolvedType.GetMembers()) - { - if (member is IPropertySymbol cp && - cp.DeclaredAccessibility == Accessibility.Public && - cp.GetMethod != null && cp.SetMethod != null && - !cp.IsIndexer && !cp.IsStatic) - { - var hasIgnore = cp.GetAttributes().Any(a => - { - var name = a.AttributeClass?.Name ?? ""; - return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; - }); - if (hasIgnore) continue; - propNames.Add(cp.Name); - } - } - - propNames.Sort(StringComparer.Ordinal); - - return propNames.Select(ComputeFnvHash).ToArray(); + // Use hierarchy-walking helper — order matches runtime TypeMetadataBase + var props = GetAllSerializablePropertySymbols(resolvedType); + return props.Select(p => ComputeFnvHash(p.Name)).ToArray(); } #endregion + /// + /// Collects all serializable property symbols from the full inheritance hierarchy. + /// Order matches runtime TypeMetadataBase.GetUnfilteredProperties exactly: + /// derived → base, each level sorted alphabetically by name. + /// Filters: public, get+set, non-indexer, non-static, no ignore attributes. + /// Deduplicates by name (most-derived override wins). + /// + private static List GetAllSerializablePropertySymbols(ITypeSymbol typeSymbol) + { + var result = new List(); + var seen = new HashSet(); + + for (var currentType = typeSymbol as INamedTypeSymbol; + currentType != null && currentType.SpecialType != SpecialType.System_Object; + currentType = currentType.BaseType) + { + var levelProps = new List(); + + foreach (var member in currentType.GetMembers()) + { + if (member is IPropertySymbol p && + p.DeclaredAccessibility == Accessibility.Public && + p.GetMethod != null && p.SetMethod != null && + !p.IsIndexer && !p.IsStatic && + seen.Add(p.Name)) // dedup: most-derived wins + { + var hasIgnore = p.GetAttributes().Any(a => + { + var name = a.AttributeClass?.Name ?? ""; + return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute"; + }); + if (hasIgnore) continue; + + levelProps.Add(p); + } + } + + // Sort each level alphabetically — matches runtime OrderBy(p => p.Name, Ordinal) + levelProps.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + result.AddRange(levelProps); + } + + return result; + } + #region Type analysis private static bool IsNullableVT(ITypeSymbol t) => diff --git a/AyCode.Core/Properties/AssemblyInfo.cs b/AyCode.Core/Properties/AssemblyInfo.cs index 70e480e..0493a4b 100644 --- a/AyCode.Core/Properties/AssemblyInfo.cs +++ b/AyCode.Core/Properties/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("AyCode.Core.Tests")] [assembly: InternalsVisibleTo("AyCode.Core.Tests.Internal")] [assembly: InternalsVisibleTo("AyCode.Benchmark")] +[assembly: InternalsVisibleTo("FruitBank.Common")] diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 16d2b52..dc76c95 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -99,7 +99,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// All: All strings within length limits are interned (legacy behavior). /// Default: All /// - public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute; + public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All; /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).