using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using Microsoft.Extensions.Logging; using SolutionCleanupTool.Interfaces; using SolutionCleanupTool.Models; namespace SolutionCleanupTool.Services { /// /// Implementation of IValidationEngine for validating solution files after cleanup /// public class ValidationEngine : IValidationEngine { private readonly ILogger _logger; private readonly ISolutionParser _solutionParser; public ValidationEngine(ILogger logger, ISolutionParser solutionParser) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _solutionParser = solutionParser ?? throw new ArgumentNullException(nameof(solutionParser)); } /// /// Validates a solution file for correctness and loadability /// /// Path to the solution file to validate /// Validation result with any errors or warnings public ValidationResult ValidateSolution(string solutionPath) { if (string.IsNullOrWhiteSpace(solutionPath)) { throw new ArgumentException("Solution path cannot be null or empty", nameof(solutionPath)); } _logger.LogInformation("Starting validation of solution: {SolutionPath}", solutionPath); var result = new ValidationResult(); try { // Check if solution file exists if (!File.Exists(solutionPath)) { result.Errors.Add($"Solution file not found: {solutionPath}"); result.IsValid = false; return result; } // Parse the solution SolutionModel solution; try { solution = _solutionParser.ParseSolution(solutionPath); } catch (Exception ex) { result.Errors.Add($"Failed to parse solution file: {ex.Message}"); result.IsValid = false; return result; } // Extract project references var projectReferences = _solutionParser.ExtractProjectReferences(solution); // Verify all project files exist and are readable if (!VerifyProjectFiles(projectReferences)) { var missingProjects = projectReferences.Where(p => !p.Exists).ToList(); foreach (var missingProject in missingProjects) { result.Errors.Add($"Project file not found or not readable: {missingProject.AbsolutePath}"); } } // Validate project file syntax for existing projects var validProjects = projectReferences.Where(p => p.Exists).ToList(); foreach (var project in validProjects) { if (!ValidateProjectFileSyntax(project.AbsolutePath)) { result.Errors.Add($"Project file has invalid syntax: {project.AbsolutePath}"); } } // Build and validate dependency graph var dependencyGraph = BuildDependencyGraph(validProjects); if (!CheckDependencyGraph(dependencyGraph)) { result.HasCircularReferences = true; result.Errors.Add("Solution contains circular project dependencies"); } // Attempt to verify solution loadability result.CanLoadSolution = VerifySolutionLoadability(solutionPath); if (!result.CanLoadSolution) { result.Warnings.Add("Solution may have issues when loaded in Visual Studio"); } // Determine overall validation result result.IsValid = result.Errors.Count == 0; _logger.LogInformation("Validation completed for solution: {SolutionPath}. Valid: {IsValid}, Errors: {ErrorCount}, Warnings: {WarningCount}", solutionPath, result.IsValid, result.Errors.Count, result.Warnings.Count); return result; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during solution validation: {SolutionPath}", solutionPath); result.Errors.Add($"Unexpected validation error: {ex.Message}"); result.IsValid = false; return result; } } /// /// Verifies that all project files in the references exist and are readable /// /// List of project references to verify /// True if all project files are valid, false otherwise public bool VerifyProjectFiles(List references) { if (references == null) { throw new ArgumentNullException(nameof(references)); } _logger.LogDebug("Verifying {ProjectCount} project files", references.Count); bool allValid = true; foreach (var reference in references) { try { // Check if file exists if (!File.Exists(reference.AbsolutePath)) { _logger.LogWarning("Project file not found: {ProjectPath}", reference.AbsolutePath); reference.Exists = false; allValid = false; continue; } // Check if file is readable try { using (var fileStream = File.OpenRead(reference.AbsolutePath)) { // Just try to open the file to verify it's readable } reference.Exists = true; _logger.LogTrace("Project file verified: {ProjectPath}", reference.AbsolutePath); } catch (UnauthorizedAccessException) { _logger.LogWarning("Project file is not readable (access denied): {ProjectPath}", reference.AbsolutePath); reference.Exists = false; allValid = false; } catch (IOException ex) { _logger.LogWarning(ex, "Project file is not readable (IO error): {ProjectPath}", reference.AbsolutePath); reference.Exists = false; allValid = false; } } catch (Exception ex) { _logger.LogError(ex, "Unexpected error verifying project file: {ProjectPath}", reference.AbsolutePath); reference.Exists = false; allValid = false; } } _logger.LogDebug("Project file verification completed. All valid: {AllValid}", allValid); return allValid; } /// /// Checks a dependency graph for circular references /// /// The dependency graph to check /// True if the graph is valid (no circular references), false otherwise public bool CheckDependencyGraph(DependencyGraph graph) { if (graph == null) { throw new ArgumentNullException(nameof(graph)); } _logger.LogDebug("Checking dependency graph for circular references"); // Use the built-in method from DependencyGraph bool hasCircularReferences = graph.HasCircularReferences(); if (hasCircularReferences) { _logger.LogWarning("Circular references detected in dependency graph"); } else { _logger.LogDebug("No circular references found in dependency graph"); } return !hasCircularReferences; } /// /// Validates the syntax of a project file /// /// Path to the project file /// True if the project file has valid syntax, false otherwise private bool ValidateProjectFileSyntax(string projectPath) { try { _logger.LogTrace("Validating project file syntax: {ProjectPath}", projectPath); // Try to load the project file as XML to validate syntax var xmlDoc = new XmlDocument(); xmlDoc.Load(projectPath); // Basic validation - check if it has a Project root element if (xmlDoc.DocumentElement?.Name != "Project") { _logger.LogWarning("Project file does not have valid Project root element: {ProjectPath}", projectPath); return false; } _logger.LogTrace("Project file syntax is valid: {ProjectPath}", projectPath); return true; } catch (XmlException ex) { _logger.LogWarning(ex, "Project file has invalid XML syntax: {ProjectPath}", projectPath); return false; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error validating project file syntax: {ProjectPath}", projectPath); return false; } } /// /// Builds a dependency graph from project references /// /// List of valid project references /// Dependency graph private DependencyGraph BuildDependencyGraph(List projectReferences) { var graph = new DependencyGraph(); graph.Nodes = projectReferences; _logger.LogDebug("Building dependency graph from {ProjectCount} projects", projectReferences.Count); foreach (var project in projectReferences) { try { // Parse project file to find ProjectReference elements var dependencies = ExtractProjectDependencies(project.AbsolutePath); foreach (var dependency in dependencies) { // Find the referenced project in our list var referencedProject = projectReferences.FirstOrDefault(p => string.Equals(Path.GetFileName(p.RelativePath), Path.GetFileName(dependency), StringComparison.OrdinalIgnoreCase) || string.Equals(p.Name, Path.GetFileNameWithoutExtension(dependency), StringComparison.OrdinalIgnoreCase)); if (referencedProject != null) { graph.AddDependency(project.Name, referencedProject.Name); _logger.LogTrace("Added dependency: {FromProject} -> {ToProject}", project.Name, referencedProject.Name); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to extract dependencies from project: {ProjectPath}", project.AbsolutePath); } } _logger.LogDebug("Dependency graph built with {NodeCount} nodes and {DependencyCount} dependencies", graph.Nodes.Count, graph.Dependencies.Values.Sum(deps => deps.Count)); return graph; } /// /// Extracts project dependencies from a project file /// /// Path to the project file /// List of dependency project paths private List ExtractProjectDependencies(string projectPath) { var dependencies = new List(); try { var xmlDoc = new XmlDocument(); xmlDoc.Load(projectPath); // Find all ProjectReference elements var projectReferences = xmlDoc.SelectNodes("//ProjectReference"); if (projectReferences != null) { foreach (XmlNode reference in projectReferences) { var includeAttribute = reference.Attributes?["Include"]; if (includeAttribute != null && !string.IsNullOrWhiteSpace(includeAttribute.Value)) { dependencies.Add(includeAttribute.Value); } } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to extract dependencies from project file: {ProjectPath}", projectPath); } return dependencies; } /// /// Attempts to verify that the solution can be loaded without errors /// /// Path to the solution file /// True if the solution appears loadable, false otherwise private bool VerifySolutionLoadability(string solutionPath) { try { _logger.LogTrace("Verifying solution loadability: {SolutionPath}", solutionPath); // Basic checks for solution loadability // 1. Solution file exists and is readable if (!File.Exists(solutionPath)) { return false; } // 2. Solution file can be parsed successfully try { var solution = _solutionParser.ParseSolution(solutionPath); var projectReferences = _solutionParser.ExtractProjectReferences(solution); // 3. All referenced projects exist var allProjectsExist = projectReferences.All(p => p.Exists); if (!allProjectsExist) { _logger.LogTrace("Solution loadability check failed: missing project files"); return false; } // 4. No circular dependencies var dependencyGraph = BuildDependencyGraph(projectReferences.Where(p => p.Exists).ToList()); if (dependencyGraph.HasCircularReferences()) { _logger.LogTrace("Solution loadability check failed: circular dependencies"); return false; } _logger.LogTrace("Solution appears to be loadable: {SolutionPath}", solutionPath); return true; } catch (Exception ex) { _logger.LogTrace(ex, "Solution loadability check failed during parsing: {SolutionPath}", solutionPath); return false; } } catch (Exception ex) { _logger.LogWarning(ex, "Unexpected error during solution loadability check: {SolutionPath}", solutionPath); return false; } } } }