971 lines
33 KiB
C#
Executable File
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; }
|
|
}
|
|
} |