using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using SolutionCleanupTool.Interfaces; using SolutionCleanupTool.Models; namespace SolutionCleanupTool.Services { /// /// Implementation of ICleanupEngine for performing cleanup operations on solution files /// public class CleanupEngine : ICleanupEngine { private readonly ILogger _logger; // Regex patterns for solution file manipulation private static readonly Regex ProjectPattern = new Regex( @"Project\(""\{(?[^}]+)\}""\)\s*=\s*""(?[^""]+)""\s*,\s*""(?[^""]+)""\s*,\s*""\{(?[^}]+)\}""[^\r\n]*(?:\r?\n(?:\s+[^\r\n]*)*)*?EndProject", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); private static readonly Regex ProjectConfigPattern = new Regex( @"\{(?[^}]+)\}\.(?[^|]+)\|(?[^.]+)\.(?[^=\s]+)\s*=\s*[^\r\n]+", RegexOptions.Compiled | RegexOptions.Multiline); public CleanupEngine(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Removes missing projects from a solution model /// /// The solution model to clean /// List of missing projects to remove /// Result of the cleanup operation public CleanupResult RemoveMissingProjects(SolutionModel solution, List missingProjects) { if (solution == null) { throw new ArgumentNullException(nameof(solution)); } if (missingProjects == null) { throw new ArgumentNullException(nameof(missingProjects)); } _logger.LogInformation("Starting cleanup of {MissingCount} missing projects from solution: {SolutionPath}", missingProjects.Count, solution.FilePath); var result = new CleanupResult { Success = true, RemovedProjects = new List(), Warnings = new List() }; try { // Create backup before making changes result.BackupFilePath = CreateBackup(solution.FilePath); _logger.LogInformation("Created backup at: {BackupPath}", result.BackupFilePath); // Get GUIDs of projects to remove var projectGuidsToRemove = missingProjects .Select(mp => mp.Reference.ProjectGuid) .ToHashSet(); // Remove missing projects from the solution model var originalProjectCount = solution.Projects.Count; solution.Projects.RemoveAll(p => projectGuidsToRemove.Contains(p.ProjectGuid)); // Handle cascading reference removal HandleCascadingReferences(solution, projectGuidsToRemove, result); // Update result result.RemovedProjects = missingProjects.ToList(); result.TotalProjectsRemoved = originalProjectCount - solution.Projects.Count; _logger.LogInformation("Successfully removed {RemovedCount} projects from solution model", result.TotalProjectsRemoved); // Write the cleaned solution back to file WriteSolution(solution, solution.FilePath); _logger.LogInformation("Cleanup completed successfully for solution: {SolutionPath}", solution.FilePath); } catch (Exception ex) { _logger.LogError(ex, "Failed to cleanup solution: {SolutionPath}", solution.FilePath); result.Success = false; result.Warnings.Add($"Cleanup failed: {ex.Message}"); // Attempt to restore from backup if cleanup failed if (!string.IsNullOrEmpty(result.BackupFilePath) && File.Exists(result.BackupFilePath)) { try { File.Copy(result.BackupFilePath, solution.FilePath, true); _logger.LogInformation("Restored original solution from backup due to cleanup failure"); result.Warnings.Add("Original solution restored from backup due to cleanup failure"); } catch (Exception restoreEx) { _logger.LogError(restoreEx, "Failed to restore solution from backup"); result.Warnings.Add($"Failed to restore from backup: {restoreEx.Message}"); } } } return result; } /// /// Creates a backup of the original solution file /// /// Path to the solution file to backup /// Path to the created backup file public string CreateBackup(string solutionPath) { if (string.IsNullOrWhiteSpace(solutionPath)) { throw new ArgumentException("Solution path cannot be null or empty", nameof(solutionPath)); } if (!File.Exists(solutionPath)) { throw new FileNotFoundException($"Solution file not found: {solutionPath}"); } _logger.LogDebug("Creating backup of solution file: {SolutionPath}", solutionPath); try { var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var backupFileName = $"{Path.GetFileNameWithoutExtension(solutionPath)}_backup_{timestamp}.sln"; var backupPath = Path.Combine(Path.GetDirectoryName(solutionPath) ?? string.Empty, backupFileName); File.Copy(solutionPath, backupPath, false); _logger.LogInformation("Created backup file: {BackupPath}", backupPath); return backupPath; } catch (Exception ex) { _logger.LogError(ex, "Failed to create backup of solution file: {SolutionPath}", solutionPath); throw new InvalidOperationException($"Failed to create backup of solution file: {solutionPath}", ex); } } /// /// Writes a solution model back to a file /// /// The solution model to write /// Path where to write the solution file public void WriteSolution(SolutionModel solution, string outputPath) { if (solution == null) { throw new ArgumentNullException(nameof(solution)); } if (string.IsNullOrWhiteSpace(outputPath)) { throw new ArgumentException("Output path cannot be null or empty", nameof(outputPath)); } _logger.LogDebug("Writing solution to file: {OutputPath}", outputPath); try { // Read the original solution file content string originalContent = File.ReadAllText(solution.FilePath); // Remove missing projects from the content string cleanedContent = RemoveProjectsFromContent(originalContent, solution); // Write the cleaned content to the output file File.WriteAllText(outputPath, cleanedContent, Encoding.UTF8); _logger.LogInformation("Successfully wrote cleaned solution to: {OutputPath}", outputPath); } catch (Exception ex) { _logger.LogError(ex, "Failed to write solution to file: {OutputPath}", outputPath); throw new InvalidOperationException($"Failed to write solution to file: {outputPath}", ex); } } /// /// Handles cascading reference removal - removes references to missing projects from other projects /// private void HandleCascadingReferences(SolutionModel solution, HashSet removedProjectGuids, CleanupResult result) { _logger.LogDebug("Handling cascading reference removal for {RemovedCount} projects", removedProjectGuids.Count); var cascadingRemovals = 0; // Check each remaining project for references to removed projects foreach (var project in solution.Projects.ToList()) { try { if (File.Exists(project.AbsolutePath)) { var projectContent = File.ReadAllText(project.AbsolutePath); var hasReferencesToRemoved = false; // Check for project references to removed projects foreach (var removedGuid in removedProjectGuids) { var guidString = removedGuid.ToString(); if (projectContent.Contains(guidString, StringComparison.OrdinalIgnoreCase)) { hasReferencesToRemoved = true; _logger.LogDebug("Project {ProjectName} has references to removed project {RemovedGuid}", project.Name, guidString); } } if (hasReferencesToRemoved) { cascadingRemovals++; result.Warnings.Add($"Project '{project.Name}' may have references to removed projects that need manual cleanup"); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to check cascading references for project: {ProjectPath}", project.AbsolutePath); result.Warnings.Add($"Could not check cascading references for project '{project.Name}': {ex.Message}"); } } if (cascadingRemovals > 0) { _logger.LogInformation("Found {CascadingCount} projects that may need manual cleanup of references", cascadingRemovals); } } /// /// Removes missing projects from the solution file content /// private string RemoveProjectsFromContent(string originalContent, SolutionModel solution) { _logger.LogDebug("Removing missing projects from solution content"); var validProjectGuids = solution.Projects.Select(p => p.ProjectGuid).ToHashSet(); var content = originalContent; // Remove project declarations var projectMatches = ProjectPattern.Matches(content); var projectsToRemove = new List(); foreach (Match match in projectMatches) { try { var projectGuidString = match.Groups["ProjectGuid"].Value; if (Guid.TryParse(projectGuidString, out var projectGuid)) { if (!validProjectGuids.Contains(projectGuid)) { projectsToRemove.Add(match); _logger.LogTrace("Marking project for removal: {ProjectGuid}", projectGuid); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to parse project GUID from match: {MatchValue}", match.Value); } } // Remove projects in reverse order to maintain match positions foreach (var match in projectsToRemove.OrderByDescending(m => m.Index)) { content = content.Remove(match.Index, match.Length); _logger.LogTrace("Removed project declaration at position {Position}", match.Index); } // Remove project configuration entries content = RemoveProjectConfigurations(content, validProjectGuids); // Clean up any empty lines that might have been left behind content = CleanupEmptyLines(content); _logger.LogDebug("Completed removal of missing projects from solution content"); return content; } /// /// Removes project configuration entries for missing projects /// private string RemoveProjectConfigurations(string content, HashSet validProjectGuids) { _logger.LogTrace("Removing project configuration entries for missing projects"); var configMatches = ProjectConfigPattern.Matches(content); var configurationsToRemove = new List(); foreach (Match match in configMatches) { try { var projectGuidString = match.Groups["ProjectGuid"].Value; if (Guid.TryParse(projectGuidString, out var projectGuid)) { if (!validProjectGuids.Contains(projectGuid)) { configurationsToRemove.Add(match); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to parse project GUID from configuration match: {MatchValue}", match.Value); } } // Remove configurations in reverse order to maintain match positions foreach (var match in configurationsToRemove.OrderByDescending(m => m.Index)) { // Find the start of the line and remove the entire line var lineStart = content.LastIndexOf('\n', match.Index) + 1; var lineEnd = content.IndexOf('\n', match.Index); if (lineEnd == -1) lineEnd = content.Length; else lineEnd++; // Include the newline var lineLength = lineEnd - lineStart; if (lineLength > 0) { content = content.Remove(lineStart, lineLength); _logger.LogTrace("Removed project configuration line at position {Position}", lineStart); } } return content; } /// /// Cleans up consecutive empty lines in the solution content /// private string CleanupEmptyLines(string content) { // Replace multiple consecutive empty lines with a single empty line var cleanedContent = Regex.Replace(content, @"(\r?\n\s*){3,}", "\r\n\r\n", RegexOptions.Multiline); // Remove trailing whitespace from lines cleanedContent = Regex.Replace(cleanedContent, @"[ \t]+(\r?\n)", "$1", RegexOptions.Multiline); return cleanedContent; } } }