using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace AyCode.Core.Serializers.SourceGenerator;
///
/// Build-time diagnostics for the AcBinary source generator.
///
/// Registered diagnostics:
///
/// - ACBIN001 — : detects circular type references
/// among [AcBinarySerializable] types and warns the developer to consider ref-handling mode.
/// - ACBIN002 — : ACCORE-BIN-I-T7K3
/// compile-time guard. Fires when a type opts out of EnablePolymorphDetectFeature AND still
/// declares an object property — the SGen-emitted writer would silently corrupt the wire.
///
///
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);
///
/// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as System.Object requires
/// polymorphic-prefix emit (ObjectWithTypeName) so the deserializer can resolve the
/// concrete runtime type. When the type opts out of the feature via
/// [AcBinarySerializable(enablePolymorphDetectFeature: false)], the prefix is suppressed
/// and the wire silently corrupts on round-trip (FixObj slot byte against typeof(object)
/// at read-time → 0-byte object wrapper → reader position drifts → downstream
/// DECIMAL_DRIFT / IndexOutOfRangeException).
///
/// 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
/// ([AcBinarySerializable(...enablePolymorphDetectFeature: true)] — default true).
/// 2. Change the property type to a concrete type (no polymorphism needed).
/// 3. Mark the property with [AcBinaryIgnore] — ignored properties are filtered out
/// at property enumeration, so this diagnostic does not fire for them.
///
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);
///
/// ACCORE-BIN-I-T7K3 guard: emits
/// (ACBIN002) for every System.Object-declared property on any
/// [AcBinarySerializable] type whose EnablePolymorphDetectFeature is false.
/// Per-class gating: types with the feature enabled (default) skip the check entirely; only
/// opt-out types are scanned for misuse.
///
private static void DetectAndReportPolymorphicMisuse(List 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));
}
}
}
}
///
/// Detects circular reference chains among [AcBinarySerializable] types at compile time
/// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges.
///
private static void DetectAndReportCycles(List classes, SourceProductionContext spc)
{
// Build lookup: WriterClassName → FullTypeName
var writerToFull = new Dictionary(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>(classes.Count);
foreach (var ci in classes)
{
var edges = new HashSet();
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(classes.Count);
foreach (var ci in classes)
color[ci.FullTypeName] = 0;
var stack = new List();
var reported = new HashSet();
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();
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;
}
}