feat: add SourceIndexer — Roslyn-based .NET source parser for audit
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
35
reports/report_c5c6fbc.md
Normal file
35
reports/report_c5c6fbc.md
Normal file
@@ -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%)**
|
||||
201
tools/NatsNet.PortTracker/Audit/SourceIndexer.cs
Normal file
201
tools/NatsNet.PortTracker/Audit/SourceIndexer.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace NatsNet.PortTracker.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Parses .cs files using Roslyn syntax trees and builds a lookup index
|
||||
/// of (className, memberName) -> list of MethodInfo.
|
||||
/// </summary>
|
||||
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<MethodInfo>> _index = new();
|
||||
|
||||
public int FilesIndexed { get; private set; }
|
||||
public int MethodsIndexed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Recursively parses all .cs files under <paramref name="sourceDir"/>
|
||||
/// (skipping obj/ and bin/) and populates the index.
|
||||
/// </summary>
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up all method declarations for a given class and member name.
|
||||
/// Case-insensitive. Returns empty list if not found.
|
||||
/// </summary>
|
||||
public List<MethodInfo> Lookup(string className, string memberName)
|
||||
{
|
||||
var key = (className.ToLowerInvariant(), memberName.ToLowerInvariant());
|
||||
return _index.TryGetValue(key, out var list) ? list : [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the class exists anywhere in the index (any member).
|
||||
/// </summary>
|
||||
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<TypeDeclarationSyntax>())
|
||||
{
|
||||
var className = typeDecl.Identifier.Text.ToLowerInvariant();
|
||||
|
||||
// Methods
|
||||
foreach (var method in typeDecl.Members.OfType<MethodDeclarationSyntax>())
|
||||
{
|
||||
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<PropertyDeclarationSyntax>())
|
||||
{
|
||||
var info = AnalyzeProperty(filePath, prop);
|
||||
AddToIndex(className, prop.Identifier.Text.ToLowerInvariant(), info);
|
||||
}
|
||||
|
||||
// Constructors — index as class name
|
||||
foreach (var ctor in typeDecl.Members.OfType<ConstructorDeclarationSyntax>())
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user