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.
This commit is contained in:
Loretta 2026-03-07 13:37:49 +01:00
parent d900442468
commit e0f546dde6
3 changed files with 79 additions and 95 deletions

View File

@ -79,20 +79,8 @@ 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)
bool? stringInternAttr = null;
if (!enableInternString)
@ -137,7 +125,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
? namedPropType.OriginalDefinition
: p.Type;
hasGenWriter = resolvedType.GetAttributes().Any(a =>
hasGenWriter = resolvedType.Locations.Any(l => l.IsInSource)
&& resolvedType.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (hasGenWriter)
@ -236,7 +225,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{
var resolvedElem = elemType is INamedTypeSymbol namedElem
? namedElem.OriginalDefinition : elemType;
elemHasGenWriter = resolvedElem.GetAttributes().Any(a =>
elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource)
&& resolvedElem.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (elemHasGenWriter)
{
@ -297,7 +287,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (dictValueKind == PropertyTypeKind.Complex)
{
var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType;
dictValueHasGenWriter = resolvedValue.GetAttributes().Any(a =>
dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource)
&& resolvedValue.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (dictValueHasGenWriter)
{
@ -343,7 +334,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
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
@ -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)
{
// 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}{{");
}
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)
{
// 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}{{");
}
else
{
sb.AppendLine($"{i}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)
{
// 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}{{");
}
else
{
sb.AppendLine($"{i}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,34 +2806,61 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// <summary>
/// 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.
/// </summary>
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
{
var propNames = new List<string>();
foreach (var member in resolvedType.GetMembers())
// Use hierarchy-walking helper — order matches runtime TypeMetadataBase
var props = GetAllSerializablePropertySymbols(resolvedType);
return props.Select(p => ComputeFnvHash(p.Name)).ToArray();
}
#endregion
/// <summary>
/// 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).
/// </summary>
private static List<IPropertySymbol> GetAllSerializablePropertySymbols(ITypeSymbol typeSymbol)
{
if (member is IPropertySymbol cp &&
cp.DeclaredAccessibility == Accessibility.Public &&
cp.GetMethod != null && cp.SetMethod != null &&
!cp.IsIndexer && !cp.IsStatic)
var result = new List<IPropertySymbol>();
var seen = new HashSet<string>();
for (var currentType = typeSymbol as INamedTypeSymbol;
currentType != null && currentType.SpecialType != SpecialType.System_Object;
currentType = currentType.BaseType)
{
var hasIgnore = cp.GetAttributes().Any(a =>
var levelProps = new List<IPropertySymbol>();
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;
propNames.Add(cp.Name);
levelProps.Add(p);
}
}
propNames.Sort(StringComparer.Ordinal);
return propNames.Select(ComputeFnvHash).ToArray();
// 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);
}
#endregion
return result;
}
#region Type analysis

View File

@ -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")]

View File

@ -99,7 +99,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// All: All strings within length limits are interned (legacy behavior).
/// Default: All
/// </summary>
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All;
/// <summary>
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).