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; } }