274 lines
12 KiB
C#
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));
|
|
}
|
|
}
|
|
} |