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,21 +79,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var member in typeSymbol.GetMembers())
|
foreach (var p in GetAllSerializablePropertySymbols(typeSymbol))
|
||||||
{
|
{
|
||||||
if (member is IPropertySymbol p &&
|
// String interning attribútum detektálás (null = no attr, true/false = explicit)
|
||||||
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;
|
bool? stringInternAttr = null;
|
||||||
if (!enableInternString)
|
if (!enableInternString)
|
||||||
{
|
{
|
||||||
|
|
@ -137,8 +125,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
? namedPropType.OriginalDefinition
|
? namedPropType.OriginalDefinition
|
||||||
: p.Type;
|
: p.Type;
|
||||||
|
|
||||||
hasGenWriter = resolvedType.GetAttributes().Any(a =>
|
hasGenWriter = resolvedType.Locations.Any(l => l.IsInSource)
|
||||||
a.AttributeClass?.ToDisplayString() == AttributeName);
|
&& resolvedType.GetAttributes().Any(a =>
|
||||||
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
||||||
|
|
||||||
if (hasGenWriter)
|
if (hasGenWriter)
|
||||||
{
|
{
|
||||||
|
|
@ -236,8 +225,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
{
|
{
|
||||||
var resolvedElem = elemType is INamedTypeSymbol namedElem
|
var resolvedElem = elemType is INamedTypeSymbol namedElem
|
||||||
? namedElem.OriginalDefinition : elemType;
|
? namedElem.OriginalDefinition : elemType;
|
||||||
elemHasGenWriter = resolvedElem.GetAttributes().Any(a =>
|
elemHasGenWriter = resolvedElem.Locations.Any(l => l.IsInSource)
|
||||||
a.AttributeClass?.ToDisplayString() == AttributeName);
|
&& resolvedElem.GetAttributes().Any(a =>
|
||||||
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
||||||
if (elemHasGenWriter)
|
if (elemHasGenWriter)
|
||||||
{
|
{
|
||||||
// Read element type's EnableMetadataFeature
|
// Read element type's EnableMetadataFeature
|
||||||
|
|
@ -297,8 +287,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
if (dictValueKind == PropertyTypeKind.Complex)
|
if (dictValueKind == PropertyTypeKind.Complex)
|
||||||
{
|
{
|
||||||
var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType;
|
var resolvedValue = valueType is INamedTypeSymbol nvt ? nvt.OriginalDefinition : valueType;
|
||||||
dictValueHasGenWriter = resolvedValue.GetAttributes().Any(a =>
|
dictValueHasGenWriter = resolvedValue.Locations.Any(l => l.IsInSource)
|
||||||
a.AttributeClass?.ToDisplayString() == AttributeName);
|
&& resolvedValue.GetAttributes().Any(a =>
|
||||||
|
a.AttributeClass?.ToDisplayString() == AttributeName);
|
||||||
if (dictValueHasGenWriter)
|
if (dictValueHasGenWriter)
|
||||||
{
|
{
|
||||||
var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue);
|
var vfn = BuildFlatName((INamedTypeSymbol)resolvedValue);
|
||||||
|
|
@ -342,7 +333,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
propEnableMetadata, elemEnableMetadata,
|
propEnableMetadata, elemEnableMetadata,
|
||||||
childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan,
|
childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan,
|
||||||
elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan));
|
elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IId<T>: Id first (index 0), then alphabetical — matches runtime TypeMetadataBase ordering
|
// IId<T>: 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 className = BuildFlatName(typeSymbol);
|
||||||
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
|
||||||
|
|
@ -1214,16 +1205,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
var writer = p.WriterClassName;
|
var writer = p.WriterClassName;
|
||||||
var nextDepth = "depth + 1";
|
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}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
sb.AppendLine($"{i}else");
|
||||||
sb.AppendLine($"{i}else");
|
sb.AppendLine($"{i}{{");
|
||||||
sb.AppendLine($"{i}{{");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.AppendLine($"{i}{{");
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxDepth check — matches WriteObjectGenerated
|
// MaxDepth check — matches WriteObjectGenerated
|
||||||
sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
sb.AppendLine($"{i} if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
||||||
|
|
@ -1351,19 +1336,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
{
|
{
|
||||||
var writer = p.ElementWriterClassName;
|
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}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
||||||
sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
sb.AppendLine($"{i}else");
|
||||||
sb.AppendLine($"{i}else");
|
sb.AppendLine($"{i}{{");
|
||||||
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);");
|
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);");
|
||||||
|
|
||||||
|
|
@ -1587,19 +1564,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
var keyType = p.DictKeyTypeName ?? "object";
|
var keyType = p.DictKeyTypeName ?? "object";
|
||||||
var valType = p.DictValueTypeName ?? "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}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
|
sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
||||||
sb.AppendLine($"{i}else if (depth > context.MaxDepth) context.WriteByte(BinaryTypeCode.Null);");
|
sb.AppendLine($"{i}else");
|
||||||
sb.AppendLine($"{i}else");
|
sb.AppendLine($"{i}{{");
|
||||||
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.WriteByte(BinaryTypeCode.Dictionary);");
|
||||||
sb.AppendLine($"{i} context.WriteVarUInt((uint){a}.Count);");
|
sb.AppendLine($"{i} context.WriteVarUInt((uint){a}.Count);");
|
||||||
|
|
@ -2749,21 +2718,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
var needsInternScan = false;
|
var needsInternScan = false;
|
||||||
|
|
||||||
// Check properties for string interning or complex children
|
// 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
|
// Early exit: if all flags are already true, no need to check more properties
|
||||||
if (needsIdScan && needsAllRefScan && needsInternScan) break;
|
if (needsIdScan && needsAllRefScan && needsInternScan) break;
|
||||||
|
|
||||||
|
|
@ -2850,35 +2806,62 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes FNV-1a hashes for all serializable properties of a child type.
|
/// Computes FNV-1a hashes for all serializable properties of a child type.
|
||||||
/// Property filtering and ordering matches runtime TypeMetadataBase exactly:
|
/// 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>
|
/// </summary>
|
||||||
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
|
private static int[] ComputeChildPropertyHashes(ITypeSymbol resolvedType)
|
||||||
{
|
{
|
||||||
var propNames = new List<string>();
|
// Use hierarchy-walking helper — order matches runtime TypeMetadataBase
|
||||||
foreach (var member in resolvedType.GetMembers())
|
var props = GetAllSerializablePropertySymbols(resolvedType);
|
||||||
{
|
return props.Select(p => ComputeFnvHash(p.Name)).ToArray();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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)
|
||||||
|
{
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
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
|
#region Type analysis
|
||||||
|
|
||||||
private static bool IsNullableVT(ITypeSymbol t) =>
|
private static bool IsNullableVT(ITypeSymbol t) =>
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ using System.Runtime.CompilerServices;
|
||||||
[assembly: InternalsVisibleTo("AyCode.Core.Tests")]
|
[assembly: InternalsVisibleTo("AyCode.Core.Tests")]
|
||||||
[assembly: InternalsVisibleTo("AyCode.Core.Tests.Internal")]
|
[assembly: InternalsVisibleTo("AyCode.Core.Tests.Internal")]
|
||||||
[assembly: InternalsVisibleTo("AyCode.Benchmark")]
|
[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).
|
/// All: All strings within length limits are interned (legacy behavior).
|
||||||
/// Default: All
|
/// Default: All
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
|
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue