MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Analysis/TechnicalDebtPlugin.cs

971 lines
33 KiB
C#
Executable File

using MarketAlly.AIPlugin;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace MarketAlly.AIPlugin.Analysis.Plugins
{
[AIPlugin("TechnicalDebt", "Quantifies and tracks technical debt with actionable improvement recommendations")]
public class TechnicalDebtPlugin : IAIPlugin
{
[AIParameter("Full path to the project or directory to analyze", required: true)]
public string ProjectPath { get; set; } = string.Empty;
[AIParameter("Calculate code complexity debt", required: false)]
public bool CalculateComplexityDebt { get; set; } = true;
[AIParameter("Analyze documentation debt", required: false)]
public bool AnalyzeDocumentationDebt { get; set; } = true;
[AIParameter("Check for outdated dependencies", required: false)]
public bool CheckDependencyDebt { get; set; } = true;
[AIParameter("Analyze test coverage debt", required: false)]
public bool AnalyzeTestDebt { get; set; } = true;
[AIParameter("Generate prioritized improvement plan", required: false)]
public bool GenerateImprovementPlan { get; set; } = true;
[AIParameter("Track debt trends over time", required: false)]
public bool TrackTrends { get; set; } = false;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["projectPath"] = typeof(string),
["calculateComplexityDebt"] = typeof(bool),
["analyzeDocumentationDebt"] = typeof(bool),
["checkDependencyDebt"] = typeof(bool),
["analyzeTestDebt"] = typeof(bool),
["generateImprovementPlan"] = typeof(bool),
["trackTrends"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
try
{
// Extract parameters
string projectPath = parameters["projectPath"]?.ToString() ?? string.Empty;
bool calculateComplexityDebt = GetBoolParameter(parameters, "calculateComplexityDebt", true);
bool analyzeDocumentationDebt = GetBoolParameter(parameters, "analyzeDocumentationDebt", true);
bool checkDependencyDebt = GetBoolParameter(parameters, "checkDependencyDebt", true);
bool analyzeTestDebt = GetBoolParameter(parameters, "analyzeTestDebt", true);
bool generateImprovementPlan = GetBoolParameter(parameters, "generateImprovementPlan", true);
bool trackTrends = GetBoolParameter(parameters, "trackTrends", false);
// Validate path
if (!Directory.Exists(projectPath) && !File.Exists(projectPath))
{
return new AIPluginResult(
new DirectoryNotFoundException($"Path not found: {projectPath}"),
"Path not found"
);
}
// Initialize debt analysis
var debtAnalysis = new TechnicalDebtAnalysis
{
ProjectPath = projectPath,
AnalysisDate = DateTime.UtcNow,
ComplexityDebt = new ComplexityDebtMetrics(),
DocumentationDebt = new DocumentationDebtMetrics(),
DependencyDebt = new DependencyDebtMetrics(),
TestDebt = new TestDebtMetrics(),
DebtItems = new List<DebtItem>()
};
// Get all source files
var sourceFiles = GetSourceFiles(projectPath);
var projectFiles = GetProjectFiles(projectPath);
// Analyze complexity debt
if (calculateComplexityDebt)
{
await AnalyzeComplexityDebt(sourceFiles, debtAnalysis);
}
// Analyze documentation debt
if (analyzeDocumentationDebt)
{
await AnalyzeDocumentationDebtMethod(sourceFiles, debtAnalysis);
}
// Analyze dependency debt
if (checkDependencyDebt)
{
await AnalyzeDependencyDebt(projectFiles, debtAnalysis);
}
// Analyze test debt
if (analyzeTestDebt)
{
await AnalyzeTestDebtMethod(sourceFiles, debtAnalysis);
}
// Calculate overall debt score
var debtScore = CalculateOverallDebtScore(debtAnalysis);
// Generate improvement plan
var improvementPlan = new List<ImprovementAction>();
if (generateImprovementPlan)
{
improvementPlan = GenerateImprovementPlanMethod(debtAnalysis);
}
// Track trends if requested
object? debtTrends = null;
if (trackTrends)
{
debtTrends = await TrackDebtTrends(projectPath, debtAnalysis);
}
var result = new
{
ProjectPath = projectPath,
AnalysisDate = debtAnalysis.AnalysisDate,
DebtScore = debtScore,
FilesAnalyzed = sourceFiles.Count,
ComplexityDebt = calculateComplexityDebt ? new
{
debtAnalysis.ComplexityDebt.TotalComplexityPoints,
debtAnalysis.ComplexityDebt.AverageMethodComplexity,
debtAnalysis.ComplexityDebt.HighComplexityMethods,
debtAnalysis.ComplexityDebt.EstimatedRefactoringHours,
DebtLevel = GetDebtLevel(debtAnalysis.ComplexityDebt.TotalComplexityPoints, "Complexity")
} : null,
DocumentationDebt = analyzeDocumentationDebt ? new
{
debtAnalysis.DocumentationDebt.TotalMethods,
debtAnalysis.DocumentationDebt.UndocumentedMethods,
debtAnalysis.DocumentationDebt.DocumentationCoverage,
debtAnalysis.DocumentationDebt.EstimatedDocumentationHours,
DebtLevel = GetDebtLevel(debtAnalysis.DocumentationDebt.UndocumentedMethods, "Documentation")
} : null,
DependencyDebt = checkDependencyDebt ? new
{
debtAnalysis.DependencyDebt.TotalDependencies,
debtAnalysis.DependencyDebt.OutdatedDependencies,
debtAnalysis.DependencyDebt.VulnerableDependencies,
debtAnalysis.DependencyDebt.MajorVersionsBehind,
debtAnalysis.DependencyDebt.EstimatedUpgradeHours,
DebtLevel = GetDebtLevel(debtAnalysis.DependencyDebt.OutdatedDependencies, "Dependency")
} : null,
TestDebt = analyzeTestDebt ? new
{
debtAnalysis.TestDebt.TotalMethods,
debtAnalysis.TestDebt.UntestedMethods,
debtAnalysis.TestDebt.TestCoverage,
debtAnalysis.TestDebt.EstimatedTestingHours,
DebtLevel = GetDebtLevel(debtAnalysis.TestDebt.UntestedMethods, "Test")
} : null,
DebtItems = debtAnalysis.DebtItems.OrderByDescending(d => d.Priority).Take(20).Select(d => new
{
d.Type,
d.Category,
d.Description,
d.Location,
d.Priority,
d.EstimatedEffort,
d.Impact,
d.RecommendedAction
}).ToList(),
ImprovementPlan = generateImprovementPlan ? improvementPlan.Select(i => new
{
i.Phase,
i.Priority,
i.Title,
i.Description,
i.EstimatedHours,
i.ExpectedBenefit,
i.Dependencies
}).ToList() : null,
DebtTrends = debtTrends,
Summary = new
{
TotalDebtItems = debtAnalysis.DebtItems.Count,
HighPriorityItems = debtAnalysis.DebtItems.Count(d => d.Priority >= 8),
EstimatedTotalEffort = debtAnalysis.DebtItems.Sum(d => d.EstimatedEffort),
DebtCategory = GetOverallDebtCategory(debtScore),
RecommendedActions = GetTopRecommendations(debtAnalysis),
ImprovementTimeline = generateImprovementPlan ? $"{improvementPlan.Sum(p => p.EstimatedHours)} hours over {improvementPlan.Count} phases" : null
}
};
return new AIPluginResult(result,
$"Technical debt analysis completed. Overall debt score: {debtScore}/100. " +
$"Found {debtAnalysis.DebtItems.Count} debt items requiring {debtAnalysis.DebtItems.Sum(d => d.EstimatedEffort)} hours of effort.");
}
catch (Exception ex)
{
return new AIPluginResult(ex, "Failed to analyze technical debt");
}
}
private async Task AnalyzeComplexityDebt(List<string> sourceFiles, TechnicalDebtAnalysis analysis)
{
var totalComplexityPoints = 0;
var methodCount = 0;
var highComplexityMethods = 0;
foreach (var filePath in sourceFiles)
{
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
var complexity = CalculateCyclomaticComplexity(method);
totalComplexityPoints += complexity;
methodCount++;
if (complexity > 10)
{
highComplexityMethods++;
var className = GetContainingClassName(method);
var methodName = method.Identifier.ValueText;
var lineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
analysis.DebtItems.Add(new DebtItem
{
Type = "Complexity",
Category = "Code Quality",
Description = $"High complexity method ({complexity} cyclomatic complexity)",
Location = $"{Path.GetFileName(filePath)}:{lineNumber} - {className}.{methodName}",
Priority = Math.Min(10, complexity / 2), // Scale 1-10
EstimatedEffort = Math.Max(2, complexity / 3), // Hours to refactor
Impact = complexity > 20 ? "High" : complexity > 15 ? "Medium" : "Low",
RecommendedAction = "Extract methods, reduce branching, simplify logic"
});
}
}
}
analysis.ComplexityDebt.TotalComplexityPoints = totalComplexityPoints;
analysis.ComplexityDebt.AverageMethodComplexity = methodCount > 0 ? (double)totalComplexityPoints / methodCount : 0;
analysis.ComplexityDebt.HighComplexityMethods = highComplexityMethods;
analysis.ComplexityDebt.EstimatedRefactoringHours = highComplexityMethods * 4; // Average 4 hours per complex method
}
private async Task AnalyzeDocumentationDebtMethod(List<string> sourceFiles, TechnicalDebtAnalysis analysis)
{
var totalMethods = 0;
var documentedMethods = 0;
foreach (var filePath in sourceFiles)
{
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword) || mod.IsKind(SyntaxKind.ProtectedKeyword)));
foreach (var method in methods)
{
totalMethods++;
var hasDocumentation = HasXmlDocumentation(method);
if (hasDocumentation)
{
documentedMethods++;
}
else
{
var className = GetContainingClassName(method);
var methodName = method.Identifier.ValueText;
var lineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
var priority = IsPublicApi(method) ? 8 : 5; // Higher priority for public APIs
analysis.DebtItems.Add(new DebtItem
{
Type = "Documentation",
Category = "Maintainability",
Description = "Public method lacks XML documentation",
Location = $"{Path.GetFileName(filePath)}:{lineNumber} - {className}.{methodName}",
Priority = priority,
EstimatedEffort = 0.5, // 30 minutes per method
Impact = IsPublicApi(method) ? "Medium" : "Low",
RecommendedAction = "Add comprehensive XML documentation with examples"
});
}
}
// Check for class-level documentation
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword)));
foreach (var cls in classes)
{
if (!HasXmlDocumentation(cls))
{
var className = cls.Identifier.ValueText;
var lineNumber = cls.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
analysis.DebtItems.Add(new DebtItem
{
Type = "Documentation",
Category = "Maintainability",
Description = "Public class lacks XML documentation",
Location = $"{Path.GetFileName(filePath)}:{lineNumber} - {className}",
Priority = 7,
EstimatedEffort = 1, // 1 hour per class
Impact = "Medium",
RecommendedAction = "Add class-level documentation explaining purpose and usage"
});
}
}
}
analysis.DocumentationDebt.TotalMethods = totalMethods;
analysis.DocumentationDebt.UndocumentedMethods = totalMethods - documentedMethods;
analysis.DocumentationDebt.DocumentationCoverage = totalMethods > 0 ? (double)documentedMethods / totalMethods * 100 : 100;
analysis.DocumentationDebt.EstimatedDocumentationHours = (totalMethods - documentedMethods) * 0.5;
}
private async Task AnalyzeDependencyDebt(List<string> projectFiles, TechnicalDebtAnalysis analysis)
{
var totalDependencies = 0;
var outdatedDependencies = 0;
var vulnerableDependencies = 0;
var majorVersionsBehind = 0;
foreach (var projectFile in projectFiles)
{
if (projectFile.EndsWith(".csproj"))
{
var projectContent = await File.ReadAllTextAsync(projectFile);
var dependencies = ExtractPackageReferences(projectContent);
foreach (var dependency in dependencies)
{
totalDependencies++;
// Simulate dependency analysis (in real implementation, you'd query NuGet API)
var isOutdated = SimulateOutdatedCheck(dependency);
var isVulnerable = SimulateVulnerabilityCheck(dependency);
var versionsBehind = SimulateMajorVersionCheck(dependency);
if (isOutdated)
{
outdatedDependencies++;
analysis.DebtItems.Add(new DebtItem
{
Type = "Dependency",
Category = "Security & Maintenance",
Description = $"Outdated package: {dependency.Name} v{dependency.Version}",
Location = Path.GetFileName(projectFile),
Priority = isVulnerable ? 9 : 6,
EstimatedEffort = versionsBehind > 1 ? 4 : 1, // More effort for major version jumps
Impact = isVulnerable ? "High" : versionsBehind > 1 ? "Medium" : "Low",
RecommendedAction = $"Update to latest version and test compatibility"
});
}
if (isVulnerable)
{
vulnerableDependencies++;
}
if (versionsBehind > 1)
{
majorVersionsBehind++;
}
}
}
}
analysis.DependencyDebt.TotalDependencies = totalDependencies;
analysis.DependencyDebt.OutdatedDependencies = outdatedDependencies;
analysis.DependencyDebt.VulnerableDependencies = vulnerableDependencies;
analysis.DependencyDebt.MajorVersionsBehind = majorVersionsBehind;
analysis.DependencyDebt.EstimatedUpgradeHours = outdatedDependencies * 2; // Average 2 hours per upgrade
}
private async Task AnalyzeTestDebtMethod(List<string> sourceFiles, TechnicalDebtAnalysis analysis)
{
var productionFiles = sourceFiles.Where(f => !IsTestFile(f)).ToList();
var testFiles = sourceFiles.Where(f => IsTestFile(f)).ToList();
var totalMethods = 0;
var testedMethods = 0;
// Get all public methods from production code
var publicMethods = new List<MethodDebtInfo>();
foreach (var filePath in productionFiles)
{
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>()
.Where(m => m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword)));
foreach (var method in methods)
{
totalMethods++;
var className = GetContainingClassName(method);
var methodName = method.Identifier.ValueText;
publicMethods.Add(new MethodDebtInfo
{
ClassName = className,
MethodName = methodName,
FilePath = filePath,
LineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1
});
}
}
// Simple heuristic to estimate test coverage
var testMethodNames = new HashSet<string>();
foreach (var testFile in testFiles)
{
var testCode = await File.ReadAllTextAsync(testFile);
var testTree = CSharpSyntaxTree.ParseText(testCode);
var testRoot = await testTree.GetRootAsync();
var testMethods = testRoot.DescendantNodes().OfType<MethodDeclarationSyntax>()
.Where(m => HasTestAttribute(m));
foreach (var testMethod in testMethods)
{
testMethodNames.Add(testMethod.Identifier.ValueText.ToLowerInvariant());
}
}
// Estimate which methods are tested (simple name matching heuristic)
foreach (var method in publicMethods)
{
var hasTest = testMethodNames.Any(t =>
t.Contains(method.MethodName.ToLowerInvariant()) ||
t.Contains(method.ClassName.ToLowerInvariant()));
if (hasTest)
{
testedMethods++;
}
else
{
var priority = IsBusinessLogic(method.MethodName) ? 8 : 5;
analysis.DebtItems.Add(new DebtItem
{
Type = "Test",
Category = "Quality Assurance",
Description = "Public method lacks unit tests",
Location = $"{Path.GetFileName(method.FilePath)}:{method.LineNumber} - {method.ClassName}.{method.MethodName}",
Priority = priority,
EstimatedEffort = 2, // 2 hours per test
Impact = IsBusinessLogic(method.MethodName) ? "High" : "Medium",
RecommendedAction = "Write comprehensive unit tests with edge cases"
});
}
}
analysis.TestDebt.TotalMethods = totalMethods;
analysis.TestDebt.UntestedMethods = totalMethods - testedMethods;
analysis.TestDebt.TestCoverage = totalMethods > 0 ? (double)testedMethods / totalMethods * 100 : 100;
analysis.TestDebt.EstimatedTestingHours = (totalMethods - testedMethods) * 2;
}
private int CalculateOverallDebtScore(TechnicalDebtAnalysis analysis)
{
// Calculate weighted debt score (0-100, higher is better)
var score = 100;
// Complexity debt impact (weight: 30%)
var complexityPenalty = Math.Min(30, analysis.ComplexityDebt.HighComplexityMethods * 3);
score -= complexityPenalty;
// Documentation debt impact (weight: 20%)
var docCoveragePenalty = Math.Min(20, (int)((100 - analysis.DocumentationDebt.DocumentationCoverage) / 5));
score -= docCoveragePenalty;
// Dependency debt impact (weight: 25%)
var depPenalty = Math.Min(25, analysis.DependencyDebt.OutdatedDependencies * 2);
score -= depPenalty;
// Test debt impact (weight: 25%)
var testCoveragePenalty = Math.Min(25, (int)((100 - analysis.TestDebt.TestCoverage) / 4));
score -= testCoveragePenalty;
return Math.Max(0, score);
}
private List<ImprovementAction> GenerateImprovementPlanMethod(TechnicalDebtAnalysis analysis)
{
var plan = new List<ImprovementAction>();
// Phase 1: Critical Issues (High priority, high impact)
var criticalItems = analysis.DebtItems.Where(d => d.Priority >= 8).ToList();
if (criticalItems.Any())
{
plan.Add(new ImprovementAction
{
Phase = 1,
Priority = "Critical",
Title = "Address Critical Technical Debt",
Description = $"Fix {criticalItems.Count} high-priority issues including security vulnerabilities and complex code",
EstimatedHours = criticalItems.Sum(i => i.EstimatedEffort),
ExpectedBenefit = "Immediate risk reduction and improved maintainability",
Dependencies = new List<string>()
});
}
// Phase 2: Complexity Reduction
if (analysis.ComplexityDebt.HighComplexityMethods > 0)
{
plan.Add(new ImprovementAction
{
Phase = 2,
Priority = "High",
Title = "Refactor Complex Methods",
Description = $"Simplify {analysis.ComplexityDebt.HighComplexityMethods} high-complexity methods",
EstimatedHours = analysis.ComplexityDebt.EstimatedRefactoringHours,
ExpectedBenefit = "Improved code readability and reduced bug risk",
Dependencies = new List<string> { "Ensure comprehensive test coverage before refactoring" }
});
}
// Phase 3: Test Coverage
if (analysis.TestDebt.TestCoverage < 80)
{
plan.Add(new ImprovementAction
{
Phase = 3,
Priority = "High",
Title = "Improve Test Coverage",
Description = $"Add tests for {analysis.TestDebt.UntestedMethods} untested methods",
EstimatedHours = analysis.TestDebt.EstimatedTestingHours,
ExpectedBenefit = "Increased confidence in deployments and easier refactoring",
Dependencies = new List<string>()
});
}
// Phase 4: Dependency Updates
if (analysis.DependencyDebt.OutdatedDependencies > 0)
{
plan.Add(new ImprovementAction
{
Phase = 4,
Priority = "Medium",
Title = "Update Dependencies",
Description = $"Update {analysis.DependencyDebt.OutdatedDependencies} outdated packages",
EstimatedHours = analysis.DependencyDebt.EstimatedUpgradeHours,
ExpectedBenefit = "Security improvements and access to latest features",
Dependencies = new List<string> { "Ensure test coverage before upgrades" }
});
}
// Phase 5: Documentation
if (analysis.DocumentationDebt.DocumentationCoverage < 90)
{
plan.Add(new ImprovementAction
{
Phase = 5,
Priority = "Medium",
Title = "Improve Documentation",
Description = $"Document {analysis.DocumentationDebt.UndocumentedMethods} public methods and classes",
EstimatedHours = analysis.DocumentationDebt.EstimatedDocumentationHours,
ExpectedBenefit = "Better developer experience and easier onboarding",
Dependencies = new List<string>()
});
}
return plan;
}
private async Task<object> TrackDebtTrends(string projectPath, TechnicalDebtAnalysis currentAnalysis)
{
var trendsFile = Path.Combine(projectPath, ".technical-debt-trends.json");
var trends = new List<TechnicalDebtSnapshot>();
// Load existing trends if available
if (File.Exists(trendsFile))
{
try
{
var existingData = await File.ReadAllTextAsync(trendsFile);
trends = JsonSerializer.Deserialize<List<TechnicalDebtSnapshot>>(existingData) ?? new List<TechnicalDebtSnapshot>();
}
catch
{
// Ignore errors loading existing trends
}
}
// Add current snapshot
var snapshot = new TechnicalDebtSnapshot
{
Date = currentAnalysis.AnalysisDate,
DebtScore = CalculateOverallDebtScore(currentAnalysis),
ComplexityDebt = currentAnalysis.ComplexityDebt.TotalComplexityPoints,
DocumentationCoverage = currentAnalysis.DocumentationDebt.DocumentationCoverage,
TestCoverage = currentAnalysis.TestDebt.TestCoverage,
OutdatedDependencies = currentAnalysis.DependencyDebt.OutdatedDependencies,
TotalDebtItems = currentAnalysis.DebtItems.Count
};
trends.Add(snapshot);
// Keep only last 30 snapshots
if (trends.Count > 30)
{
trends = trends.OrderByDescending(t => t.Date).Take(30).ToList();
}
// Save trends
try
{
var trendsJson = JsonSerializer.Serialize(trends, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(trendsFile, trendsJson);
}
catch
{
// Ignore save errors
}
// Calculate trend analysis
if (trends.Count >= 2)
{
var previous = trends.OrderByDescending(t => t.Date).Skip(1).First();
var current = snapshot;
return new
{
TrendDirection = current.DebtScore > previous.DebtScore ? "Improving" :
current.DebtScore < previous.DebtScore ? "Deteriorating" : "Stable",
ScoreChange = current.DebtScore - previous.DebtScore,
ComplexityTrend = current.ComplexityDebt - previous.ComplexityDebt,
DocumentationTrend = current.DocumentationCoverage - previous.DocumentationCoverage,
TestCoverageTrend = current.TestCoverage - previous.TestCoverage,
DependencyTrend = current.OutdatedDependencies - previous.OutdatedDependencies,
HistoricalData = trends.OrderByDescending(t => t.Date).Take(10).ToList()
};
}
return new { Message = "Insufficient historical data for trend analysis" };
}
// Helper methods
private List<string> GetSourceFiles(string path)
{
var files = new List<string>();
if (File.Exists(path) && path.EndsWith(".cs"))
{
files.Add(path);
}
else if (Directory.Exists(path))
{
files.AddRange(Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories)
.Where(f => !f.Contains("\\bin\\") && !f.Contains("\\obj\\") &&
!f.EndsWith(".Designer.cs") && !f.EndsWith(".g.cs")));
}
return files;
}
private List<string> GetProjectFiles(string path)
{
var files = new List<string>();
if (Directory.Exists(path))
{
files.AddRange(Directory.GetFiles(path, "*.csproj", SearchOption.AllDirectories));
files.AddRange(Directory.GetFiles(path, "*.vbproj", SearchOption.AllDirectories));
files.AddRange(Directory.GetFiles(path, "packages.config", SearchOption.AllDirectories));
}
return files;
}
private int CalculateCyclomaticComplexity(SyntaxNode node)
{
int complexity = 1; // Base complexity
var descendants = node.DescendantNodes();
// Decision points that increase complexity
complexity += descendants.OfType<IfStatementSyntax>().Count();
complexity += descendants.OfType<WhileStatementSyntax>().Count();
complexity += descendants.OfType<ForStatementSyntax>().Count();
complexity += descendants.OfType<ForEachStatementSyntax>().Count();
complexity += descendants.OfType<DoStatementSyntax>().Count();
complexity += descendants.OfType<SwitchStatementSyntax>().Count();
complexity += descendants.OfType<ConditionalExpressionSyntax>().Count();
complexity += descendants.OfType<CatchClauseSyntax>().Count();
// Logical operators (&& and ||)
var binaryExpressions = descendants.OfType<BinaryExpressionSyntax>();
foreach (var expr in binaryExpressions)
{
if (expr.OperatorToken.IsKind(SyntaxKind.AmpersandAmpersandToken) ||
expr.OperatorToken.IsKind(SyntaxKind.BarBarToken))
{
complexity++;
}
}
return complexity;
}
private string GetContainingClassName(SyntaxNode node)
{
var classDeclaration = node.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (classDeclaration != null)
{
return classDeclaration.Identifier.ValueText;
}
var structDeclaration = node.Ancestors().OfType<StructDeclarationSyntax>().FirstOrDefault();
if (structDeclaration != null)
{
return structDeclaration.Identifier.ValueText;
}
return "Unknown";
}
private bool HasXmlDocumentation(SyntaxNode node)
{
var documentationComment = node.GetLeadingTrivia()
.FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia));
return !documentationComment.IsKind(SyntaxKind.None);
}
private bool IsPublicApi(MethodDeclarationSyntax method)
{
return method.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
}
private List<PackageReference> ExtractPackageReferences(string projectContent)
{
var packages = new List<PackageReference>();
// Simple regex to extract PackageReference elements
var packagePattern = @"<PackageReference\s+Include=""([^""]+)""\s+Version=""([^""]+)""";
var matches = Regex.Matches(projectContent, packagePattern, RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
packages.Add(new PackageReference
{
Name = match.Groups[1].Value,
Version = match.Groups[2].Value
});
}
return packages;
}
private bool SimulateOutdatedCheck(PackageReference package)
{
// Simulate outdated package detection
// In real implementation, you'd query NuGet API
var random = new Random(package.Name.GetHashCode());
return random.NextDouble() < 0.3; // 30% chance of being outdated
}
private bool SimulateVulnerabilityCheck(PackageReference package)
{
// Simulate vulnerability detection
// In real implementation, you'd query security databases
var vulnerablePackages = new[] { "Newtonsoft.Json", "System.Text.Json", "Microsoft.AspNetCore" };
return vulnerablePackages.Any(vp => package.Name.Contains(vp)) && SimulateOutdatedCheck(package);
}
private int SimulateMajorVersionCheck(PackageReference package)
{
// Simulate major version difference calculation
var random = new Random(package.Name.GetHashCode() + 1);
return random.Next(0, 4); // 0-3 major versions behind
}
private bool IsTestFile(string filePath)
{
var fileName = Path.GetFileName(filePath).ToLowerInvariant();
var directory = Path.GetDirectoryName(filePath)?.ToLowerInvariant() ?? string.Empty;
return fileName.Contains("test") || fileName.Contains("spec") ||
directory.Contains("test") || directory.Contains("spec") ||
fileName.EndsWith("tests.cs") || fileName.EndsWith("test.cs");
}
private bool HasTestAttribute(MethodDeclarationSyntax method)
{
var attributes = method.AttributeLists.SelectMany(al => al.Attributes);
var testAttributes = new[] { "Test", "TestMethod", "Fact", "Theory" };
return attributes.Any(attr =>
testAttributes.Any(ta => attr.Name.ToString().Contains(ta)));
}
private bool IsBusinessLogic(string methodName)
{
var businessKeywords = new[] { "Calculate", "Process", "Validate", "Execute", "Handle", "Manage" };
return businessKeywords.Any(keyword => methodName.Contains(keyword));
}
private string GetDebtLevel(int value, string category)
{
return category switch
{
"Complexity" => value > 50 ? "Critical" : value > 20 ? "High" : value > 10 ? "Medium" : "Low",
"Documentation" => value > 100 ? "Critical" : value > 50 ? "High" : value > 20 ? "Medium" : "Low",
"Dependency" => value > 20 ? "Critical" : value > 10 ? "High" : value > 5 ? "Medium" : "Low",
"Test" => value > 100 ? "Critical" : value > 50 ? "High" : value > 20 ? "Medium" : "Low",
_ => "Unknown"
};
}
private string GetOverallDebtCategory(int debtScore)
{
return debtScore switch
{
>= 80 => "Excellent - Low technical debt",
>= 60 => "Good - Manageable technical debt",
>= 40 => "Fair - Moderate technical debt requiring attention",
>= 20 => "Poor - High technical debt needs immediate action",
_ => "Critical - Severe technical debt blocking progress"
};
}
private List<string> GetTopRecommendations(TechnicalDebtAnalysis analysis)
{
var recommendations = new List<string>();
// Get top 5 recommendations based on priority and impact
var topItems = analysis.DebtItems
.OrderByDescending(d => d.Priority)
.ThenByDescending(d => d.Impact == "High" ? 3 : d.Impact == "Medium" ? 2 : 1)
.Take(5);
foreach (var item in topItems)
{
recommendations.Add($"{item.Type}: {item.RecommendedAction}");
}
if (!recommendations.Any())
{
recommendations.Add("Continue maintaining current code quality standards");
}
return recommendations;
}
private bool GetBoolParameter(IReadOnlyDictionary<string, object> parameters, string key, bool defaultValue)
{
return parameters.TryGetValue(key, out var value) ? Convert.ToBoolean(value) : defaultValue;
}
}
// Supporting data structures
public class TechnicalDebtAnalysis
{
public string ProjectPath { get; set; } = string.Empty;
public DateTime AnalysisDate { get; set; }
public ComplexityDebtMetrics ComplexityDebt { get; set; } = new();
public DocumentationDebtMetrics DocumentationDebt { get; set; } = new();
public DependencyDebtMetrics DependencyDebt { get; set; } = new();
public TestDebtMetrics TestDebt { get; set; } = new();
public List<DebtItem> DebtItems { get; set; } = new();
}
public class ComplexityDebtMetrics
{
public int TotalComplexityPoints { get; set; }
public double AverageMethodComplexity { get; set; }
public int HighComplexityMethods { get; set; }
public double EstimatedRefactoringHours { get; set; }
}
public class DocumentationDebtMetrics
{
public int TotalMethods { get; set; }
public int UndocumentedMethods { get; set; }
public double DocumentationCoverage { get; set; }
public double EstimatedDocumentationHours { get; set; }
}
public class DependencyDebtMetrics
{
public int TotalDependencies { get; set; }
public int OutdatedDependencies { get; set; }
public int VulnerableDependencies { get; set; }
public int MajorVersionsBehind { get; set; }
public double EstimatedUpgradeHours { get; set; }
}
public class TestDebtMetrics
{
public int TotalMethods { get; set; }
public int UntestedMethods { get; set; }
public double TestCoverage { get; set; }
public double EstimatedTestingHours { get; set; }
}
public class DebtItem
{
public string Type { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public int Priority { get; set; } // 1-10 scale
public double EstimatedEffort { get; set; } // Hours
public string Impact { get; set; } = string.Empty; // Low, Medium, High
public string RecommendedAction { get; set; } = string.Empty;
}
public class ImprovementAction
{
public int Phase { get; set; }
public string Priority { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public double EstimatedHours { get; set; }
public string ExpectedBenefit { get; set; } = string.Empty;
public List<string> Dependencies { get; set; } = new();
}
public class PackageReference
{
public string Name { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
}
public class MethodDebtInfo
{
public string ClassName { get; set; } = string.Empty;
public string MethodName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public int LineNumber { get; set; }
}
public class TechnicalDebtSnapshot
{
public DateTime Date { get; set; }
public int DebtScore { get; set; }
public int ComplexityDebt { get; set; }
public double DocumentationCoverage { get; set; }
public double TestCoverage { get; set; }
public int OutdatedDependencies { get; set; }
public int TotalDebtItems { get; set; }
}
}