389 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |