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

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;
}
}
}