MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Refacto.../CodeAnalysisPlugin.cs

604 lines
19 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.Refactoring.Plugins
{
[AIPlugin("CodeAnalysis", "Analyzes code structure, complexity metrics, and suggests refactoring improvements")]
public class CodeAnalysisPlugin : IAIPlugin
{
[AIParameter("Full path to the file or directory to analyze", required: true)]
public string Path { get; set; }
[AIParameter("Analysis depth: basic, detailed, comprehensive", required: false)]
public string AnalysisDepth { get; set; } = "detailed";
[AIParameter("Include complexity metrics in analysis", required: false)]
public bool IncludeComplexity { get; set; } = true;
[AIParameter("Include code smell detection", required: false)]
public bool IncludeCodeSmells { get; set; } = true;
[AIParameter("Include refactoring suggestions", required: false)]
public bool IncludeSuggestions { get; set; } = true;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["path"] = typeof(string),
["analysisDepth"] = typeof(string),
["includeComplexity"] = typeof(bool),
["includeCodeSmells"] = typeof(bool),
["includeSuggestions"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
try
{
// Extract parameters
string path = parameters["path"].ToString();
string analysisDepth = parameters.TryGetValue("analysisDepth", out var depthObj)
? depthObj.ToString().ToLower()
: "detailed";
bool includeComplexity = parameters.TryGetValue("includeComplexity", out var complexityObj)
? Convert.ToBoolean(complexityObj)
: true;
bool includeCodeSmells = parameters.TryGetValue("includeCodeSmells", out var smellsObj)
? Convert.ToBoolean(smellsObj)
: true;
bool includeSuggestions = parameters.TryGetValue("includeSuggestions", out var suggestionsObj)
? Convert.ToBoolean(suggestionsObj)
: true;
// Validate path
if (!File.Exists(path) && !Directory.Exists(path))
{
return new AIPluginResult(
new FileNotFoundException($"Path not found: {path}"),
"Invalid path"
);
}
var analysisResults = new List<CodeAnalysisResult>();
if (File.Exists(path))
{
// Analyze single file
var result = await AnalyzeFileAsync(path, analysisDepth, includeComplexity, includeCodeSmells, includeSuggestions);
if (result != null)
analysisResults.Add(result);
}
else
{
// Analyze directory
var csharpFiles = Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories)
.Where(f => !ShouldExcludeFile(f))
.ToList();
foreach (var file in csharpFiles)
{
var result = await AnalyzeFileAsync(file, analysisDepth, includeComplexity, includeCodeSmells, includeSuggestions);
if (result != null)
analysisResults.Add(result);
}
}
// Generate summary
var summary = GenerateAnalysisSummary(analysisResults, analysisDepth);
return new AIPluginResult(new
{
Message = $"Code analysis completed for {analysisResults.Count} file(s)",
Path = path,
AnalysisDepth = analysisDepth,
Summary = summary,
DetailedResults = analysisResults,
Timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
return new AIPluginResult(ex, $"Code analysis failed: {ex.Message}");
}
}
private async Task<CodeAnalysisResult> AnalyzeFileAsync(string filePath, string analysisDepth,
bool includeComplexity, bool includeCodeSmells, bool includeSuggestions)
{
try
{
var sourceCode = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var root = syntaxTree.GetRoot();
var result = new CodeAnalysisResult
{
FilePath = filePath,
FileName = System.IO.Path.GetFileName(filePath),
LinesOfCode = sourceCode.Split('\n').Length,
Timestamp = DateTime.UtcNow
};
// Basic structure analysis
await AnalyzeStructure(result, root);
// Complexity analysis
if (includeComplexity)
{
await AnalyzeComplexity(result, root);
}
// Code smell detection
if (includeCodeSmells)
{
await DetectCodeSmells(result, root, sourceCode);
}
// Generate suggestions
if (includeSuggestions)
{
await GenerateRefactoringSuggestions(result, root, analysisDepth);
}
return result;
}
catch (Exception ex)
{
return new CodeAnalysisResult
{
FilePath = filePath,
FileName = System.IO.Path.GetFileName(filePath),
Error = ex.Message,
Timestamp = DateTime.UtcNow
};
}
}
private async Task AnalyzeStructure(CodeAnalysisResult result, SyntaxNode root)
{
var structure = new CodeStructure();
// Count different types of declarations
structure.Classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>().Count();
structure.Interfaces = root.DescendantNodes().OfType<InterfaceDeclarationSyntax>().Count();
structure.Methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
structure.Properties = root.DescendantNodes().OfType<PropertyDeclarationSyntax>().Count();
structure.Fields = root.DescendantNodes().OfType<FieldDeclarationSyntax>().Count();
// Analyze using statements
structure.UsingStatements = root.DescendantNodes().OfType<UsingDirectiveSyntax>().Count();
// Analyze namespaces
var namespaces = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
.Select(n => n.Name.ToString())
.Distinct()
.ToList();
structure.Namespaces = namespaces;
result.Structure = structure;
await Task.CompletedTask;
}
private async Task AnalyzeComplexity(CodeAnalysisResult result, SyntaxNode root)
{
var complexity = new ComplexityMetrics();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
var methodComplexity = CalculateCyclomaticComplexity(method);
var cognitiveComplexity = CalculateCognitiveComplexity(method);
complexity.Methods.Add(new MethodComplexity
{
MethodName = method.Identifier.ValueText,
CyclomaticComplexity = methodComplexity,
CognitiveComplexity = cognitiveComplexity,
LineCount = method.GetText().Lines.Count,
ParameterCount = method.ParameterList.Parameters.Count
});
}
complexity.AverageCyclomaticComplexity = complexity.Methods.Any()
? complexity.Methods.Average(m => m.CyclomaticComplexity)
: 0;
complexity.AverageCognitiveComplexity = complexity.Methods.Any()
? complexity.Methods.Average(m => m.CognitiveComplexity)
: 0;
complexity.MaxComplexity = complexity.Methods.Any()
? complexity.Methods.Max(m => m.CyclomaticComplexity)
: 0;
result.Complexity = complexity;
await Task.CompletedTask;
}
private async Task DetectCodeSmells(CodeAnalysisResult result, SyntaxNode root, string sourceCode)
{
var codeSmells = new List<CodeSmell>();
// God Class detection
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var cls in classes)
{
var methodCount = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
var lineCount = cls.GetText().Lines.Count;
if (methodCount > 20 || lineCount > 500)
{
codeSmells.Add(new CodeSmell
{
Type = "God Class",
Severity = "High",
Description = $"Class '{cls.Identifier.ValueText}' is too large ({methodCount} methods, {lineCount} lines)",
Location = $"Line {cls.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Suggestion = "Consider splitting this class into smaller, more focused classes"
});
}
}
// Long Method detection
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
var lineCount = method.GetText().Lines.Count;
if (lineCount > 50)
{
codeSmells.Add(new CodeSmell
{
Type = "Long Method",
Severity = "Medium",
Description = $"Method '{method.Identifier.ValueText}' is too long ({lineCount} lines)",
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Suggestion = "Consider extracting parts of this method into smaller methods"
});
}
}
// Long Parameter List detection
foreach (var method in methods)
{
var paramCount = method.ParameterList.Parameters.Count;
if (paramCount > 5)
{
codeSmells.Add(new CodeSmell
{
Type = "Long Parameter List",
Severity = "Medium",
Description = $"Method '{method.Identifier.ValueText}' has too many parameters ({paramCount})",
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Suggestion = "Consider grouping parameters into a class or using method overloads"
});
}
}
// Duplicate Code detection (simplified)
var stringLiterals = root.DescendantNodes().OfType<LiteralExpressionSyntax>()
.Where(l => l.Token.IsKind(SyntaxKind.StringLiteralToken))
.GroupBy(l => l.Token.ValueText)
.Where(g => g.Count() > 3 && g.Key.Length > 10)
.ToList();
foreach (var group in stringLiterals)
{
codeSmells.Add(new CodeSmell
{
Type = "Duplicate String Literals",
Severity = "Low",
Description = $"String literal '{group.Key}' appears {group.Count()} times",
Location = "Multiple locations",
Suggestion = "Consider extracting this string to a constant"
});
}
result.CodeSmells = codeSmells;
await Task.CompletedTask;
}
private async Task GenerateRefactoringSuggestions(CodeAnalysisResult result, SyntaxNode root, string analysisDepth)
{
var suggestions = new List<RefactoringSuggestion>();
// Extract Method suggestions
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
foreach (var method in methods)
{
var complexity = CalculateCyclomaticComplexity(method);
if (complexity > 10)
{
suggestions.Add(new RefactoringSuggestion
{
Type = "Extract Method",
Priority = "High",
Description = $"Method '{method.Identifier.ValueText}' has high complexity ({complexity})",
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Recommendation = "Consider extracting complex logic into separate methods",
EstimatedEffort = "Medium"
});
}
}
// Extract Class suggestions
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var cls in classes)
{
var methodCount = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Count();
if (methodCount > 15)
{
suggestions.Add(new RefactoringSuggestion
{
Type = "Extract Class",
Priority = "High",
Description = $"Class '{cls.Identifier.ValueText}' has too many responsibilities ({methodCount} methods)",
Location = $"Line {cls.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Recommendation = "Consider splitting into multiple classes with single responsibilities",
EstimatedEffort = "High"
});
}
}
// Introduce Parameter Object suggestions
foreach (var method in methods)
{
var paramCount = method.ParameterList.Parameters.Count;
if (paramCount > 4)
{
suggestions.Add(new RefactoringSuggestion
{
Type = "Introduce Parameter Object",
Priority = "Medium",
Description = $"Method '{method.Identifier.ValueText}' has many parameters ({paramCount})",
Location = $"Line {method.GetLocation().GetLineSpan().StartLinePosition.Line + 1}",
Recommendation = "Consider grouping related parameters into a class",
EstimatedEffort = "Low"
});
}
}
// Documentation suggestions
var undocumentedPublicMembers = root.DescendantNodes()
.Where(n => IsPublicMember(n) && !HasDocumentation(n))
.Take(10) // Limit suggestions
.ToList();
if (undocumentedPublicMembers.Any())
{
suggestions.Add(new RefactoringSuggestion
{
Type = "Add Documentation",
Priority = "Low",
Description = $"{undocumentedPublicMembers.Count} public members lack XML documentation",
Location = "Multiple locations",
Recommendation = "Add XML documentation to improve code maintainability",
EstimatedEffort = "Low"
});
}
result.Suggestions = suggestions;
await Task.CompletedTask;
}
private int CalculateCyclomaticComplexity(MethodDeclarationSyntax method)
{
int complexity = 1; // Base complexity
// Count decision points
var decisionNodes = method.DescendantNodes().Where(node =>
node.IsKind(SyntaxKind.IfStatement) ||
node.IsKind(SyntaxKind.WhileStatement) ||
node.IsKind(SyntaxKind.ForStatement) ||
node.IsKind(SyntaxKind.ForEachStatement) ||
node.IsKind(SyntaxKind.DoStatement) ||
node.IsKind(SyntaxKind.SwitchStatement) ||
node.IsKind(SyntaxKind.CaseSwitchLabel) ||
node.IsKind(SyntaxKind.CatchClause) ||
node.IsKind(SyntaxKind.ConditionalExpression)
);
complexity += decisionNodes.Count();
// Count logical operators
var logicalOperators = method.DescendantTokens().Where(token =>
token.IsKind(SyntaxKind.AmpersandAmpersandToken) ||
token.IsKind(SyntaxKind.BarBarToken)
);
complexity += logicalOperators.Count();
return complexity;
}
private int CalculateCognitiveComplexity(MethodDeclarationSyntax method)
{
// Simplified cognitive complexity calculation
// In practice, this would need more sophisticated nesting depth tracking
int complexity = 0;
int nestingLevel = 0;
foreach (var node in method.DescendantNodes())
{
switch (node.Kind())
{
case SyntaxKind.IfStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.ForStatement:
case SyntaxKind.ForEachStatement:
case SyntaxKind.DoStatement:
complexity += 1 + nestingLevel;
break;
case SyntaxKind.SwitchStatement:
complexity += 1 + nestingLevel;
break;
case SyntaxKind.CatchClause:
complexity += 1 + nestingLevel;
break;
}
// Track nesting (simplified)
if (node.IsKind(SyntaxKind.Block))
{
nestingLevel++;
}
}
return complexity;
}
private bool IsPublicMember(SyntaxNode node)
{
if (node is MemberDeclarationSyntax member)
{
return member.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
}
return false;
}
private bool HasDocumentation(SyntaxNode node)
{
return node.GetLeadingTrivia()
.Any(trivia => trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia));
}
private bool ShouldExcludeFile(string filePath)
{
var fileName = System.IO.Path.GetFileName(filePath);
var excludePatterns = new[] { ".Designer.cs", ".generated.cs", "AssemblyInfo.cs", "GlobalAssemblyInfo.cs" };
return excludePatterns.Any(pattern => fileName.EndsWith(pattern, StringComparison.OrdinalIgnoreCase));
}
private object GenerateAnalysisSummary(List<CodeAnalysisResult> results, string analysisDepth)
{
if (!results.Any())
{
return new { Message = "No files analyzed" };
}
var totalFiles = results.Count;
var totalLinesOfCode = results.Sum(r => r.LinesOfCode);
var totalClasses = results.Sum(r => r.Structure?.Classes ?? 0);
var totalMethods = results.Sum(r => r.Structure?.Methods ?? 0);
var averageComplexity = results
.Where(r => r.Complexity?.AverageCyclomaticComplexity > 0)
.Average(r => r.Complexity?.AverageCyclomaticComplexity ?? 0);
var topIssues = results
.SelectMany(r => r.CodeSmells ?? new List<CodeSmell>())
.GroupBy(cs => cs.Type)
.OrderByDescending(g => g.Count())
.Take(5)
.Select(g => new { Type = g.Key, Count = g.Count() })
.ToList();
var topSuggestions = results
.SelectMany(r => r.Suggestions ?? new List<RefactoringSuggestion>())
.GroupBy(s => s.Type)
.OrderByDescending(g => g.Count())
.Take(5)
.Select(g => new { Type = g.Key, Count = g.Count() })
.ToList();
return new
{
FilesAnalyzed = totalFiles,
TotalLinesOfCode = totalLinesOfCode,
TotalClasses = totalClasses,
TotalMethods = totalMethods,
AverageComplexity = Math.Round(averageComplexity, 2),
TopCodeSmells = topIssues,
TopRefactoringSuggestions = topSuggestions,
QualityScore = CalculateQualityScore(results),
AnalysisDepth = analysisDepth
};
}
private double CalculateQualityScore(List<CodeAnalysisResult> results)
{
if (!results.Any()) return 0;
double score = 100.0;
// Penalize high complexity
var avgComplexity = results
.Where(r => r.Complexity?.AverageCyclomaticComplexity > 0)
.Average(r => r.Complexity?.AverageCyclomaticComplexity ?? 0);
if (avgComplexity > 10) score -= (avgComplexity - 10) * 2;
// Penalize code smells
var totalSmells = results.Sum(r => r.CodeSmells?.Count ?? 0);
var totalMethods = results.Sum(r => r.Structure?.Methods ?? 1);
var smellRatio = (double)totalSmells / totalMethods;
score -= smellRatio * 20;
return Math.Max(0, Math.Min(100, Math.Round(score, 1)));
}
}
// Supporting classes for code analysis
public class CodeAnalysisResult
{
public string FilePath { get; set; }
public string FileName { get; set; }
public int LinesOfCode { get; set; }
public CodeStructure Structure { get; set; }
public ComplexityMetrics Complexity { get; set; }
public List<CodeSmell> CodeSmells { get; set; } = new List<CodeSmell>();
public List<RefactoringSuggestion> Suggestions { get; set; } = new List<RefactoringSuggestion>();
public string Error { get; set; }
public DateTime Timestamp { get; set; }
}
public class CodeStructure
{
public int Classes { get; set; }
public int Interfaces { get; set; }
public int Methods { get; set; }
public int Properties { get; set; }
public int Fields { get; set; }
public int UsingStatements { get; set; }
public List<string> Namespaces { get; set; } = new List<string>();
}
public class ComplexityMetrics
{
public List<MethodComplexity> Methods { get; set; } = new List<MethodComplexity>();
public double AverageCyclomaticComplexity { get; set; }
public double AverageCognitiveComplexity { get; set; }
public int MaxComplexity { get; set; }
}
public class MethodComplexity
{
public string MethodName { get; set; }
public int CyclomaticComplexity { get; set; }
public int CognitiveComplexity { get; set; }
public int LineCount { get; set; }
public int ParameterCount { get; set; }
}
public class CodeSmell
{
public string Type { get; set; }
public string Severity { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public string Suggestion { get; set; }
}
public class RefactoringSuggestion
{
public string Type { get; set; }
public string Priority { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public string Recommendation { get; set; }
public string EstimatedEffort { get; set; }
}
}