using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using StellaOps.Signals.Models; namespace StellaOps.Signals.Parsing; /// /// Simple JSON-based callgraph parser used for initial language coverage. /// public sealed class SimpleJsonCallgraphParser : ICallgraphParser { private readonly JsonSerializerOptions serializerOptions; public SimpleJsonCallgraphParser(string language) { ArgumentException.ThrowIfNullOrWhiteSpace(language); Language = language; serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public string Language { get; } public async Task ParseAsync(Stream artifactStream, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(artifactStream); using var document = await JsonDocument.ParseAsync(artifactStream, cancellationToken: cancellationToken).ConfigureAwait(false); var root = document.RootElement; if (TryParseLegacy(root, out var legacyResult)) { return legacyResult; } if (TryParseSchemaV1(root, out var schemaResult)) { return schemaResult; } throw new CallgraphParserValidationException("Callgraph artifact payload is empty or missing required fields."); } private static bool TryParseLegacy(JsonElement root, out CallgraphParseResult result) { result = default!; if (!root.TryGetProperty("graph", out var graphElement)) { return false; } var nodesElement = graphElement.GetProperty("nodes"); var edgesElement = graphElement.TryGetProperty("edges", out var edgesValue) ? edgesValue : default; var nodes = new List(nodesElement.GetArrayLength()); foreach (var nodeElement in nodesElement.EnumerateArray()) { var id = nodeElement.GetProperty("id").GetString(); if (string.IsNullOrWhiteSpace(id)) { throw new CallgraphParserValidationException("Callgraph node is missing an id."); } nodes.Add(new CallgraphNode( Id: id.Trim(), Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(), Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function", Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null, File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null, Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null)); } var edges = new List(); if (edgesElement.ValueKind == JsonValueKind.Array) { foreach (var edgeElement in edgesElement.EnumerateArray()) { var source = edgeElement.GetProperty("source").GetString(); var target = edgeElement.GetProperty("target").GetString(); if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target)) { throw new CallgraphParserValidationException("Callgraph edge requires both source and target."); } var type = edgeElement.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "call" : "call"; edges.Add(new CallgraphEdge(source.Trim(), target.Trim(), type)); } } var formatVersion = root.TryGetProperty("formatVersion", out var versionEl) ? versionEl.GetString() : null; result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(formatVersion) ? "1.0" : formatVersion!.Trim()); return true; } private static bool TryParseSchemaV1(JsonElement root, out CallgraphParseResult result) { result = default!; if (!root.TryGetProperty("nodes", out var nodesElement) && !root.TryGetProperty("edges", out _)) { return false; } var nodes = new List(); if (nodesElement.ValueKind == JsonValueKind.Array) { foreach (var nodeElement in nodesElement.EnumerateArray()) { var id = nodeElement.TryGetProperty("sid", out var sidEl) ? sidEl.GetString() : nodeElement.GetProperty("id").GetString(); if (string.IsNullOrWhiteSpace(id)) { throw new CallgraphParserValidationException("Callgraph node is missing an id."); } nodes.Add(new CallgraphNode( Id: id.Trim(), Name: nodeElement.TryGetProperty("name", out var nameEl) ? nameEl.GetString() ?? id.Trim() : id.Trim(), Kind: nodeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "function" : "function", Namespace: nodeElement.TryGetProperty("namespace", out var nsEl) ? nsEl.GetString() : null, File: nodeElement.TryGetProperty("file", out var fileEl) ? fileEl.GetString() : null, Line: nodeElement.TryGetProperty("line", out var lineEl) && lineEl.ValueKind == JsonValueKind.Number ? lineEl.GetInt32() : null)); } } if (!root.TryGetProperty("edges", out var edgesElement) || edgesElement.ValueKind != JsonValueKind.Array) { edgesElement = default; } var edges = new List(); if (edgesElement.ValueKind == JsonValueKind.Array) { foreach (var edgeElement in edgesElement.EnumerateArray()) { var from = edgeElement.TryGetProperty("from", out var fromEl) ? fromEl.GetString() : edgeElement.GetProperty("source").GetString(); var to = edgeElement.TryGetProperty("to", out var toEl) ? toEl.GetString() : edgeElement.GetProperty("target").GetString(); if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to)) { throw new CallgraphParserValidationException("Callgraph edge requires both source and target."); } var kind = edgeElement.TryGetProperty("kind", out var kindEl) ? kindEl.GetString() ?? "call" : edgeElement.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "call" : "call"; edges.Add(new CallgraphEdge(from.Trim(), to.Trim(), kind)); } } if (nodes.Count == 0) { // When nodes are omitted (framework overlay), derive them from the referenced edges. var uniqueNodeIds = new HashSet(StringComparer.Ordinal); foreach (var edge in edges) { uniqueNodeIds.Add(edge.SourceId); uniqueNodeIds.Add(edge.TargetId); } foreach (var nodeId in uniqueNodeIds) { nodes.Add(new CallgraphNode(nodeId, nodeId, "function", null, null, null)); } } var schemaVersion = root.TryGetProperty("schema_version", out var schemaEl) ? schemaEl.GetString() : "1.0"; result = new CallgraphParseResult(nodes, edges, string.IsNullOrWhiteSpace(schemaVersion) ? "1.0" : schemaVersion!.Trim()); return true; } }