using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ArtificersScrollwork.Sidecar.Models; namespace ArtificersScrollwork.Sidecar.Parsing; /// /// Walks a syntax tree and extracts ClassInfo for every class declaration. /// public class ClassWalker : CSharpSyntaxWalker { private readonly SemanticModel _model; private readonly string _filePath; public List Classes { get; } = new(); public ClassWalker(SemanticModel model, string filePath) { _model = model; _filePath = filePath; } public override void VisitClassDeclaration(ClassDeclarationSyntax node) { var symbol = _model.GetDeclaredSymbol(node); if (symbol is null) { base.VisitClassDeclaration(node); return; } var baseClass = symbol.BaseType?.Name; var interfaces = symbol.Interfaces.Select(i => i.Name).ToList(); var ns = symbol.ContainingNamespace?.ToDisplayString() ?? ""; var attrs = symbol.GetAttributes().Select(a => a.AttributeClass?.Name ?? "").ToList(); // Heuristics for class category bool isGump = IsGump(symbol); bool isMobile = IsMobile(symbol); bool isItem = IsItem(symbol); // Methods var methods = node.Members .OfType() .Select(m => ParseMethod(m, symbol)) .ToList(); // Properties var properties = node.Members .OfType() .Select(p => new Models.PropertyInfo( p.Identifier.Text, p.Type.ToString(), p.AccessorList?.Accessors.Any(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)) ?? false, p.AccessorList?.Accessors.Any(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)) ?? false )) .ToList(); Classes.Add(new ClassInfo( symbol.Name, ns, _filePath, baseClass, interfaces, properties, methods, attrs, isGump, isMobile, isItem )); base.VisitClassDeclaration(node); } private Models.MethodInfo ParseMethod(MethodDeclarationSyntax m, INamedTypeSymbol classSymbol) { var isOverride = m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.OverrideKeyword)); var isVirtual = m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.VirtualKeyword)); var parameters = m.ParameterList.Parameters .Select(p => new ParameterInfo(p.Identifier.Text, p.Type?.ToString() ?? "")) .ToList(); // Check if this method sends a Gump bool callsGump = false; string? gumpClass = null; if (m.Body is not null) { var sendGumps = m.Body.DescendantNodes() .OfType() .Where(inv => inv.ToString().Contains("SendGump")) .FirstOrDefault(); if (sendGumps is not null) { callsGump = true; // Try to extract the Gump class name from `new XxxGump(...)` var newExpr = sendGumps.DescendantNodes() .OfType() .FirstOrDefault(); gumpClass = newExpr?.Type.ToString(); } } return new Models.MethodInfo( m.Identifier.Text, m.ReturnType.ToString(), parameters, isOverride, isVirtual, callsGump, gumpClass ); } private static bool IsGump(INamedTypeSymbol symbol) { var current = symbol.BaseType; while (current is not null) { if (current.Name is "Gump" or "GumpPlus") return true; current = current.BaseType; } return false; } private static bool IsMobile(INamedTypeSymbol symbol) { var current = symbol.BaseType; while (current is not null) { if (current.Name is "Mobile" or "BaseCreature" or "BaseVendor") return true; current = current.BaseType; } return false; } private static bool IsItem(INamedTypeSymbol symbol) { var current = symbol.BaseType; while (current is not null) { if (current.Name is "Item" or "BaseWeapon" or "BaseArmor" or "BaseClothing") return true; current = current.BaseType; } return false; } }