using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using SolutionCleanupTool.Interfaces; using SolutionCleanupTool.Models; namespace SolutionCleanupTool.Services { /// /// Implementation of ISolutionParser for parsing Visual Studio solution files /// public class SolutionParser : ISolutionParser { private readonly ILogger _logger; // Regex patterns for parsing solution file components private static readonly Regex ProjectPattern = new Regex( @"Project\(""\{(?[^}]+)\}""\)\s*=\s*""(?[^""]+)""\s*,\s*""(?[^""]+)""\s*,\s*""\{(?[^}]+)\}""", RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex SolutionConfigPattern = new Regex( @"(?[^|]+)\|(?[^=\s]+)\s*=\s*[^|]+\|[^\r\n]+", RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex GlobalSectionPattern = new Regex( @"GlobalSection\((?[^)]+)\)\s*=\s*(?[^\r\n]+)(?.*?)EndGlobalSection", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline); public SolutionParser(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Parses a Visual Studio solution file and returns a structured model /// /// Path to the .sln file /// Parsed solution model /// Thrown when solution path is null or empty /// Thrown when solution file doesn't exist /// Thrown when solution file format is invalid public SolutionModel ParseSolution(string solutionPath) { if (string.IsNullOrWhiteSpace(solutionPath)) { throw new ArgumentException("Solution path cannot be null or empty", nameof(solutionPath)); } if (!File.Exists(solutionPath)) { throw new FileNotFoundException($"Solution file not found: {solutionPath}"); } _logger.LogInformation("Parsing solution file: {SolutionPath}", solutionPath); try { string solutionContent = File.ReadAllText(solutionPath); var solutionModel = new SolutionModel { FilePath = Path.GetFullPath(solutionPath) }; // Parse projects solutionModel.Projects = ParseProjects(solutionContent, solutionPath); _logger.LogDebug("Found {ProjectCount} projects in solution", solutionModel.Projects.Count); // Parse solution configurations solutionModel.Configurations = ParseSolutionConfigurations(solutionContent); _logger.LogDebug("Found {ConfigCount} configurations in solution", solutionModel.Configurations.Count); // Parse global sections solutionModel.GlobalSections = ParseGlobalSections(solutionContent); _logger.LogDebug("Found {SectionCount} global sections in solution", solutionModel.GlobalSections.Count); _logger.LogInformation("Successfully parsed solution file: {SolutionPath}", solutionPath); return solutionModel; } catch (Exception ex) when (!(ex is ArgumentException || ex is FileNotFoundException)) { _logger.LogError(ex, "Failed to parse solution file: {SolutionPath}", solutionPath); throw new InvalidOperationException($"Failed to parse solution file: {solutionPath}", ex); } } /// /// Extracts project references from a parsed solution model /// /// The parsed solution model /// List of project references /// Thrown when solution is null public List ExtractProjectReferences(SolutionModel solution) { if (solution == null) { throw new ArgumentNullException(nameof(solution)); } _logger.LogDebug("Extracting project references from solution: {SolutionPath}", solution.FilePath); var projectReferences = new List(); foreach (var project in solution.Projects) { // Check if the project file exists project.Exists = File.Exists(project.AbsolutePath); if (!project.Exists) { _logger.LogWarning("Project file not found: {ProjectPath}", project.AbsolutePath); } projectReferences.Add(project); } _logger.LogDebug("Extracted {ReferenceCount} project references", projectReferences.Count); return projectReferences; } /// /// Parses project entries from solution file content /// private List ParseProjects(string solutionContent, string solutionPath) { var projects = new List(); var solutionDirectory = Path.GetDirectoryName(solutionPath) ?? string.Empty; var matches = ProjectPattern.Matches(solutionContent); foreach (Match match in matches) { try { var projectTypeGuid = match.Groups["ProjectTypeGuid"].Value; var name = match.Groups["Name"].Value; var relativePath = match.Groups["RelativePath"].Value; var projectGuidString = match.Groups["ProjectGuid"].Value; // Skip solution folders and other non-project entries if (IsProjectTypeGuid(projectTypeGuid) && !string.IsNullOrWhiteSpace(relativePath)) { var absolutePath = Path.GetFullPath(Path.Combine(solutionDirectory, relativePath)); var project = new ProjectReference { Name = name, RelativePath = relativePath, AbsolutePath = absolutePath, ProjectGuid = Guid.Parse(projectGuidString), ProjectTypeGuid = projectTypeGuid }; projects.Add(project); _logger.LogTrace("Parsed project: {ProjectName} at {ProjectPath}", name, relativePath); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to parse project entry: {ProjectEntry}", match.Value); // Continue parsing other projects even if one fails } } return projects; } /// /// Parses solution configurations from solution file content /// private List ParseSolutionConfigurations(string solutionContent) { var configurations = new List(); var seenConfigurations = new HashSet(); var matches = SolutionConfigPattern.Matches(solutionContent); foreach (Match match in matches) { try { var name = match.Groups["Name"].Value.Trim(); var platform = match.Groups["Platform"].Value.Trim(); var configKey = $"{name}|{platform}"; // Avoid duplicates if (!seenConfigurations.Contains(configKey)) { configurations.Add(new SolutionConfiguration { Name = name, Platform = platform }); seenConfigurations.Add(configKey); _logger.LogTrace("Parsed configuration: {ConfigName}|{Platform}", name, platform); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to parse configuration entry: {ConfigEntry}", match.Value); } } return configurations; } /// /// Parses global sections from solution file content /// private Dictionary ParseGlobalSections(string solutionContent) { var globalSections = new Dictionary(); var matches = GlobalSectionPattern.Matches(solutionContent); foreach (Match match in matches) { try { var sectionName = match.Groups["SectionName"].Value.Trim(); var sectionType = match.Groups["SectionType"].Value.Trim(); var content = match.Groups["Content"].Value.Trim(); var sectionKey = $"{sectionName}({sectionType})"; globalSections[sectionKey] = content; _logger.LogTrace("Parsed global section: {SectionName}", sectionKey); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to parse global section: {SectionEntry}", match.Value); } } return globalSections; } /// /// Determines if a GUID represents a project type (not a solution folder) /// private static bool IsProjectTypeGuid(string projectTypeGuid) { // Common project type GUIDs - this is not exhaustive but covers most cases var knownProjectTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC", // C# Project "F184B08F-C81C-45F6-A57F-5ABD9991F28F", // VB.NET Project "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942", // C++ Project "A9ACE9BB-CECE-4E62-9AA4-C7E7C5BD2124", // Database Project "349C5851-65DF-11DA-9384-00065B846F21", // Web Application Project "E24C65DC-7377-472B-9ABA-BC803B73C61A", // Web Site Project "F85E285D-A4E0-4152-9332-AB1D724D3325", // Modeling Project "6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705", // F# Project "E53F8FEA-EAE0-44A6-8774-FFD645390401", // ASP.NET MVC 3 Project "E3E379DF-F4C6-4180-9B81-6769533ABE47", // ASP.NET MVC 4 Project "603C0E0B-DB56-11DC-BE95-000D561079B0", // ASP.NET MVC 1 Project "F85E285D-A4E0-4152-9332-AB1D724D3325", // F# Project "786C830F-07A1-408B-BD7F-6EE04809D6DB", // Portable Library Project "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC" // .NET Core Project (uses same GUID as C#) }; // Solution folder GUID that we want to exclude const string SolutionFolderGuid = "2150E333-8FDC-42A3-9474-1A3956D46DE8"; return !string.Equals(projectTypeGuid, SolutionFolderGuid, StringComparison.OrdinalIgnoreCase) && (knownProjectTypes.Contains(projectTypeGuid) || // If it's not a known solution folder, assume it's a project !string.Equals(projectTypeGuid, SolutionFolderGuid, StringComparison.OrdinalIgnoreCase)); } } }