183 lines
8.5 KiB
C#
183 lines
8.5 KiB
C#
using System.Collections.Generic;
|
|
using Microsoft.CodeAnalysis;
|
|
|
|
namespace AyCode.Core.Serializers.SourceGenerator;
|
|
|
|
/// <summary>
|
|
/// Build-time diagnostics for the AcBinary source generator.
|
|
///
|
|
/// <para><b>Registered diagnostics</b>:</para>
|
|
/// <list type="bullet">
|
|
/// <item><c>ACBIN001</c> — <see cref="CircularReferenceWarning"/>: detects circular type references
|
|
/// among <c>[AcBinarySerializable]</c> types and warns the developer to consider ref-handling mode.</item>
|
|
/// <item><c>ACBIN002</c> — <see cref="PolymorphicPropertyWithFeatureDisabledError"/>: ACCORE-BIN-I-T7K3
|
|
/// compile-time guard. Fires when a type opts out of <c>EnablePolymorphDetectFeature</c> AND still
|
|
/// declares an <c>object</c> property — the SGen-emitted writer would silently corrupt the wire.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public partial class AcBinarySourceGenerator
|
|
{
|
|
private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
|
|
id: "ACBIN001",
|
|
title: "Circular reference detected",
|
|
messageFormat: "Type '{0}' participates in a circular reference chain: {1}. Consider using ReferenceHandling.OnlyId or .All to avoid exponential serialization size.",
|
|
category: "AcBinarySerializer",
|
|
defaultSeverity: DiagnosticSeverity.Warning,
|
|
isEnabledByDefault: true);
|
|
|
|
/// <summary>
|
|
/// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as <c>System.Object</c> requires
|
|
/// polymorphic-prefix emit (<c>ObjectWithTypeName</c>) so the deserializer can resolve the
|
|
/// concrete runtime type. When the type opts out of the feature via
|
|
/// <c>[AcBinarySerializable(enablePolymorphDetectFeature: false)]</c>, the prefix is suppressed
|
|
/// and the wire silently corrupts on round-trip (FixObj slot byte against <c>typeof(object)</c>
|
|
/// at read-time → 0-byte object wrapper → reader position drifts → downstream
|
|
/// <c>DECIMAL_DRIFT</c> / <c>IndexOutOfRangeException</c>).
|
|
///
|
|
/// Surface the misconfiguration at build time so the silent corruption is structurally
|
|
/// impossible. Three escape hatches for the developer:
|
|
/// 1. Enable the polymorph-detect feature on the type
|
|
/// (<c>[AcBinarySerializable(...enablePolymorphDetectFeature: true)]</c> — default true).
|
|
/// 2. Change the property type to a concrete type (no polymorphism needed).
|
|
/// 3. Mark the property with <c>[AcBinaryIgnore]</c> — ignored properties are filtered out
|
|
/// at property enumeration, so this diagnostic does not fire for them.
|
|
/// </summary>
|
|
private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new(
|
|
id: "ACBIN002",
|
|
title: "Polymorphic property requires EnablePolymorphDetectFeature",
|
|
messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but EnablePolymorphDetectFeature is disabled on the type. " +
|
|
"The generated writer would silently corrupt the wire on round-trip. " +
|
|
"To fix: (1) enable EnablePolymorphDetectFeature on [AcBinarySerializable], (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].",
|
|
category: "AcBinarySerializer",
|
|
defaultSeverity: DiagnosticSeverity.Error,
|
|
isEnabledByDefault: true);
|
|
|
|
/// <summary>
|
|
/// ACCORE-BIN-I-T7K3 guard: emits <see cref="PolymorphicPropertyWithFeatureDisabledError"/>
|
|
/// (ACBIN002) for every <c>System.Object</c>-declared property on any
|
|
/// <c>[AcBinarySerializable]</c> type whose <c>EnablePolymorphDetectFeature</c> is <c>false</c>.
|
|
/// Per-class gating: types with the feature enabled (default) skip the check entirely; only
|
|
/// opt-out types are scanned for misuse.
|
|
/// </summary>
|
|
private static void DetectAndReportPolymorphicMisuse(List<SerializableClassInfo> classes, SourceProductionContext spc)
|
|
{
|
|
foreach (var ci in classes)
|
|
{
|
|
if (ci.EnablePolymorphDetect) continue; // Feature enabled → polymorphic prefix is emitted, no misuse possible.
|
|
|
|
foreach (var p in ci.Properties)
|
|
{
|
|
if (p.IsObjectDeclaredType)
|
|
{
|
|
spc.ReportDiagnostic(Diagnostic.Create(
|
|
PolymorphicPropertyWithFeatureDisabledError, Location.None,
|
|
ci.ClassName, p.Name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects circular reference chains among [AcBinarySerializable] types at compile time
|
|
/// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges.
|
|
/// </summary>
|
|
private static void DetectAndReportCycles(List<SerializableClassInfo> classes, SourceProductionContext spc)
|
|
{
|
|
// Build lookup: WriterClassName → FullTypeName
|
|
var writerToFull = new Dictionary<string, string>(classes.Count);
|
|
foreach (var ci in classes)
|
|
{
|
|
var writerName = string.IsNullOrEmpty(ci.Namespace)
|
|
? $"{ci.ClassName}_GeneratedWriter"
|
|
: $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter";
|
|
writerToFull[writerName] = ci.FullTypeName;
|
|
}
|
|
|
|
// Build adjacency list: FullTypeName → set of referenced FullTypeNames
|
|
var adjacency = new Dictionary<string, HashSet<string>>(classes.Count);
|
|
foreach (var ci in classes)
|
|
{
|
|
var edges = new HashSet<string>();
|
|
foreach (var p in ci.Properties)
|
|
{
|
|
if (p.TypeKind == PropertyTypeKind.Complex && p.HasGeneratedWriter && p.WriterClassName != null)
|
|
{
|
|
if (writerToFull.TryGetValue(p.WriterClassName, out var target))
|
|
edges.Add(target);
|
|
}
|
|
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.ElementWriterClassName != null)
|
|
{
|
|
if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target))
|
|
edges.Add(target);
|
|
}
|
|
if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter && p.DictValueWriterClassName != null)
|
|
{
|
|
if (writerToFull.TryGetValue(p.DictValueWriterClassName, out var target))
|
|
edges.Add(target);
|
|
}
|
|
}
|
|
adjacency[ci.FullTypeName] = edges;
|
|
}
|
|
|
|
// DFS with 3-color marking: White=0, Gray=1, Black=2
|
|
var color = new Dictionary<string, int>(classes.Count);
|
|
foreach (var ci in classes)
|
|
color[ci.FullTypeName] = 0;
|
|
|
|
var stack = new List<string>();
|
|
var reported = new HashSet<string>();
|
|
|
|
void Dfs(string node)
|
|
{
|
|
color[node] = 1; // Gray
|
|
stack.Add(node);
|
|
|
|
if (adjacency.TryGetValue(node, out var neighbors))
|
|
{
|
|
foreach (var next in neighbors)
|
|
{
|
|
if (!color.TryGetValue(next, out var c)) continue;
|
|
if (c == 1) // Gray → back-edge = cycle
|
|
{
|
|
var cycleStart = stack.IndexOf(next);
|
|
var parts = new List<string>();
|
|
for (var i = cycleStart; i < stack.Count; i++)
|
|
parts.Add(ShortTypeName(stack[i]));
|
|
parts.Add(ShortTypeName(next)); // close the cycle
|
|
|
|
var cycleDesc = string.Join(" → ", parts);
|
|
for (var i = cycleStart; i < stack.Count; i++)
|
|
{
|
|
if (reported.Add(stack[i]))
|
|
{
|
|
spc.ReportDiagnostic(Diagnostic.Create(
|
|
CircularReferenceWarning, Location.None,
|
|
ShortTypeName(stack[i]), cycleDesc));
|
|
}
|
|
}
|
|
}
|
|
else if (c == 0) // White → unvisited
|
|
{
|
|
Dfs(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
stack.RemoveAt(stack.Count - 1);
|
|
color[node] = 2; // Black
|
|
}
|
|
|
|
foreach (var ci in classes)
|
|
{
|
|
if (color[ci.FullTypeName] == 0)
|
|
Dfs(ci.FullTypeName);
|
|
}
|
|
}
|
|
|
|
private static string ShortTypeName(string fullTypeName)
|
|
{
|
|
var dot = fullTypeName.LastIndexOf('.');
|
|
return dot >= 0 ? fullTypeName.Substring(dot + 1) : fullTypeName;
|
|
}
|
|
}
|