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:
parent
d900442468
commit
e0f546dde6
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue