using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NatsNet.PortTracker.Audit; /// /// Parses .cs files using Roslyn syntax trees and builds a lookup index /// of (className, memberName) -> list of MethodInfo. /// public sealed class SourceIndexer { public record MethodInfo( string FilePath, int LineNumber, int BodyLineCount, bool IsStub, bool IsPartial, int StatementCount); // Key: (className lowercase, memberName lowercase) private readonly Dictionary<(string, string), List> _index = new(); public int FilesIndexed { get; private set; } public int MethodsIndexed { get; private set; } /// /// Recursively parses all .cs files under /// (skipping obj/ and bin/) and populates the index. /// public void IndexDirectory(string sourceDir) { var files = Directory.EnumerateFiles(sourceDir, "*.cs", SearchOption.AllDirectories) .Where(f => { var rel = Path.GetRelativePath(sourceDir, f); return !rel.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") && !rel.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}") && !rel.StartsWith($"obj{Path.DirectorySeparatorChar}") && !rel.StartsWith($"bin{Path.DirectorySeparatorChar}"); }); foreach (var file in files) { IndexFile(file); FilesIndexed++; } } /// /// Looks up all method declarations for a given class and member name. /// Case-insensitive. Returns empty list if not found. /// public List Lookup(string className, string memberName) { var key = (className.ToLowerInvariant(), memberName.ToLowerInvariant()); return _index.TryGetValue(key, out var list) ? list : []; } /// /// Returns true if the class exists anywhere in the index (any member). /// public bool HasClass(string className) { var lower = className.ToLowerInvariant(); return _index.Keys.Any(k => k.Item1 == lower); } private void IndexFile(string filePath) { var source = File.ReadAllText(filePath); var tree = CSharpSyntaxTree.ParseText(source, path: filePath); var root = tree.GetCompilationUnitRoot(); foreach (var typeDecl in root.DescendantNodes().OfType()) { var className = typeDecl.Identifier.Text.ToLowerInvariant(); // Methods foreach (var method in typeDecl.Members.OfType()) { var info = AnalyzeMethod(filePath, method.Body, method.ExpressionBody, method.GetLocation()); AddToIndex(className, method.Identifier.Text.ToLowerInvariant(), info); } // Properties (get/set are like methods) foreach (var prop in typeDecl.Members.OfType()) { var info = AnalyzeProperty(filePath, prop); AddToIndex(className, prop.Identifier.Text.ToLowerInvariant(), info); } // Constructors — index as class name foreach (var ctor in typeDecl.Members.OfType()) { var info = AnalyzeMethod(filePath, ctor.Body, ctor.ExpressionBody, ctor.GetLocation()); AddToIndex(className, ctor.Identifier.Text.ToLowerInvariant(), info); } } } private MethodInfo AnalyzeMethod(string filePath, BlockSyntax? body, ArrowExpressionClauseSyntax? expressionBody, Location location) { var lineSpan = location.GetLineSpan(); var lineNumber = lineSpan.StartLinePosition.Line + 1; if (expressionBody is not null) { // Expression-bodied: => expr; var isStub = IsNotImplementedExpression(expressionBody.Expression); return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1); } if (body is null || body.Statements.Count == 0) { // No body or empty body return new MethodInfo(filePath, lineNumber, 0, IsStub: true, IsPartial: false, StatementCount: 0); } var bodyLines = body.GetLocation().GetLineSpan(); var bodyLineCount = bodyLines.EndLinePosition.Line - bodyLines.StartLinePosition.Line - 1; // exclude braces var statements = body.Statements; var hasNotImplemented = statements.Any(s => IsNotImplementedStatement(s)); var meaningfulCount = statements.Count(s => !IsNotImplementedStatement(s)); // Pure stub: single throw NotImplementedException if (statements.Count == 1 && hasNotImplemented) return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: true, IsPartial: false, StatementCount: 0); // Partial: has some logic AND a NotImplementedException if (hasNotImplemented && meaningfulCount > 0) return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: true, StatementCount: meaningfulCount); // Real logic return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: false, StatementCount: meaningfulCount); } private MethodInfo AnalyzeProperty(string filePath, PropertyDeclarationSyntax prop) { var lineSpan = prop.GetLocation().GetLineSpan(); var lineNumber = lineSpan.StartLinePosition.Line + 1; // Expression-bodied property: int Foo => expr; if (prop.ExpressionBody is not null) { var isStub = IsNotImplementedExpression(prop.ExpressionBody.Expression); return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1); } // Auto-property: int Foo { get; set; } — this is valid, not a stub if (prop.AccessorList is not null && prop.AccessorList.Accessors.All(a => a.Body is null && a.ExpressionBody is null)) return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1); // Property with accessor bodies — check if any are stubs if (prop.AccessorList is not null) { var hasStub = prop.AccessorList.Accessors.Any(a => (a.ExpressionBody is not null && IsNotImplementedExpression(a.ExpressionBody.Expression)) || (a.Body is not null && a.Body.Statements.Count == 1 && IsNotImplementedStatement(a.Body.Statements[0]))); return new MethodInfo(filePath, lineNumber, 0, IsStub: hasStub, IsPartial: false, StatementCount: hasStub ? 0 : 1); } return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1); } private static bool IsNotImplementedExpression(ExpressionSyntax expr) { // throw new NotImplementedException(...) if (expr is ThrowExpressionSyntax throwExpr) return throwExpr.Expression is ObjectCreationExpressionSyntax oc && oc.Type.ToString().Contains("NotImplementedException"); // new NotImplementedException() — shouldn't normally be standalone but handle it return expr is ObjectCreationExpressionSyntax oc2 && oc2.Type.ToString().Contains("NotImplementedException"); } private static bool IsNotImplementedStatement(StatementSyntax stmt) { // throw new NotImplementedException(...); if (stmt is ThrowStatementSyntax throwStmt && throwStmt.Expression is not null) return throwStmt.Expression is ObjectCreationExpressionSyntax oc && oc.Type.ToString().Contains("NotImplementedException"); // Expression statement containing throw expression if (stmt is ExpressionStatementSyntax exprStmt) return IsNotImplementedExpression(exprStmt.Expression); return false; } private void AddToIndex(string className, string memberName, MethodInfo info) { var key = (className, memberName); if (!_index.TryGetValue(key, out var list)) { list = []; _index[key] = list; } list.Add(info); MethodsIndexed++; } }