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

389 lines
16 KiB
C#

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
{
/// <summary>
/// Implementation of IValidationEngine for validating solution files after cleanup
/// </summary>
public class ValidationEngine : IValidationEngine
{
private readonly ILogger<ValidationEngine> _logger;
private readonly ISolutionParser _solutionParser;
public ValidationEngine(ILogger<ValidationEngine> logger, ISolutionParser solutionParser)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_solutionParser = solutionParser ?? throw new ArgumentNullException(nameof(solutionParser));
}
/// <summary>
/// Validates a solution file for correctness and loadability
/// </summary>
/// <param name="solutionPath">Path to the solution file to validate</param>
/// <returns>Validation result with any errors or warnings</returns>
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;
}
}
/// <summary>
/// Verifies that all project files in the references exist and are readable
/// </summary>
/// <param name="references">List of project references to verify</param>
/// <returns>True if all project files are valid, false otherwise</returns>
public bool VerifyProjectFiles(List<ProjectReference> 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;
}
/// <summary>
/// Checks a dependency graph for circular references
/// </summary>
/// <param name="graph">The dependency graph to check</param>
/// <returns>True if the graph is valid (no circular references), false otherwise</returns>
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;
}
/// <summary>
/// Validates the syntax of a project file
/// </summary>
/// <param name="projectPath">Path to the project file</param>
/// <returns>True if the project file has valid syntax, false otherwise</returns>
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;
}
}
/// <summary>
/// Builds a dependency graph from project references
/// </summary>
/// <param name="projectReferences">List of valid project references</param>
/// <returns>Dependency graph</returns>
private DependencyGraph BuildDependencyGraph(List<ProjectReference> 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;
}
/// <summary>
/// Extracts project dependencies from a project file
/// </summary>
/// <param name="projectPath">Path to the project file</param>
/// <returns>List of dependency project paths</returns>
private List<string> ExtractProjectDependencies(string projectPath)
{
var dependencies = new List<string>();
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;
}
/// <summary>
/// Attempts to verify that the solution can be loaded without errors
/// </summary>
/// <param name="solutionPath">Path to the solution file</param>
/// <returns>True if the solution appears loadable, false otherwise</returns>
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;
}
}
}
}