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

274 lines
12 KiB
C#

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
{
/// <summary>
/// Implementation of ISolutionParser for parsing Visual Studio solution files
/// </summary>
public class SolutionParser : ISolutionParser
{
private readonly ILogger<SolutionParser> _logger;
// Regex patterns for parsing solution file components
private static readonly Regex ProjectPattern = new Regex(
@"Project\(""\{(?<ProjectTypeGuid>[^}]+)\}""\)\s*=\s*""(?<Name>[^""]+)""\s*,\s*""(?<RelativePath>[^""]+)""\s*,\s*""\{(?<ProjectGuid>[^}]+)\}""",
RegexOptions.Compiled | RegexOptions.Multiline);
private static readonly Regex SolutionConfigPattern = new Regex(
@"(?<Name>[^|]+)\|(?<Platform>[^=\s]+)\s*=\s*[^|]+\|[^\r\n]+",
RegexOptions.Compiled | RegexOptions.Multiline);
private static readonly Regex GlobalSectionPattern = new Regex(
@"GlobalSection\((?<SectionName>[^)]+)\)\s*=\s*(?<SectionType>[^\r\n]+)(?<Content>.*?)EndGlobalSection",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);
public SolutionParser(ILogger<SolutionParser> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Parses a Visual Studio solution file and returns a structured model
/// </summary>
/// <param name="solutionPath">Path to the .sln file</param>
/// <returns>Parsed solution model</returns>
/// <exception cref="ArgumentException">Thrown when solution path is null or empty</exception>
/// <exception cref="FileNotFoundException">Thrown when solution file doesn't exist</exception>
/// <exception cref="InvalidOperationException">Thrown when solution file format is invalid</exception>
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);
}
}
/// <summary>
/// Extracts project references from a parsed solution model
/// </summary>
/// <param name="solution">The parsed solution model</param>
/// <returns>List of project references</returns>
/// <exception cref="ArgumentNullException">Thrown when solution is null</exception>
public List<ProjectReference> 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<ProjectReference>();
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;
}
/// <summary>
/// Parses project entries from solution file content
/// </summary>
private List<ProjectReference> ParseProjects(string solutionContent, string solutionPath)
{
var projects = new List<ProjectReference>();
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;
}
/// <summary>
/// Parses solution configurations from solution file content
/// </summary>
private List<SolutionConfiguration> ParseSolutionConfigurations(string solutionContent)
{
var configurations = new List<SolutionConfiguration>();
var seenConfigurations = new HashSet<string>();
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;
}
/// <summary>
/// Parses global sections from solution file content
/// </summary>
private Dictionary<string, string> ParseGlobalSections(string solutionContent)
{
var globalSections = new Dictionary<string, string>();
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;
}
/// <summary>
/// Determines if a GUID represents a project type (not a solution folder)
/// </summary>
private static bool IsProjectTypeGuid(string projectTypeGuid)
{
// Common project type GUIDs - this is not exhaustive but covers most cases
var knownProjectTypes = new HashSet<string>(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));
}
}
}