MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Analysis/ComplexityAnalyzerPlugin.cs

660 lines
22 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.Threading.Tasks;
namespace MarketAlly.AIPlugin.Analysis.Plugins
{
[AIPlugin("ComplexityAnalyzer", "Measures cyclomatic and cognitive complexity with refactoring suggestions")]
public class ComplexityAnalyzerPlugin : IAIPlugin
{
[AIParameter("Full path to the file or directory to analyze", required: true)]
public string Path { get; set; } = string.Empty;
[AIParameter("Calculate cyclomatic complexity", required: false)]
public bool CalculateCyclomatic { get; set; } = true;
[AIParameter("Calculate cognitive complexity", required: false)]
public bool CalculateCognitive { get; set; } = true;
[AIParameter("Maximum acceptable cyclomatic complexity", required: false)]
public int MaxCyclomaticComplexity { get; set; } = 10;
[AIParameter("Maximum acceptable cognitive complexity", required: false)]
public int MaxCognitiveComplexity { get; set; } = 15;
[AIParameter("Generate complexity reduction suggestions", required: false)]
public bool GenerateSuggestions { get; set; } = true;
[AIParameter("Include method-level complexity breakdown", required: false)]
public bool IncludeMethodBreakdown { get; set; } = true;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["path"] = typeof(string),
["calculateCyclomatic"] = typeof(bool),
["calculateCognitive"] = typeof(bool),
["maxCyclomaticComplexity"] = typeof(int),
["maxCognitiveComplexity"] = typeof(int),
["generateSuggestions"] = typeof(bool),
["includeMethodBreakdown"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
try
{
// Extract parameters
string path = parameters["path"]?.ToString() ?? string.Empty;
bool calculateCyclomatic = GetBoolParameter(parameters, "calculateCyclomatic", true);
bool calculateCognitive = GetBoolParameter(parameters, "calculateCognitive", true);
int maxCyclomatic = GetIntParameter(parameters, "maxCyclomaticComplexity", 10);
int maxCognitive = GetIntParameter(parameters, "maxCognitiveComplexity", 15);
bool generateSuggestions = GetBoolParameter(parameters, "generateSuggestions", true);
bool includeMethodBreakdown = GetBoolParameter(parameters, "includeMethodBreakdown", true);
// Validate path
if (!File.Exists(path) && !Directory.Exists(path))
{
return new AIPluginResult(
new FileNotFoundException($"Path not found: {path}"),
"Path not found"
);
}
// Get files to analyze
var filesToAnalyze = GetFilesToAnalyze(path);
if (!filesToAnalyze.Any())
{
return new AIPluginResult(
new InvalidOperationException("No C# files found to analyze"),
"No files found"
);
}
// Analyze complexity for each file
var fileResults = new List<FileComplexityResult>();
var overallMetrics = new ComplexityMetrics();
var highComplexityMethods = new List<MethodComplexityInfo>();
var violations = new List<ComplexityViolation>();
foreach (string filePath in filesToAnalyze)
{
var fileResult = await AnalyzeFileComplexity(
filePath, calculateCyclomatic, calculateCognitive,
maxCyclomatic, maxCognitive, includeMethodBreakdown);
fileResults.Add(fileResult);
overallMetrics.Add(fileResult.Metrics);
highComplexityMethods.AddRange(fileResult.HighComplexityMethods);
violations.AddRange(fileResult.Violations);
}
// Generate suggestions if requested
var suggestions = new List<string>();
if (generateSuggestions)
{
suggestions = GenerateComplexityReductionSuggestions(highComplexityMethods, violations);
}
// Calculate overall complexity score (0-100, higher is better)
int overallScore = CalculateOverallComplexityScore(overallMetrics, maxCyclomatic, maxCognitive);
var result = new
{
Path = path,
FilesAnalyzed = filesToAnalyze.Count,
CyclomaticComplexity = calculateCyclomatic ? new
{
Average = overallMetrics.AverageCyclomaticComplexity,
Maximum = overallMetrics.MaxCyclomaticComplexity,
Total = overallMetrics.TotalCyclomaticComplexity,
MethodsAboveThreshold = overallMetrics.CyclomaticViolations
} : null,
CognitiveComplexity = calculateCognitive ? new
{
Average = overallMetrics.AverageCognitiveComplexity,
Maximum = overallMetrics.MaxCognitiveComplexity,
Total = overallMetrics.TotalCognitiveComplexity,
MethodsAboveThreshold = overallMetrics.CognitiveViolations
} : null,
HighComplexityMethods = highComplexityMethods.Take(10).Select(m => new
{
m.MethodName,
m.ClassName,
m.FilePath,
m.LineNumber,
m.CyclomaticComplexity,
m.CognitiveComplexity,
m.ParameterCount,
m.LinesOfCode
}).ToList(),
ComplexityViolations = violations.Select(v => new
{
v.MethodName,
v.ClassName,
v.FilePath,
v.LineNumber,
v.ViolationType,
v.ActualValue,
v.ThresholdValue,
v.Severity
}).ToList(),
ReductionSuggestions = suggestions,
MethodBreakdown = includeMethodBreakdown ? fileResults.SelectMany(f => f.MethodDetails).ToList() : null,
OverallComplexityScore = overallScore,
Summary = new
{
TotalMethods = overallMetrics.TotalMethods,
HighComplexityMethods = highComplexityMethods.Count,
TotalViolations = violations.Count,
AverageMethodComplexity = Math.Round(overallMetrics.AverageCyclomaticComplexity, 2),
RecommendedActions = violations.Count > 0 ? "Refactor high-complexity methods" : "Complexity within acceptable limits"
}
};
return new AIPluginResult(result, $"Complexity analysis completed for {filesToAnalyze.Count} files");
}
catch (Exception ex)
{
return new AIPluginResult(ex, "Failed to analyze complexity");
}
}
private async Task<FileComplexityResult> AnalyzeFileComplexity(
string filePath, bool calculateCyclomatic, bool calculateCognitive,
int maxCyclomatic, int maxCognitive, bool includeMethodBreakdown)
{
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: filePath);
var root = await syntaxTree.GetRootAsync();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>().ToList();
var constructors = root.DescendantNodes().OfType<ConstructorDeclarationSyntax>().ToList();
var properties = root.DescendantNodes().OfType<PropertyDeclarationSyntax>()
.Where(p => p.AccessorList?.Accessors.Any(a => a.Body != null || a.ExpressionBody != null) == true)
.ToList();
var result = new FileComplexityResult
{
FilePath = filePath,
FileName = System.IO.Path.GetFileName(filePath),
Metrics = new ComplexityMetrics(),
HighComplexityMethods = new List<MethodComplexityInfo>(),
Violations = new List<ComplexityViolation>(),
MethodDetails = new List<object>()
};
// Analyze methods
foreach (var method in methods)
{
var methodInfo = AnalyzeMethod(method, filePath, calculateCyclomatic, calculateCognitive);
result.Metrics.Add(methodInfo);
if (methodInfo.CyclomaticComplexity > maxCyclomatic || methodInfo.CognitiveComplexity > maxCognitive)
{
result.HighComplexityMethods.Add(methodInfo);
}
// Check for violations
if (calculateCyclomatic && methodInfo.CyclomaticComplexity > maxCyclomatic)
{
result.Violations.Add(new ComplexityViolation
{
MethodName = methodInfo.MethodName,
ClassName = methodInfo.ClassName,
FilePath = filePath,
LineNumber = methodInfo.LineNumber,
ViolationType = "CyclomaticComplexity",
ActualValue = methodInfo.CyclomaticComplexity,
ThresholdValue = maxCyclomatic,
Severity = methodInfo.CyclomaticComplexity > maxCyclomatic * 1.5 ? "High" : "Medium"
});
}
if (calculateCognitive && methodInfo.CognitiveComplexity > maxCognitive)
{
result.Violations.Add(new ComplexityViolation
{
MethodName = methodInfo.MethodName,
ClassName = methodInfo.ClassName,
FilePath = filePath,
LineNumber = methodInfo.LineNumber,
ViolationType = "CognitiveComplexity",
ActualValue = methodInfo.CognitiveComplexity,
ThresholdValue = maxCognitive,
Severity = methodInfo.CognitiveComplexity > maxCognitive * 1.5 ? "High" : "Medium"
});
}
if (includeMethodBreakdown)
{
result.MethodDetails.Add(new
{
methodInfo.MethodName,
methodInfo.ClassName,
methodInfo.CyclomaticComplexity,
methodInfo.CognitiveComplexity,
methodInfo.ParameterCount,
methodInfo.LinesOfCode,
methodInfo.LineNumber
});
}
}
// Analyze constructors
foreach (var constructor in constructors)
{
var methodInfo = AnalyzeConstructor(constructor, filePath, calculateCyclomatic, calculateCognitive);
result.Metrics.Add(methodInfo);
if (methodInfo.CyclomaticComplexity > maxCyclomatic || methodInfo.CognitiveComplexity > maxCognitive)
{
result.HighComplexityMethods.Add(methodInfo);
}
}
return result;
}
private MethodComplexityInfo AnalyzeMethod(MethodDeclarationSyntax method, string filePath,
bool calculateCyclomatic, bool calculateCognitive)
{
var className = GetContainingClassName(method);
var lineNumber = method.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
var info = new MethodComplexityInfo
{
MethodName = method.Identifier.ValueText,
ClassName = className,
FilePath = filePath,
LineNumber = lineNumber,
ParameterCount = method.ParameterList.Parameters.Count,
LinesOfCode = CalculateLinesOfCode(method)
};
if (calculateCyclomatic)
{
info.CyclomaticComplexity = CalculateCyclomaticComplexity(method);
}
if (calculateCognitive)
{
info.CognitiveComplexity = CalculateCognitiveComplexity(method);
}
return info;
}
private MethodComplexityInfo AnalyzeConstructor(ConstructorDeclarationSyntax constructor, string filePath,
bool calculateCyclomatic, bool calculateCognitive)
{
var className = GetContainingClassName(constructor);
var lineNumber = constructor.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
var info = new MethodComplexityInfo
{
MethodName = $"{className} (constructor)",
ClassName = className,
FilePath = filePath,
LineNumber = lineNumber,
ParameterCount = constructor.ParameterList.Parameters.Count,
LinesOfCode = CalculateLinesOfCode(constructor)
};
if (calculateCyclomatic)
{
info.CyclomaticComplexity = CalculateCyclomaticComplexity(constructor);
}
if (calculateCognitive)
{
info.CognitiveComplexity = CalculateCognitiveComplexity(constructor);
}
return info;
}
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<SwitchExpressionSyntax>().Count();
complexity += descendants.OfType<ConditionalExpressionSyntax>().Count(); // Ternary operator
complexity += descendants.OfType<CatchClauseSyntax>().Count();
// Case statements in switch
foreach (var switchStmt in descendants.OfType<SwitchStatementSyntax>())
{
complexity += switchStmt.Sections.Count - 1; // Subtract 1 because switch already counted
}
// Switch expression arms
foreach (var switchExpr in descendants.OfType<SwitchExpressionSyntax>())
{
complexity += switchExpr.Arms.Count - 1; // Subtract 1 because switch already counted
}
// 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 int CalculateCognitiveComplexity(SyntaxNode node)
{
var calculator = new CognitiveComplexityCalculator();
return calculator.Calculate(node);
}
private int CalculateLinesOfCode(SyntaxNode node)
{
var span = node.GetLocation().GetLineSpan();
return span.EndLinePosition.Line - span.StartLinePosition.Line + 1;
}
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 List<string> GetFilesToAnalyze(string path)
{
var files = new List<string>();
if (File.Exists(path))
{
if (path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
{
files.Add(path);
}
}
else if (Directory.Exists(path))
{
files.AddRange(Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories));
}
return files;
}
private List<string> GenerateComplexityReductionSuggestions(
List<MethodComplexityInfo> highComplexityMethods,
List<ComplexityViolation> violations)
{
var suggestions = new List<string>();
if (!highComplexityMethods.Any())
{
suggestions.Add("✅ All methods have acceptable complexity levels.");
return suggestions;
}
// General suggestions
suggestions.Add("🔧 Consider the following complexity reduction strategies:");
// Method extraction suggestions
var methodsWithHighCyclomatic = highComplexityMethods.Where(m => m.CyclomaticComplexity > 15).ToList();
if (methodsWithHighCyclomatic.Any())
{
suggestions.Add($"📝 Extract smaller methods from {methodsWithHighCyclomatic.Count} method(s) with high cyclomatic complexity (>15)");
suggestions.Add(" • Break down large conditional blocks into separate methods");
suggestions.Add(" • Extract loop bodies into dedicated methods");
suggestions.Add(" • Use early returns to reduce nesting levels");
}
// Parameter count suggestions
var methodsWithManyParams = highComplexityMethods.Where(m => m.ParameterCount > 5).ToList();
if (methodsWithManyParams.Any())
{
suggestions.Add($"📦 Reduce parameter count for {methodsWithManyParams.Count} method(s) with >5 parameters");
suggestions.Add(" • Consider using parameter objects or DTOs");
suggestions.Add(" • Use builder pattern for complex object creation");
}
// Large method suggestions
var largeMethods = highComplexityMethods.Where(m => m.LinesOfCode > 50).ToList();
if (largeMethods.Any())
{
suggestions.Add($"📏 Break down {largeMethods.Count} large method(s) (>50 lines)");
suggestions.Add(" • Apply Single Responsibility Principle");
suggestions.Add(" • Extract helper methods for specific tasks");
}
// Specific method suggestions
var topComplexMethods = highComplexityMethods
.OrderByDescending(m => m.CyclomaticComplexity + m.CognitiveComplexity)
.Take(3);
suggestions.Add("🎯 Priority refactoring targets:");
foreach (var method in topComplexMethods)
{
suggestions.Add($" • {method.ClassName}.{method.MethodName} " +
$"(Cyclomatic: {method.CyclomaticComplexity}, Cognitive: {method.CognitiveComplexity})");
}
return suggestions;
}
private int CalculateOverallComplexityScore(ComplexityMetrics metrics, int maxCyclomatic, int maxCognitive)
{
if (metrics.TotalMethods == 0) return 100;
// Calculate percentage of methods within acceptable limits
var cyclomaticScore = metrics.TotalMethods > 0
? (double)(metrics.TotalMethods - metrics.CyclomaticViolations) / metrics.TotalMethods * 100
: 100;
var cognitiveScore = metrics.TotalMethods > 0
? (double)(metrics.TotalMethods - metrics.CognitiveViolations) / metrics.TotalMethods * 100
: 100;
// Weighted average (cognitive complexity is slightly more important)
var overallScore = (cyclomaticScore * 0.4 + cognitiveScore * 0.6);
// Penalty for extremely high complexity methods
var avgComplexity = metrics.AverageCyclomaticComplexity;
if (avgComplexity > maxCyclomatic * 2)
{
overallScore *= 0.7; // 30% penalty
}
else if (avgComplexity > maxCyclomatic * 1.5)
{
overallScore *= 0.85; // 15% penalty
}
return Math.Max(0, Math.Min(100, (int)Math.Round(overallScore)));
}
private bool GetBoolParameter(IReadOnlyDictionary<string, object> parameters, string key, bool defaultValue)
{
return parameters.TryGetValue(key, out var value) ? Convert.ToBoolean(value) : defaultValue;
}
private int GetIntParameter(IReadOnlyDictionary<string, object> parameters, string key, int defaultValue)
{
return parameters.TryGetValue(key, out var value) ? Convert.ToInt32(value) : defaultValue;
}
}
// Supporting classes for complexity analysis
public class ComplexityMetrics
{
public int TotalMethods { get; set; }
public int TotalCyclomaticComplexity { get; set; }
public int TotalCognitiveComplexity { get; set; }
public int MaxCyclomaticComplexity { get; set; }
public int MaxCognitiveComplexity { get; set; }
public int CyclomaticViolations { get; set; }
public int CognitiveViolations { get; set; }
public double AverageCyclomaticComplexity => TotalMethods > 0 ? (double)TotalCyclomaticComplexity / TotalMethods : 0;
public double AverageCognitiveComplexity => TotalMethods > 0 ? (double)TotalCognitiveComplexity / TotalMethods : 0;
public void Add(MethodComplexityInfo method)
{
TotalMethods++;
TotalCyclomaticComplexity += method.CyclomaticComplexity;
TotalCognitiveComplexity += method.CognitiveComplexity;
if (method.CyclomaticComplexity > MaxCyclomaticComplexity)
MaxCyclomaticComplexity = method.CyclomaticComplexity;
if (method.CognitiveComplexity > MaxCognitiveComplexity)
MaxCognitiveComplexity = method.CognitiveComplexity;
}
public void Add(ComplexityMetrics other)
{
TotalMethods += other.TotalMethods;
TotalCyclomaticComplexity += other.TotalCyclomaticComplexity;
TotalCognitiveComplexity += other.TotalCognitiveComplexity;
if (other.MaxCyclomaticComplexity > MaxCyclomaticComplexity)
MaxCyclomaticComplexity = other.MaxCyclomaticComplexity;
if (other.MaxCognitiveComplexity > MaxCognitiveComplexity)
MaxCognitiveComplexity = other.MaxCognitiveComplexity;
CyclomaticViolations += other.CyclomaticViolations;
CognitiveViolations += other.CognitiveViolations;
}
}
public class MethodComplexityInfo
{
public string MethodName { get; set; } = string.Empty;
public string ClassName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public int LineNumber { get; set; }
public int CyclomaticComplexity { get; set; }
public int CognitiveComplexity { get; set; }
public int ParameterCount { get; set; }
public int LinesOfCode { get; set; }
}
public class ComplexityViolation
{
public string MethodName { get; set; } = string.Empty;
public string ClassName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public int LineNumber { get; set; }
public string ViolationType { get; set; } = string.Empty;
public int ActualValue { get; set; }
public int ThresholdValue { get; set; }
public string Severity { get; set; } = string.Empty;
}
public class FileComplexityResult
{
public string FilePath { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public ComplexityMetrics Metrics { get; set; } = new();
public List<MethodComplexityInfo> HighComplexityMethods { get; set; } = new();
public List<ComplexityViolation> Violations { get; set; } = new();
public List<object> MethodDetails { get; set; } = new();
}
// Cognitive Complexity Calculator (implements the cognitive complexity algorithm)
public class CognitiveComplexityCalculator
{
private int _complexity;
private int _nestingLevel;
public int Calculate(SyntaxNode node)
{
_complexity = 0;
_nestingLevel = 0;
Visit(node);
return _complexity;
}
private void Visit(SyntaxNode node)
{
switch (node)
{
case IfStatementSyntax _:
case WhileStatementSyntax _:
case ForStatementSyntax _:
case ForEachStatementSyntax _:
case DoStatementSyntax _:
_complexity += 1 + _nestingLevel;
_nestingLevel++;
VisitChildren(node);
_nestingLevel--;
break;
case SwitchStatementSyntax switchStmt:
_complexity += 1 + _nestingLevel;
_nestingLevel++;
VisitChildren(node);
_nestingLevel--;
break;
case ConditionalExpressionSyntax _:
_complexity += 1 + _nestingLevel;
VisitChildren(node);
break;
case CatchClauseSyntax _:
_complexity += 1 + _nestingLevel;
_nestingLevel++;
VisitChildren(node);
_nestingLevel--;
break;
case BinaryExpressionSyntax binary when
binary.OperatorToken.IsKind(SyntaxKind.AmpersandAmpersandToken) ||
binary.OperatorToken.IsKind(SyntaxKind.BarBarToken):
_complexity += 1;
VisitChildren(node);
break;
default:
VisitChildren(node);
break;
}
}
private void VisitChildren(SyntaxNode node)
{
foreach (var child in node.ChildNodes())
{
Visit(child);
}
}
}
}