using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace StellaOps.Scanner.Reachability; public sealed class ReachabilityGraphBuilder { private const string GraphSchemaVersion = "1.0"; private readonly HashSet nodes = new(StringComparer.Ordinal); private readonly HashSet edges = new(); public ReachabilityGraphBuilder AddNode(string symbolId) { if (!string.IsNullOrWhiteSpace(symbolId)) { nodes.Add(symbolId.Trim()); } return this; } public ReachabilityGraphBuilder AddEdge(string from, string to, string kind = "call") { if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to)) { return this; } var edge = new ReachabilityEdge(from.Trim(), to.Trim(), string.IsNullOrWhiteSpace(kind) ? "call" : kind.Trim()); edges.Add(edge); nodes.Add(edge.From); nodes.Add(edge.To); return this; } public string BuildJson(bool indented = true) { var payload = new ReachabilityGraphPayload { SchemaVersion = GraphSchemaVersion, Nodes = nodes.Select(id => new ReachabilityNode(id)).ToList(), Edges = edges.Select(edge => new ReachabilityEdgePayload(edge.From, edge.To, edge.Kind)).ToList() }; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = indented }; return JsonSerializer.Serialize(payload, options); } public static ReachabilityGraphBuilder FromFixture(string variantPath) { ArgumentException.ThrowIfNullOrWhiteSpace(variantPath); var builder = new ReachabilityGraphBuilder(); foreach (var fileName in new[] { "callgraph.static.json", "callgraph.framework.json" }) { var path = Path.Combine(variantPath, fileName); if (!File.Exists(path)) { continue; } using var stream = File.OpenRead(path); using var document = JsonDocument.Parse(stream); var root = document.RootElement; if (root.TryGetProperty("nodes", out var nodesElement) && nodesElement.ValueKind == JsonValueKind.Array) { foreach (var node in nodesElement.EnumerateArray()) { var sid = node.TryGetProperty("sid", out var sidElement) ? sidElement.GetString() : node.GetProperty("id").GetString(); builder.AddNode(sid ?? string.Empty); } } if (root.TryGetProperty("edges", out var edgesElement) && edgesElement.ValueKind == JsonValueKind.Array) { foreach (var edge in edgesElement.EnumerateArray()) { var from = edge.TryGetProperty("from", out var fromEl) ? fromEl.GetString() : edge.GetProperty("source").GetString(); var to = edge.TryGetProperty("to", out var toEl) ? toEl.GetString() : edge.GetProperty("target").GetString(); var kind = edge.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() : edge.TryGetProperty("type", out var typeEl) ? typeEl.GetString() : "call"; builder.AddEdge(from ?? string.Empty, to ?? string.Empty, kind ?? "call"); } } } return builder; } private sealed record ReachabilityEdge(string From, string To, string Kind); private sealed record ReachabilityNode(string Sid); private sealed record ReachabilityEdgePayload(string From, string To, string Kind); private sealed record ReachabilityGraphPayload { public string SchemaVersion { get; set; } = GraphSchemaVersion; public List Nodes { get; set; } = new(); public List Edges { get; set; } = new(); } }