using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Logging; using SolutionCleanupTool.Interfaces; using SolutionCleanupTool.Models; namespace SolutionCleanupTool.Services { /// /// Implementation of IProjectReferenceAnalyzer for analyzing project references /// public class ProjectReferenceAnalyzer : IProjectReferenceAnalyzer { private readonly ILogger _logger; public ProjectReferenceAnalyzer(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Analyzes a single project reference for validity /// /// The project reference to analyze /// Analysis result indicating validity and any issues /// Thrown when reference is null public AnalysisResult AnalyzeReference(ProjectReference reference) { if (reference == null) { throw new ArgumentNullException(nameof(reference)); } _logger.LogDebug("Analyzing project reference: {ProjectName} at {ProjectPath}", reference.Name, reference.AbsolutePath); var result = new AnalysisResult { Reference = reference, IsValid = false, IssueType = ProjectReferenceIssueType.None }; try { // Check if the project file exists at the specified path if (!File.Exists(reference.AbsolutePath)) { // Try to determine if it's a missing file or incorrect path var issueType = ClassifyMissingProjectIssue(reference); result.IssueType = issueType; result.ErrorMessage = issueType == ProjectReferenceIssueType.FileNotFound ? $"Project file not found at path: {reference.AbsolutePath}" : $"Project file path appears incorrect: {reference.AbsolutePath}"; _logger.LogWarning("Project reference issue: {ErrorMessage}", result.ErrorMessage); return result; } // Validate that the project file is well-formed if (!IsValidProjectFile(reference.AbsolutePath)) { result.IssueType = ProjectReferenceIssueType.InvalidProjectFile; result.ErrorMessage = $"Project file is not well-formed or readable: {reference.AbsolutePath}"; _logger.LogWarning("Invalid project file: {ProjectPath}", reference.AbsolutePath); return result; } // If we get here, the reference is valid result.IsValid = true; reference.Exists = true; _logger.LogDebug("Project reference is valid: {ProjectName}", reference.Name); } catch (Exception ex) { result.IssueType = ProjectReferenceIssueType.InvalidProjectFile; result.ErrorMessage = $"Error analyzing project reference: {ex.Message}"; _logger.LogError(ex, "Error analyzing project reference: {ProjectName}", reference.Name); } return result; } /// /// Finds all missing projects from a list of references /// /// List of project references to check /// List of missing projects /// Thrown when references is null public List FindMissingProjects(List references) { if (references == null) { throw new ArgumentNullException(nameof(references)); } _logger.LogInformation("Finding missing projects from {ReferenceCount} references", references.Count); var missingProjects = new List(); var projectReferenceCounts = new Dictionary>(); // First pass: identify all missing projects and track which projects reference them foreach (var reference in references) { var analysisResult = AnalyzeReference(reference); if (!analysisResult.IsValid) { var projectKey = reference.AbsolutePath.ToLowerInvariant(); // Track which projects reference this missing project if (!projectReferenceCounts.ContainsKey(projectKey)) { projectReferenceCounts[projectKey] = new List(); } // Add the referencing project (in this case, it's from the solution file) var referencingProject = Path.GetFileName(reference.RelativePath); if (!projectReferenceCounts[projectKey].Contains(referencingProject)) { projectReferenceCounts[projectKey].Add(referencingProject); } } } // Second pass: create MissingProject objects with complete referencing information var processedProjects = new HashSet(); foreach (var reference in references) { var projectKey = reference.AbsolutePath.ToLowerInvariant(); if (!reference.Exists && !processedProjects.Contains(projectKey)) { var missingProject = new MissingProject { Reference = reference, ExpectedPath = reference.AbsolutePath, ReferencingProjects = projectReferenceCounts.ContainsKey(projectKey) ? projectReferenceCounts[projectKey] : new List(), ErrorMessage = $"Project file not found: {reference.AbsolutePath}" }; missingProjects.Add(missingProject); processedProjects.Add(projectKey); _logger.LogDebug("Added missing project: {ProjectName} referenced by {ReferenceCount} projects", reference.Name, missingProject.ReferencingProjects.Count); } } _logger.LogInformation("Found {MissingCount} missing projects", missingProjects.Count); return missingProjects; } /// /// Builds a dependency graph from valid project references /// /// List of valid project references /// Dependency graph showing project relationships /// Thrown when validReferences is null public DependencyGraph BuildDependencyGraph(List validReferences) { if (validReferences == null) { throw new ArgumentNullException(nameof(validReferences)); } _logger.LogInformation("Building dependency graph from {ReferenceCount} valid references", validReferences.Count); var dependencyGraph = new DependencyGraph { Nodes = validReferences.ToList() }; // Build the dependency relationships by analyzing project files foreach (var project in validReferences.Where(r => r.Exists)) { try { var projectDependencies = ExtractProjectDependencies(project, validReferences); foreach (var dependency in projectDependencies) { dependencyGraph.AddDependency(project.Name, dependency); _logger.LogTrace("Added dependency: {FromProject} -> {ToProject}", project.Name, dependency); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to extract dependencies for project: {ProjectName}", project.Name); // Continue processing other projects even if one fails } } // Check for circular references if (dependencyGraph.HasCircularReferences()) { _logger.LogWarning("Circular references detected in dependency graph"); } _logger.LogInformation("Built dependency graph with {NodeCount} nodes and {DependencyCount} dependencies", dependencyGraph.Nodes.Count, dependencyGraph.Dependencies.Values.Sum(deps => deps.Count)); return dependencyGraph; } /// /// Classifies the type of issue with a missing project reference /// private ProjectReferenceIssueType ClassifyMissingProjectIssue(ProjectReference reference) { try { // Check if the directory exists but the file doesn't var directory = Path.GetDirectoryName(reference.AbsolutePath); if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory)) { // Directory exists but file doesn't - likely a missing file return ProjectReferenceIssueType.FileNotFound; } // Check if there might be a similar file in the same directory or nearby var fileName = Path.GetFileName(reference.AbsolutePath); var parentDirectory = Path.GetDirectoryName(directory); if (!string.IsNullOrEmpty(parentDirectory) && Directory.Exists(parentDirectory)) { // Look for similar files in parent directory or subdirectories var similarFiles = Directory.GetFiles(parentDirectory, fileName, SearchOption.AllDirectories); if (similarFiles.Length > 0) { return ProjectReferenceIssueType.IncorrectPath; } } // Default to file not found if we can't determine the specific issue return ProjectReferenceIssueType.FileNotFound; } catch (Exception ex) { _logger.LogDebug(ex, "Error classifying missing project issue for: {ProjectPath}", reference.AbsolutePath); return ProjectReferenceIssueType.FileNotFound; } } /// /// Validates that a project file is well-formed and readable /// private bool IsValidProjectFile(string projectPath) { try { // Try to load the project file as XML var projectXml = XDocument.Load(projectPath); // Basic validation - check if it has a Project root element return projectXml.Root?.Name.LocalName == "Project"; } catch (Exception ex) { _logger.LogDebug(ex, "Project file validation failed for: {ProjectPath}", projectPath); return false; } } /// /// Extracts project dependencies from a project file /// private List ExtractProjectDependencies(ProjectReference project, List allProjects) { var dependencies = new List(); try { var projectXml = XDocument.Load(project.AbsolutePath); // Look for ProjectReference elements var projectReferences = projectXml.Descendants() .Where(e => e.Name.LocalName == "ProjectReference") .Select(e => e.Attribute("Include")?.Value) .Where(path => !string.IsNullOrEmpty(path)); foreach (var referencePath in projectReferences) { // Convert relative path to absolute path var projectDirectory = Path.GetDirectoryName(project.AbsolutePath); if (string.IsNullOrEmpty(projectDirectory) || string.IsNullOrEmpty(referencePath)) { continue; } var absoluteReferencePath = Path.GetFullPath(Path.Combine(projectDirectory, referencePath)); // Find the corresponding project in our list var referencedProject = allProjects.FirstOrDefault(p => string.Equals(p.AbsolutePath, absoluteReferencePath, StringComparison.OrdinalIgnoreCase)); if (referencedProject != null) { dependencies.Add(referencedProject.Name); } } } catch (Exception ex) { _logger.LogDebug(ex, "Failed to extract dependencies from project: {ProjectPath}", project.AbsolutePath); } return dependencies; } } }