SOMS/SolutionCleanupTool/Services/CleanupEngine.cs
2025-12-31 14:25:09 +08:00

359 lines
15 KiB
C#

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
{
/// <summary>
/// Implementation of ICleanupEngine for performing cleanup operations on solution files
/// </summary>
public class CleanupEngine : ICleanupEngine
{
private readonly ILogger<CleanupEngine> _logger;
// Regex patterns for solution file manipulation
private static readonly Regex ProjectPattern = new Regex(
@"Project\(""\{(?<ProjectTypeGuid>[^}]+)\}""\)\s*=\s*""(?<Name>[^""]+)""\s*,\s*""(?<RelativePath>[^""]+)""\s*,\s*""\{(?<ProjectGuid>[^}]+)\}""[^\r\n]*(?:\r?\n(?:\s+[^\r\n]*)*)*?EndProject",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);
private static readonly Regex ProjectConfigPattern = new Regex(
@"\{(?<ProjectGuid>[^}]+)\}\.(?<ConfigName>[^|]+)\|(?<Platform>[^.]+)\.(?<PropertyName>[^=\s]+)\s*=\s*[^\r\n]+",
RegexOptions.Compiled | RegexOptions.Multiline);
public CleanupEngine(ILogger<CleanupEngine> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Removes missing projects from a solution model
/// </summary>
/// <param name="solution">The solution model to clean</param>
/// <param name="missingProjects">List of missing projects to remove</param>
/// <returns>Result of the cleanup operation</returns>
public CleanupResult RemoveMissingProjects(SolutionModel solution, List<MissingProject> 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<MissingProject>(),
Warnings = new List<string>()
};
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;
}
/// <summary>
/// Creates a backup of the original solution file
/// </summary>
/// <param name="solutionPath">Path to the solution file to backup</param>
/// <returns>Path to the created backup file</returns>
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);
}
}
/// <summary>
/// Writes a solution model back to a file
/// </summary>
/// <param name="solution">The solution model to write</param>
/// <param name="outputPath">Path where to write the solution file</param>
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);
}
}
/// <summary>
/// Handles cascading reference removal - removes references to missing projects from other projects
/// </summary>
private void HandleCascadingReferences(SolutionModel solution, HashSet<Guid> 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);
}
}
/// <summary>
/// Removes missing projects from the solution file content
/// </summary>
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<Match>();
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;
}
/// <summary>
/// Removes project configuration entries for missing projects
/// </summary>
private string RemoveProjectConfigurations(string content, HashSet<Guid> validProjectGuids)
{
_logger.LogTrace("Removing project configuration entries for missing projects");
var configMatches = ProjectConfigPattern.Matches(content);
var configurationsToRemove = new List<Match>();
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;
}
/// <summary>
/// Cleans up consecutive empty lines in the solution content
/// </summary>
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;
}
}
}