using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ArtificersScrollwork.Sidecar.Models; namespace ArtificersScrollwork.Sidecar.Parsing; /// /// Traces method call chains across ServUO scripts using Roslyn and produces a FlowNode tree. /// Entry point: TraceMethod(className, methodName). /// Max recursion depth: 8 levels. /// public class CallChainTracer { private const int MaxDepth = 8; private readonly CSharpCompilation _compilation; private readonly Dictionary _classMap; public CallChainTracer(CSharpCompilation compilation) { _compilation = compilation; _classMap = BuildClassMap(); } // ── Public entry point ──────────────────────────────────────────────────── public FlowNode TraceMethod(string className, string methodName) { if (!_classMap.TryGetValue(className, out var entry)) throw new KeyNotFoundException($"Class '{className}' not found"); var methodSyntax = entry.Syntax.Members .OfType() .FirstOrDefault(m => m.Identifier.Text == methodName) ?? throw new KeyNotFoundException($"Method '{methodName}' not found in '{className}'"); var rootNode = new FlowNode( Id: NewId(), Type: "method_call", Label: $"{className}.{methodName}", Children: TraceMethodBody(methodSyntax, entry.Model, depth: 0), FakeInputKey: null, ResolvedGump: null, AssetRef: null ); return rootNode; } // ── Internal traversal ──────────────────────────────────────────────────── private List TraceMethodBody( MethodDeclarationSyntax method, SemanticModel model, int depth) { if (depth >= MaxDepth) return []; if (method.Body is null && method.ExpressionBody is null) return []; SyntaxNode body = (SyntaxNode?)method.Body ?? method.ExpressionBody!; return TraceStatements(body.ChildNodes(), model, depth); } private List TraceStatements( IEnumerable nodes, SemanticModel model, int depth) { var result = new List(); foreach (var node in nodes) { var children = TraceNode(node, model, depth); result.AddRange(children); } return result; } private List TraceNode(SyntaxNode node, SemanticModel model, int depth) { return node switch { IfStatementSyntax ifStmt => [TraceIf(ifStmt, model, depth)], SwitchStatementSyntax sw => [TraceSwitch(sw, model, depth)], ReturnStatementSyntax ret => [TraceReturn(ret, model, depth)], ExpressionStatementSyntax expr => TraceExpressionStatement(expr, model, depth), LocalDeclarationStatementSyntax local => TraceLocal(local, model, depth), ForEachStatementSyntax forEach => [TraceForEach(forEach, model, depth)], ForStatementSyntax forStmt => [TraceFor(forStmt, model, depth)], BlockSyntax block => TraceStatements(block.Statements, model, depth), _ => [], }; } // ── Statement handlers ──────────────────────────────────────────────────── private FlowNode TraceIf(IfStatementSyntax ifStmt, SemanticModel model, int depth) { var conditionText = ifStmt.Condition.ToString().Trim(); var fakeKey = ExtractFakeInputKey(ifStmt.Condition); var trueChildren = TraceNode(ifStmt.Statement, model, depth + 1); var falseChildren = ifStmt.Else is not null ? TraceNode(ifStmt.Else.Statement, model, depth + 1) : []; var branches = new List(); if (trueChildren.Count > 0) branches.Add(new FlowNode(NewId(), "branch_true", "true", trueChildren)); if (falseChildren.Count > 0) branches.Add(new FlowNode(NewId(), "branch_false", "false", falseChildren)); return new FlowNode( Id: NewId(), Type: "condition", Label: $"if ({Truncate(conditionText, 60)})", Children: branches, FakeInputKey: fakeKey, ResolvedGump: null, AssetRef: null ); } private FlowNode TraceSwitch(SwitchStatementSyntax sw, SemanticModel model, int depth) { var conditionText = sw.Expression.ToString().Trim(); var branches = new List(); foreach (var section in sw.Sections) { var label = string.Join(", ", section.Labels.Select(l => l.ToString().Trim())); var sectionChildren = TraceStatements(section.Statements, model, depth + 1); if (sectionChildren.Count > 0) branches.Add(new FlowNode(NewId(), "branch_case", label, sectionChildren)); } return new FlowNode( Id: NewId(), Type: "condition", Label: $"switch ({Truncate(conditionText, 60)})", Children: branches, FakeInputKey: ExtractFakeInputKey(sw.Expression), ResolvedGump: null, AssetRef: null ); } private FlowNode TraceReturn(ReturnStatementSyntax ret, SemanticModel model, int depth) { var label = ret.Expression is not null ? $"return {Truncate(ret.Expression.ToString(), 50)}" : "return"; return new FlowNode(NewId(), "return", label, []); } private List TraceExpressionStatement( ExpressionStatementSyntax expr, SemanticModel model, int depth) { return TraceExpression(expr.Expression, model, depth); } private List TraceLocal( LocalDeclarationStatementSyntax local, SemanticModel model, int depth) { // Only interesting if the initializer contains a method call var nodes = new List(); foreach (var variable in local.Declaration.Variables) { if (variable.Initializer?.Value is InvocationExpressionSyntax invoc) nodes.AddRange(TraceInvocation(invoc, model, depth, label: variable.Identifier.Text + " = …")); } return nodes; } private FlowNode TraceForEach(ForEachStatementSyntax forEach, SemanticModel model, int depth) { var label = $"foreach ({forEach.Type} {forEach.Identifier} in {Truncate(forEach.Expression.ToString(), 40)})"; var children = TraceNode(forEach.Statement, model, depth + 1); return new FlowNode(NewId(), "condition", label, children, FakeInputKey: null); } private FlowNode TraceFor(ForStatementSyntax forStmt, SemanticModel model, int depth) { var label = $"for (…; {Truncate(forStmt.Condition?.ToString() ?? "", 40)}; …)"; var children = TraceNode(forStmt.Statement, model, depth + 1); return new FlowNode(NewId(), "condition", label, children, FakeInputKey: null); } // ── Expression handlers ─────────────────────────────────────────────────── private List TraceExpression(ExpressionSyntax expr, SemanticModel model, int depth) { return expr switch { InvocationExpressionSyntax invoc => TraceInvocation(invoc, model, depth), AssignmentExpressionSyntax assign => TraceAssignment(assign, model, depth), ConditionalExpressionSyntax ternary => TraceExpressionList([ternary.WhenTrue, ternary.WhenFalse], model, depth), AwaitExpressionSyntax awaitExpr => TraceExpression(awaitExpr.Expression, model, depth), _ => [], }; } private List TraceExpressionList(IEnumerable exprs, SemanticModel model, int depth) { return exprs.SelectMany(e => TraceExpression(e, model, depth)).ToList(); } private List TraceAssignment(AssignmentExpressionSyntax assign, SemanticModel model, int depth) { return TraceExpression(assign.Right, model, depth); } private List TraceInvocation( InvocationExpressionSyntax invoc, SemanticModel model, int depth, string? label = null) { var invocStr = invoc.ToString(); // ── SendGump check ──────────────────────────────────────────────────── if (IsSendGump(invocStr)) { var gumpClass = ExtractGumpClass(invoc); return [new FlowNode( Id: NewId(), Type: "gump_send", Label: label ?? invocStr.Truncate(80), Children: [], FakeInputKey: null, ResolvedGump: gumpClass, AssetRef: null )]; } // ── Property / member access (not a real call chain node) ──────────── if (IsPropertyAccess(invoc)) { return [new FlowNode(NewId(), "property_access", label ?? Truncate(invocStr, 80), [])]; } // ── Try to resolve and recurse into the called method ───────────────── if (depth < MaxDepth) { var calledMethod = TryResolveMethod(invoc, model); if (calledMethod is not null) { var resolvedLabel = label ?? BuildCallLabel(invoc); var children = TraceMethodBody(calledMethod.Syntax, calledMethod.Model, depth + 1); if (children.Count > 0) { return [new FlowNode(NewId(), "method_call", resolvedLabel, children)]; } } } // ── Leaf call node ──────────────────────────────────────────────────── var callLabel = label ?? BuildCallLabel(invoc); return [new FlowNode(NewId(), "method_call", callLabel, [])]; } // ── Helpers ─────────────────────────────────────────────────────────────── private Dictionary BuildClassMap() { var map = new Dictionary( StringComparer.OrdinalIgnoreCase); foreach (var tree in _compilation.SyntaxTrees) { var model = _compilation.GetSemanticModel(tree); foreach (var cls in tree.GetRoot().DescendantNodes().OfType()) { var symbol = model.GetDeclaredSymbol(cls); if (symbol is not null && !map.ContainsKey(symbol.Name)) map[symbol.Name] = (cls, model); } } return map; } private record ResolvedMethod(MethodDeclarationSyntax Syntax, SemanticModel Model); private ResolvedMethod? TryResolveMethod(InvocationExpressionSyntax invoc, SemanticModel model) { try { var symbolInfo = model.GetSymbolInfo(invoc); var symbol = symbolInfo.Symbol as IMethodSymbol; if (symbol is null) return null; var containingClass = symbol.ContainingType?.Name; if (containingClass is null) return null; if (!_classMap.TryGetValue(containingClass, out var entry)) return null; var methodSyntax = entry.Syntax.Members .OfType() .FirstOrDefault(m => m.Identifier.Text == symbol.Name); return methodSyntax is not null ? new ResolvedMethod(methodSyntax, entry.Model) : null; } catch { return null; } } private static bool IsSendGump(string invocStr) => invocStr.Contains("SendGump", StringComparison.OrdinalIgnoreCase); private static bool IsPropertyAccess(InvocationExpressionSyntax invoc) { var memberAccess = invoc.Expression as MemberAccessExpressionSyntax; return memberAccess?.Name.Identifier.Text is "ToString" or "GetType"; } private static string? ExtractGumpClass(InvocationExpressionSyntax invoc) { var newExpr = invoc.DescendantNodes().OfType().FirstOrDefault(); return newExpr?.Type.ToString(); } private static string? ExtractFakeInputKey(ExpressionSyntax expr) { // If the condition references a simple identifier or member access, use it as the key return expr switch { IdentifierNameSyntax id => id.Identifier.Text, MemberAccessExpressionSyntax ma => ma.Name.Identifier.Text, BinaryExpressionSyntax bin => ExtractFakeInputKey(bin.Left), PrefixUnaryExpressionSyntax pre => ExtractFakeInputKey(pre.Operand), _ => null, }; } private static string BuildCallLabel(InvocationExpressionSyntax invoc) { var name = invoc.Expression switch { MemberAccessExpressionSyntax ma => $"{ma.Expression}.{ma.Name}", IdentifierNameSyntax id => id.Identifier.Text, _ => invoc.Expression.ToString(), }; return Truncate(name, 80); } private static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…"; private static int _counter; private static string NewId() => $"n{System.Threading.Interlocked.Increment(ref _counter)}"; } internal static class StringExtensions { public static string Truncate(this string s, int max) => s.Length <= max ? s : s[..max] + "…"; }