359 lines
15 KiB
C#
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;
|
|
}
|
|
}
|
|
} |