From e0f546dde67bafe476146d86d37f54b93e35c1ca Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 7 Mar 2026 13:37:49 +0100 Subject: [PATCH] Improve property ordering, null handling, and string interning Refactored property enumeration in AcBinarySourceGenerator to match runtime ordering and filtering using a new helper. Null checks for reference types are now unconditional in generated code. Changed default string interning mode to All. Added InternalsVisibleTo for FruitBank.Common. Writer attribute checks now only apply to source-defined types. --- .../AcBinarySourceGenerator.cs | 171 ++++++++---------- AyCode.Core/Properties/AssemblyInfo.cs | 1 + .../Binaries/AcBinarySerializerOptions.cs | 2 +- 3 files changed, 79 insertions(+), 95 deletions(-) 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).