319 lines
14 KiB
C#
319 lines
14 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Implementation of IProjectReferenceAnalyzer for analyzing project references
|
|
/// </summary>
|
|
public class ProjectReferenceAnalyzer : IProjectReferenceAnalyzer
|
|
{
|
|
private readonly ILogger<ProjectReferenceAnalyzer> _logger;
|
|
|
|
public ProjectReferenceAnalyzer(ILogger<ProjectReferenceAnalyzer> logger)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyzes a single project reference for validity
|
|
/// </summary>
|
|
/// <param name="reference">The project reference to analyze</param>
|
|
/// <returns>Analysis result indicating validity and any issues</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when reference is null</exception>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds all missing projects from a list of references
|
|
/// </summary>
|
|
/// <param name="references">List of project references to check</param>
|
|
/// <returns>List of missing projects</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when references is null</exception>
|
|
public List<MissingProject> FindMissingProjects(List<ProjectReference> references)
|
|
{
|
|
if (references == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(references));
|
|
}
|
|
|
|
_logger.LogInformation("Finding missing projects from {ReferenceCount} references", references.Count);
|
|
|
|
var missingProjects = new List<MissingProject>();
|
|
var projectReferenceCounts = new Dictionary<string, List<string>>();
|
|
|
|
// 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<string>();
|
|
}
|
|
|
|
// 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<string>();
|
|
|
|
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<string>(),
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a dependency graph from valid project references
|
|
/// </summary>
|
|
/// <param name="validReferences">List of valid project references</param>
|
|
/// <returns>Dependency graph showing project relationships</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when validReferences is null</exception>
|
|
public DependencyGraph BuildDependencyGraph(List<ProjectReference> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies the type of issue with a missing project reference
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a project file is well-formed and readable
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts project dependencies from a project file
|
|
/// </summary>
|
|
private List<string> ExtractProjectDependencies(ProjectReference project, List<ProjectReference> allProjects)
|
|
{
|
|
var dependencies = new List<string>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |