MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Refacto.../CodeFormatterPlugin.cs

678 lines
21 KiB
C#
Executable File

using MarketAlly.AIPlugin;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter;
namespace MarketAlly.AIPlugin.Refactoring.Plugins
{
[AIPlugin("CodeFormatter", "Auto-formats code and enforces coding standards")]
public class CodeFormatterPlugin : IAIPlugin
{
[AIParameter("Full path to the file or directory to format", required: true)]
public string Path { get; set; }
[AIParameter("Formatting style: microsoft, google, allman, k&r", required: false)]
public string FormattingStyle { get; set; } = "microsoft";
[AIParameter("Fix indentation issues", required: false)]
public bool FixIndentation { get; set; } = true;
[AIParameter("Organize using statements", required: false)]
public bool OrganizeUsings { get; set; } = true;
[AIParameter("Remove unnecessary code", required: false)]
public bool RemoveUnnecessary { get; set; } = true;
[AIParameter("Apply changes to files", required: false)]
public bool ApplyChanges { get; set; } = false;
[AIParameter("Include file backup when applying changes", required: false)]
public bool CreateBackup { get; set; } = true;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["path"] = typeof(string),
["formattingStyle"] = typeof(string),
["formattingstyle"] = typeof(string), // Allow lowercase
["fixIndentation"] = typeof(bool),
["fixindentation"] = typeof(bool), // Allow lowercase
["organizeUsings"] = typeof(bool),
["organizeusings"] = typeof(bool), // Allow lowercase
["removeUnnecessary"] = typeof(bool),
["removeunnecessary"] = typeof(bool), // Allow lowercase
["applyChanges"] = typeof(bool),
["applychanges"] = typeof(bool), // Allow lowercase
["createBackup"] = typeof(bool),
["createbackup"] = typeof(bool) // Allow lowercase
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
try
{
// Extract parameters with case-insensitive handling
string path = parameters["path"].ToString();
string formattingStyle = GetParameterValue(parameters, "formattingStyle", "formattingstyle")?.ToString()?.ToLower() ?? "microsoft";
bool fixIndentation = GetBoolParameter(parameters, "fixIndentation", "fixindentation", true);
bool organizeUsings = GetBoolParameter(parameters, "organizeUsings", "organizeusings", true);
bool removeUnnecessary = GetBoolParameter(parameters, "removeUnnecessary", "removeunnecessary", true);
bool applyChanges = GetBoolParameter(parameters, "applyChanges", "applychanges", false);
bool createBackup = GetBoolParameter(parameters, "createBackup", "createbackup", true);
// Validate path
if (!File.Exists(path) && !Directory.Exists(path))
{
return new AIPluginResult(
new FileNotFoundException($"Path not found: {path}"),
"Invalid path"
);
}
var formattingResults = new List<FormattingResult>();
if (File.Exists(path))
{
// Format single file
var result = await FormatFileAsync(path, formattingStyle, fixIndentation, organizeUsings, removeUnnecessary, applyChanges, createBackup);
if (result != null)
formattingResults.Add(result);
}
else
{
// Format directory
var csharpFiles = Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories)
.Where(f => !ShouldExcludeFile(f))
.ToList();
foreach (var file in csharpFiles)
{
var result = await FormatFileAsync(file, formattingStyle, fixIndentation, organizeUsings, removeUnnecessary, applyChanges, createBackup);
if (result != null)
formattingResults.Add(result);
}
}
// Generate summary
var summary = GenerateFormattingSummary(formattingResults, applyChanges);
return new AIPluginResult(new
{
Message = $"Code formatting completed for {formattingResults.Count} file(s)",
Path = path,
FormattingStyle = formattingStyle,
ChangesApplied = applyChanges,
Summary = summary,
DetailedResults = formattingResults,
Timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
return new AIPluginResult(ex, $"Code formatting failed: {ex.Message}");
}
}
private async Task<FormattingResult> FormatFileAsync(string filePath, string formattingStyle,
bool fixIndentation, bool organizeUsings, bool removeUnnecessary, bool applyChanges, bool createBackup)
{
try
{
var originalContent = await File.ReadAllTextAsync(filePath);
var syntaxTree = CSharpSyntaxTree.ParseText(originalContent);
var root = syntaxTree.GetRoot();
var result = new FormattingResult
{
FilePath = filePath,
FileName = System.IO.Path.GetFileName(filePath),
OriginalLineCount = originalContent.Split('\n').Length,
Timestamp = DateTime.UtcNow
};
// Apply various formatting operations
var formattedRoot = root;
// 1. Organize using statements
if (organizeUsings)
{
formattedRoot = OrganizeUsingStatements(formattedRoot);
result.UsingsOrganized = true;
}
// 2. Remove unnecessary code
if (removeUnnecessary)
{
formattedRoot = await RemoveUnnecessaryCodeAsync(formattedRoot);
result.UnnecessaryCodeRemoved = true;
}
// 3. Apply formatting style
if (fixIndentation)
{
var workspace = new AdhocWorkspace();
var formattingOptions = GetFormattingOptions(formattingStyle, workspace);
formattedRoot = Formatter.Format(formattedRoot, workspace, formattingOptions);
result.IndentationFixed = true;
}
// 4. Apply additional style-specific formatting
formattedRoot = await ApplyStyleSpecificFormatting(formattedRoot, formattingStyle);
var formattedContent = formattedRoot.ToFullString();
result.FormattedLineCount = formattedContent.Split('\n').Length;
result.FormattedContent = formattedContent;
// Calculate changes
result.Changes = CalculateFormattingChanges(originalContent, formattedContent);
// Apply changes if requested
if (applyChanges && result.Changes.TotalChanges > 0)
{
if (createBackup)
{
var backupPath = $"{filePath}.{DateTime.Now:yyyyMMdd_HHmmss}.bak";
File.Copy(filePath, backupPath);
result.BackupPath = backupPath;
}
await File.WriteAllTextAsync(filePath, formattedContent, Encoding.UTF8);
result.ChangesApplied = true;
}
return result;
}
catch (Exception ex)
{
return new FormattingResult
{
FilePath = filePath,
FileName = System.IO.Path.GetFileName(filePath),
Error = ex.Message,
Timestamp = DateTime.UtcNow
};
}
}
private SyntaxNode OrganizeUsingStatements(SyntaxNode root)
{
var compilationUnit = root as CompilationUnitSyntax;
if (compilationUnit == null)
return root;
var usings = compilationUnit.Usings;
if (!usings.Any())
return root;
// Group and sort using statements
var systemUsings = new List<UsingDirectiveSyntax>();
var thirdPartyUsings = new List<UsingDirectiveSyntax>();
var projectUsings = new List<UsingDirectiveSyntax>();
foreach (var usingDirective in usings)
{
var namespaceName = usingDirective.Name.ToString();
if (namespaceName.StartsWith("System"))
{
systemUsings.Add(usingDirective);
}
else if (IsThirdPartyNamespace(namespaceName))
{
thirdPartyUsings.Add(usingDirective);
}
else
{
projectUsings.Add(usingDirective);
}
}
// Sort each group alphabetically
systemUsings = systemUsings.OrderBy(u => u.Name.ToString()).ToList();
thirdPartyUsings = thirdPartyUsings.OrderBy(u => u.Name.ToString()).ToList();
projectUsings = projectUsings.OrderBy(u => u.Name.ToString()).ToList();
// Combine groups with blank lines between them
var organizedUsings = new List<UsingDirectiveSyntax>();
organizedUsings.AddRange(systemUsings);
if (systemUsings.Any() && (thirdPartyUsings.Any() || projectUsings.Any()))
{
// Add blank line after system usings
var lastSystemUsing = organizedUsings.Last();
organizedUsings[organizedUsings.Count - 1] = lastSystemUsing.WithTrailingTrivia(
lastSystemUsing.GetTrailingTrivia().Add(SyntaxFactory.CarriageReturnLineFeed));
}
organizedUsings.AddRange(thirdPartyUsings);
if (thirdPartyUsings.Any() && projectUsings.Any())
{
// Add blank line after third-party usings
var lastThirdPartyUsing = organizedUsings.Last();
organizedUsings[organizedUsings.Count - 1] = lastThirdPartyUsing.WithTrailingTrivia(
lastThirdPartyUsing.GetTrailingTrivia().Add(SyntaxFactory.CarriageReturnLineFeed));
}
organizedUsings.AddRange(projectUsings);
// Replace the using statements
var newCompilationUnit = compilationUnit.WithUsings(SyntaxFactory.List(organizedUsings));
return newCompilationUnit;
}
private async Task<SyntaxNode> RemoveUnnecessaryCodeAsync(SyntaxNode root)
{
var newRoot = root;
// Remove empty statements
var emptyStatements = root.DescendantNodes().OfType<EmptyStatementSyntax>().ToList();
newRoot = newRoot.RemoveNodes(emptyStatements, SyntaxRemoveOptions.KeepNoTrivia);
// Remove redundant else statements
newRoot = await RemoveRedundantElseStatements(newRoot);
// Remove unnecessary parentheses
newRoot = await RemoveUnnecessaryParentheses(newRoot);
// Remove unused using statements (simplified version)
newRoot = await RemoveUnusedUsings(newRoot);
return newRoot;
}
private async Task<SyntaxNode> RemoveRedundantElseStatements(SyntaxNode root)
{
var ifStatements = root.DescendantNodes().OfType<IfStatementSyntax>().ToList();
var newRoot = root;
foreach (var ifStatement in ifStatements)
{
if (ifStatement.Else != null && IsRedundantElse(ifStatement))
{
var newIfStatement = ifStatement.WithElse(null);
newRoot = newRoot.ReplaceNode(ifStatement, newIfStatement);
}
}
return await Task.FromResult(newRoot);
}
private bool IsRedundantElse(IfStatementSyntax ifStatement)
{
// Check if the if statement ends with a return, throw, or break
var statement = ifStatement.Statement;
if (statement is BlockSyntax block)
{
var lastStatement = block.Statements.LastOrDefault();
return lastStatement is ReturnStatementSyntax ||
lastStatement is ThrowStatementSyntax ||
lastStatement is BreakStatementSyntax ||
lastStatement is ContinueStatementSyntax;
}
return statement is ReturnStatementSyntax ||
statement is ThrowStatementSyntax ||
statement is BreakStatementSyntax ||
statement is ContinueStatementSyntax;
}
private async Task<SyntaxNode> RemoveUnnecessaryParentheses(SyntaxNode root)
{
var parenthesizedExpressions = root.DescendantNodes().OfType<ParenthesizedExpressionSyntax>().ToList();
var newRoot = root;
foreach (var parenthesized in parenthesizedExpressions)
{
if (IsUnnecessaryParentheses(parenthesized))
{
newRoot = newRoot.ReplaceNode(parenthesized, parenthesized.Expression);
}
}
return await Task.FromResult(newRoot);
}
private bool IsUnnecessaryParentheses(ParenthesizedExpressionSyntax parenthesized)
{
// Simplified check - remove parentheses around single identifiers or literals
return parenthesized.Expression is IdentifierNameSyntax ||
parenthesized.Expression is LiteralExpressionSyntax ||
parenthesized.Expression is ThisExpressionSyntax;
}
private async Task<SyntaxNode> RemoveUnusedUsings(SyntaxNode root)
{
var compilationUnit = root as CompilationUnitSyntax;
if (compilationUnit == null)
return root;
var usedNamespaces = new HashSet<string>();
// Collect all type references in the code
var typeReferences = root.DescendantNodes()
.Where(n => n is IdentifierNameSyntax || n is QualifiedNameSyntax)
.Select(n => n.ToString())
.ToHashSet();
// Check which using statements are actually used (simplified approach)
var usingsToKeep = new List<UsingDirectiveSyntax>();
foreach (var usingDirective in compilationUnit.Usings)
{
var namespaceName = usingDirective.Name.ToString();
var namespaceParts = namespaceName.Split('.');
// Keep using if any type reference could come from this namespace
bool isUsed = typeReferences.Any(typeRef =>
namespaceParts.Any(part => typeRef.Contains(part))) ||
IsEssentialNamespace(namespaceName);
if (isUsed)
{
usingsToKeep.Add(usingDirective);
}
}
return await Task.FromResult(compilationUnit.WithUsings(SyntaxFactory.List(usingsToKeep)));
}
private bool IsEssentialNamespace(string namespaceName)
{
// Keep essential namespaces that are commonly used but might not be detected
var essentialNamespaces = new[]
{
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks"
};
return essentialNamespaces.Contains(namespaceName);
}
private async Task<SyntaxNode> ApplyStyleSpecificFormatting(SyntaxNode root, string formattingStyle)
{
var newRoot = root;
switch (formattingStyle.ToLower())
{
case "allman":
newRoot = await ApplyAllmanStyle(newRoot);
break;
case "kr":
case "k&r":
newRoot = await ApplyKRStyle(newRoot);
break;
case "google":
newRoot = await ApplyGoogleStyle(newRoot);
break;
case "microsoft":
default:
// Microsoft style is the default for Roslyn formatter
break;
}
return newRoot;
}
private async Task<SyntaxNode> ApplyAllmanStyle(SyntaxNode root)
{
// Allman style: opening braces on new lines
var newRoot = root;
// This is a simplified implementation
// In practice, you'd need more sophisticated brace positioning
var blocks = root.DescendantNodes().OfType<BlockSyntax>().ToList();
foreach (var block in blocks)
{
var newBlock = block.WithOpenBraceToken(
block.OpenBraceToken.WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed)
);
newRoot = newRoot.ReplaceNode(block, newBlock);
}
return await Task.FromResult(newRoot);
}
private async Task<SyntaxNode> ApplyKRStyle(SyntaxNode root)
{
// K&R style: opening braces on same line
var newRoot = root;
var blocks = root.DescendantNodes().OfType<BlockSyntax>().ToList();
foreach (var block in blocks)
{
var newBlock = block.WithOpenBraceToken(
block.OpenBraceToken.WithLeadingTrivia(SyntaxFactory.Space)
);
newRoot = newRoot.ReplaceNode(block, newBlock);
}
return await Task.FromResult(newRoot);
}
private async Task<SyntaxNode> ApplyGoogleStyle(SyntaxNode root)
{
// Google C# style guide formatting
var newRoot = root;
// Apply specific Google style rules
// This is a simplified implementation
return await Task.FromResult(newRoot);
}
private OptionSet GetFormattingOptions(string formattingStyle, Workspace workspace)
{
var options = workspace.Options;
// Configure indentation
options = options.WithChangedOption(FormattingOptions.IndentationSize, LanguageNames.CSharp, 4);
options = options.WithChangedOption(FormattingOptions.TabSize, LanguageNames.CSharp, 4);
options = options.WithChangedOption(FormattingOptions.UseTabs, LanguageNames.CSharp, false);
// Configure spacing
options = options.WithChangedOption(FormattingOptions.SmartIndent, LanguageNames.CSharp, FormattingOptions.IndentStyle.Smart);
// Style-specific options
switch (formattingStyle.ToLower())
{
case "allman":
// Allman style preferences
break;
case "kr":
case "k&r":
// K&R style preferences
break;
case "google":
// Google style preferences
options = options.WithChangedOption(FormattingOptions.IndentationSize, LanguageNames.CSharp, 2);
options = options.WithChangedOption(FormattingOptions.TabSize, LanguageNames.CSharp, 2);
break;
case "microsoft":
default:
// Microsoft style (default)
break;
}
return options;
}
private FormattingChanges CalculateFormattingChanges(string originalContent, string formattedContent)
{
var originalLines = originalContent.Split('\n');
var formattedLines = formattedContent.Split('\n');
var changes = new FormattingChanges();
// Simple diff calculation
changes.LinesChanged = 0;
changes.WhitespaceChanges = 0;
changes.StructuralChanges = 0;
int maxLines = Math.Max(originalLines.Length, formattedLines.Length);
for (int i = 0; i < maxLines; i++)
{
var originalLine = i < originalLines.Length ? originalLines[i] : "";
var formattedLine = i < formattedLines.Length ? formattedLines[i] : "";
if (originalLine != formattedLine)
{
changes.LinesChanged++;
// Check if it's just whitespace changes
if (originalLine.Trim() == formattedLine.Trim())
{
changes.WhitespaceChanges++;
}
else
{
changes.StructuralChanges++;
}
}
}
changes.TotalChanges = changes.LinesChanged;
changes.ChangePercentage = originalLines.Length > 0
? Math.Round((double)changes.LinesChanged / originalLines.Length * 100, 1)
: 0;
return changes;
}
private bool IsThirdPartyNamespace(string namespaceName)
{
// Common third-party namespace patterns
var thirdPartyPrefixes = new[]
{
"Microsoft.Extensions",
"Microsoft.AspNetCore",
"Microsoft.EntityFrameworkCore",
"Newtonsoft",
"AutoMapper",
"Serilog",
"NLog",
"FluentValidation",
"MediatR",
"Moq",
"NUnit",
"Xunit"
};
return thirdPartyPrefixes.Any(prefix => namespaceName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
}
private bool ShouldExcludeFile(string filePath)
{
var fileName = System.IO.Path.GetFileName(filePath);
var excludePatterns = new[]
{
".Designer.cs",
".generated.cs",
".g.cs",
"AssemblyInfo.cs",
"GlobalAssemblyInfo.cs",
"TemporaryGeneratedFile_",
".AssemblyAttributes.cs"
};
return excludePatterns.Any(pattern => fileName.Contains(pattern, StringComparison.OrdinalIgnoreCase));
}
private object GenerateFormattingSummary(List<FormattingResult> results, bool changesApplied)
{
if (!results.Any())
{
return new { Message = "No files processed" };
}
var successfulResults = results.Where(r => string.IsNullOrEmpty(r.Error)).ToList();
var failedResults = results.Where(r => !string.IsNullOrEmpty(r.Error)).ToList();
var totalFilesProcessed = results.Count;
var totalLinesProcessed = successfulResults.Sum(r => r.OriginalLineCount);
var totalChanges = successfulResults.Sum(r => r.Changes?.TotalChanges ?? 0);
var averageChangePercentage = successfulResults.Any()
? successfulResults.Average(r => r.Changes?.ChangePercentage ?? 0)
: 0;
var formattingActions = new
{
UsingsOrganized = successfulResults.Count(r => r.UsingsOrganized),
IndentationFixed = successfulResults.Count(r => r.IndentationFixed),
UnnecessaryCodeRemoved = successfulResults.Count(r => r.UnnecessaryCodeRemoved)
};
return new
{
TotalFilesProcessed = totalFilesProcessed,
SuccessfulFiles = successfulResults.Count,
FailedFiles = failedResults.Count,
TotalLinesProcessed = totalLinesProcessed,
TotalChanges = totalChanges,
AverageChangePercentage = Math.Round(averageChangePercentage, 1),
ChangesApplied = changesApplied,
FormattingActions = formattingActions,
FailedFileDetails = failedResults.Select(r => new { r.FilePath, r.Error }).ToList()
};
}
// Helper methods for parameter extraction
private object GetParameterValue(IReadOnlyDictionary<string, object> parameters, params string[] keys)
{
foreach (var key in keys)
{
if (parameters.TryGetValue(key, out var value))
return value;
}
return null;
}
private bool GetBoolParameter(IReadOnlyDictionary<string, object> parameters, string key1, string key2, bool defaultValue = false)
{
var value = GetParameterValue(parameters, key1, key2);
return value != null ? Convert.ToBoolean(value) : defaultValue;
}
}
// Supporting classes for code formatting
public class FormattingResult
{
public string FilePath { get; set; }
public string FileName { get; set; }
public int OriginalLineCount { get; set; }
public int FormattedLineCount { get; set; }
public string FormattedContent { get; set; }
public FormattingChanges Changes { get; set; }
public bool UsingsOrganized { get; set; }
public bool IndentationFixed { get; set; }
public bool UnnecessaryCodeRemoved { get; set; }
public bool ChangesApplied { get; set; }
public string BackupPath { get; set; }
public string Error { get; set; }
public DateTime Timestamp { get; set; }
}
public class FormattingChanges
{
public int LinesChanged { get; set; }
public int WhitespaceChanges { get; set; }
public int StructuralChanges { get; set; }
public int TotalChanges { get; set; }
public double ChangePercentage { get; set; }
}
}