using System; using System.Collections.Generic; using System.Linq; using StellaOps.Signals.Models; namespace StellaOps.Signals.Parsing; /// /// Migrates call graphs from legacy formats to stella.callgraph.v1. /// public static class CallgraphSchemaMigrator { /// /// Ensures document conforms to v1 schema, migrating if necessary. /// public static CallgraphDocument EnsureV1(CallgraphDocument document) { ArgumentNullException.ThrowIfNull(document); if (!string.Equals(document.Schema, CallgraphSchemaVersions.V1, StringComparison.Ordinal)) { document.Schema = CallgraphSchemaVersions.V1; } // Migrate language string to enum if (document.LanguageType == CallgraphLanguage.Unknown && !string.IsNullOrWhiteSpace(document.Language)) { document.LanguageType = ParseLanguage(document.Language); } // Ensure all nodes have visibility inferred if not set var updatedNodes = new List(document.Nodes.Count); foreach (var node in document.Nodes) { var visibility = node.Visibility == SymbolVisibility.Unknown ? InferVisibility(node.Name, node.Namespace) : node.Visibility; var symbolKey = string.IsNullOrWhiteSpace(node.SymbolKey) ? BuildSymbolKey(node) : node.SymbolKey; var isEntrypointCandidate = node.IsEntrypointCandidate || IsEntrypointCandidate(node); if (visibility != node.Visibility || !string.Equals(symbolKey, node.SymbolKey, StringComparison.Ordinal) || isEntrypointCandidate != node.IsEntrypointCandidate) { var updatedNode = node with { Visibility = visibility, SymbolKey = symbolKey, IsEntrypointCandidate = isEntrypointCandidate }; updatedNodes.Add(updatedNode); } else { updatedNodes.Add(node); } } document.Nodes = updatedNodes .OrderBy(n => n.Id, StringComparer.Ordinal) .ToList(); // Ensure all edges have reasons inferred if not set var updatedEdges = new List(document.Edges.Count); foreach (var edge in document.Edges) { if (edge.Reason == EdgeReason.Unknown) { var reason = InferEdgeReason(edge); var updatedEdge = edge with { Reason = reason }; updatedEdges.Add(updatedEdge); } else { updatedEdges.Add(edge); } } document.Edges = updatedEdges .OrderBy(e => e.SourceId, StringComparer.Ordinal) .ThenBy(e => e.TargetId, StringComparer.Ordinal) .ThenBy(e => e.Type, StringComparer.Ordinal) .ThenBy(e => e.Offset ?? -1) .ToList(); // Build entrypoints from nodes if not present if (document.Entrypoints.Count == 0) { document.Entrypoints = InferEntrypoints(document.Nodes, document.LanguageType, document.Roots); } else { document.Entrypoints = NormalizeEntrypoints(document.Entrypoints); } return document; } private static CallgraphLanguage ParseLanguage(string language) { return language.ToLowerInvariant() switch { "dotnet" or ".net" or "csharp" or "c#" => CallgraphLanguage.DotNet, "java" => CallgraphLanguage.Java, "node" or "nodejs" or "javascript" or "typescript" => CallgraphLanguage.Node, "python" => CallgraphLanguage.Python, "go" or "golang" => CallgraphLanguage.Go, "rust" => CallgraphLanguage.Rust, "ruby" => CallgraphLanguage.Ruby, "php" => CallgraphLanguage.Php, "binary" or "native" or "elf" => CallgraphLanguage.Binary, "swift" => CallgraphLanguage.Swift, "kotlin" => CallgraphLanguage.Kotlin, _ => CallgraphLanguage.Unknown }; } private static SymbolVisibility InferVisibility(string name, string? ns) { // Heuristic: symbols with "Internal" in namespace are internal if (!string.IsNullOrEmpty(ns) && ns.Contains("Internal", StringComparison.OrdinalIgnoreCase)) return SymbolVisibility.Internal; // Heuristic: private symbols often have underscore prefix or specific patterns if (name.StartsWith('_') || name.StartsWith("<")) return SymbolVisibility.Private; // Default to public for exposed symbols return SymbolVisibility.Public; } private static string BuildSymbolKey(CallgraphNode node) { var parts = new List(); if (!string.IsNullOrEmpty(node.Namespace)) parts.Add(node.Namespace); parts.Add(node.Name); return string.Join(".", parts); } private static bool IsEntrypointCandidate(CallgraphNode node) { var name = node.Name; var kind = node.Kind; // Main methods if (name.Equals("Main", StringComparison.OrdinalIgnoreCase)) return true; // Controller methods if (name.Contains("Controller") || name.Contains("Handler")) return true; // Test methods if (node.Analyzer?.ContainsKey("test") == true) return true; // Module initializers if (name.Contains(".cctor") || name.Contains("ModuleInitializer")) return true; return false; } private static EdgeReason InferEdgeReason(CallgraphEdge edge) { // Heuristic based on edge kind and type if (edge.Kind == EdgeKind.Runtime) return EdgeReason.RuntimeMinted; if (edge.Kind == EdgeKind.Heuristic) return EdgeReason.DynamicImport; // Infer from legacy type field var type = edge.Type.ToLowerInvariant(); return type switch { "call" or "direct" => EdgeReason.DirectCall, "virtual" or "callvirt" => EdgeReason.VirtualCall, "newobj" or "new" => EdgeReason.NewObj, "ldftn" or "delegate" => EdgeReason.DelegateCreate, "reflection" => EdgeReason.ReflectionString, "di" or "injection" => EdgeReason.DiBinding, "async" or "continuation" => EdgeReason.AsyncContinuation, "event" => EdgeReason.EventHandler, "generic" => EdgeReason.GenericInstantiation, "native" or "pinvoke" or "ffi" => EdgeReason.NativeInterop, _ => EdgeReason.DirectCall }; } private static List InferEntrypoints( List nodes, CallgraphLanguage language, List? roots) { var entrypoints = new List(); var order = 0; // First, add any explicitly declared roots if (roots != null) { foreach (var root in roots) { var node = nodes.FirstOrDefault(n => n.Id == root.Id); if (node == null) continue; var kind = InferEntrypointKindFromPhase(root.Phase); var phase = ParsePhase(root.Phase); var framework = InferFramework(node.Name, language); entrypoints.Add(new CallgraphEntrypoint { NodeId = root.Id, Kind = kind, Framework = framework, Source = root.Source ?? "root_declaration", Phase = phase, Order = order++ }); } } // Then, add inferred entrypoint candidates foreach (var node in nodes.Where(n => n.IsEntrypointCandidate)) { // Skip if already added from roots if (entrypoints.Any(e => e.NodeId == node.Id)) continue; var kind = InferEntrypointKind(node.Name, language); var framework = InferFramework(node.Name, language); entrypoints.Add(new CallgraphEntrypoint { NodeId = node.Id, Kind = kind, Framework = framework, Source = "inference", Phase = kind == EntrypointKind.ModuleInit ? EntrypointPhase.ModuleInit : EntrypointPhase.Runtime, Order = order++ }); } return entrypoints .OrderBy(e => (int)e.Phase) .ThenBy(e => e.Order) .ToList(); } private static List NormalizeEntrypoints(List entrypoints) { var normalized = new List(entrypoints.Count); var seen = new HashSet(StringComparer.Ordinal); foreach (var entrypoint in entrypoints) { var nodeId = entrypoint.NodeId?.Trim(); if (string.IsNullOrWhiteSpace(nodeId)) { continue; } var normalizedEntrypoint = new CallgraphEntrypoint { NodeId = nodeId, Kind = entrypoint.Kind, Route = string.IsNullOrWhiteSpace(entrypoint.Route) ? null : entrypoint.Route.Trim(), HttpMethod = string.IsNullOrWhiteSpace(entrypoint.HttpMethod) ? null : entrypoint.HttpMethod.Trim().ToUpperInvariant(), Framework = entrypoint.Framework, Source = string.IsNullOrWhiteSpace(entrypoint.Source) ? null : entrypoint.Source.Trim(), Phase = entrypoint.Phase, Order = 0 }; var key = $"{normalizedEntrypoint.NodeId}|{normalizedEntrypoint.Kind}|{normalizedEntrypoint.Framework}|{normalizedEntrypoint.Phase}|{normalizedEntrypoint.Route}|{normalizedEntrypoint.HttpMethod}|{normalizedEntrypoint.Source}"; if (seen.Add(key)) { normalized.Add(normalizedEntrypoint); } } var sorted = normalized .OrderBy(e => (int)e.Phase) .ThenBy(e => e.NodeId, StringComparer.Ordinal) .ThenBy(e => e.Kind) .ThenBy(e => e.Framework) .ThenBy(e => e.Route, StringComparer.Ordinal) .ThenBy(e => e.HttpMethod, StringComparer.Ordinal) .ThenBy(e => e.Source, StringComparer.Ordinal) .ToList(); var ordered = new List(sorted.Count); foreach (var group in sorted.GroupBy(e => e.Phase).OrderBy(g => (int)g.Key)) { var order = 0; foreach (var entrypoint in group) { entrypoint.Order = order++; ordered.Add(entrypoint); } } return ordered; } private static EntrypointKind InferEntrypointKindFromPhase(string phase) { return phase.ToLowerInvariant() switch { "init" or "module_init" or "static_init" => EntrypointKind.ModuleInit, "main" or "entry" => EntrypointKind.Main, "http" or "request" => EntrypointKind.Http, "grpc" => EntrypointKind.Grpc, "cli" or "command" => EntrypointKind.Cli, "job" or "background" => EntrypointKind.Job, "event" => EntrypointKind.Event, "queue" or "message" => EntrypointKind.MessageQueue, "timer" or "scheduled" => EntrypointKind.Timer, "test" => EntrypointKind.Test, _ => EntrypointKind.Unknown }; } private static EntrypointPhase ParsePhase(string phase) { return phase.ToLowerInvariant() switch { "init" or "module_init" or "static_init" => EntrypointPhase.ModuleInit, "startup" or "main" or "entry" => EntrypointPhase.AppStart, "shutdown" or "cleanup" => EntrypointPhase.Shutdown, _ => EntrypointPhase.Runtime }; } private static EntrypointKind InferEntrypointKind(string name, CallgraphLanguage language) { if (name.Contains("Controller", StringComparison.OrdinalIgnoreCase) || name.Contains("Handler", StringComparison.OrdinalIgnoreCase)) return EntrypointKind.Http; if (name.Equals("Main", StringComparison.OrdinalIgnoreCase)) return EntrypointKind.Main; if (name.Contains(".cctor") || name.Contains("ModuleInitializer")) return EntrypointKind.ModuleInit; if (name.Contains("Test", StringComparison.OrdinalIgnoreCase) || name.Contains("Fact", StringComparison.OrdinalIgnoreCase) || name.Contains("Theory", StringComparison.OrdinalIgnoreCase)) return EntrypointKind.Test; return EntrypointKind.Unknown; } private static EntrypointFramework InferFramework(string name, CallgraphLanguage language) { return language switch { CallgraphLanguage.DotNet when name.Contains("Controller") => EntrypointFramework.AspNetCore, CallgraphLanguage.DotNet when name.Contains("Function") => EntrypointFramework.AzureFunctions, CallgraphLanguage.Java when name.Contains("Controller") => EntrypointFramework.Spring, CallgraphLanguage.Java when name.Contains("Handler") => EntrypointFramework.AwsLambda, CallgraphLanguage.Node when name.Contains("express") => EntrypointFramework.Express, CallgraphLanguage.Node when name.Contains("fastify") => EntrypointFramework.Fastify, CallgraphLanguage.Python when name.Contains("fastapi") => EntrypointFramework.FastApi, CallgraphLanguage.Python when name.Contains("flask") => EntrypointFramework.Flask, CallgraphLanguage.Python when name.Contains("django") => EntrypointFramework.Django, CallgraphLanguage.Ruby => EntrypointFramework.Rails, CallgraphLanguage.Go when name.Contains("gin") => EntrypointFramework.Gin, CallgraphLanguage.Go when name.Contains("echo") => EntrypointFramework.Echo, CallgraphLanguage.Rust when name.Contains("actix") => EntrypointFramework.Actix, CallgraphLanguage.Rust when name.Contains("rocket") => EntrypointFramework.Rocket, _ => EntrypointFramework.Unknown }; } }