diff --git a/reports/current.md b/reports/current.md index e0a6d28..ea1a213 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-27 10:16:08 UTC +Generated: 2026-02-27 10:16:59 UTC ## Modules (12 total) diff --git a/reports/report_c5c6fbc.md b/reports/report_c5c6fbc.md new file mode 100644 index 0000000..ea1a213 --- /dev/null +++ b/reports/report_c5c6fbc.md @@ -0,0 +1,35 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-27 10:16:59 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| unknown | 3394 | +| verified | 279 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2680 | +| n_a | 187 | +| verified | 390 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**868/6942 items complete (12.5%)** diff --git a/tools/NatsNet.PortTracker/Audit/SourceIndexer.cs b/tools/NatsNet.PortTracker/Audit/SourceIndexer.cs new file mode 100644 index 0000000..85b6443 --- /dev/null +++ b/tools/NatsNet.PortTracker/Audit/SourceIndexer.cs @@ -0,0 +1,201 @@ +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++; + } +}