MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Refacto.../MauiAwareSolutionScanner.cs

509 lines
16 KiB
C#
Executable File

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using MarketAlly.ProjectDetector;
namespace MarketAlly.AIPlugin.Refactoring.SolutionConsole;
public class MauiAwareSolutionScanner
{
public async Task<SolutionStructure> AnalyzeSolution(string solutionPath)
{
var structure = new SolutionStructure
{
SolutionPath = solutionPath,
SolutionName = Path.GetFileName(solutionPath)
};
// Find .sln files
var solutionFiles = Directory.GetFiles(solutionPath, "*.sln", SearchOption.TopDirectoryOnly);
if (solutionFiles.Any())
{
structure.SolutionFile = solutionFiles.First();
await ParseSolutionFile(structure);
}
// Discover all project files
await DiscoverProjects(structure);
// Analyze MAUI-specific structure
await AnalyzeMauiProjects(structure);
return structure;
}
private async Task ParseSolutionFile(SolutionStructure structure)
{
try
{
var solutionContent = await File.ReadAllTextAsync(structure.SolutionFile);
var lines = solutionContent.Split('\n');
foreach (var line in lines)
{
// Parse project entries: Project("{GUID}") = "ProjectName", "ProjectPath", "{ProjectGUID}"
var projectMatch = Regex.Match(line, @"Project\(""([^""]+)""\)\s*=\s*""([^""]+)"",\s*""([^""]+)"",\s*""([^""]+)""");
if (projectMatch.Success)
{
var projectTypeGuid = projectMatch.Groups[1].Value;
var projectName = projectMatch.Groups[2].Value;
var projectPath = projectMatch.Groups[3].Value;
var projectGuid = projectMatch.Groups[4].Value;
var solutionProject = new SolutionProject
{
Name = projectName,
RelativePath = projectPath,
FullPath = Path.Combine(Path.GetDirectoryName(structure.SolutionFile)!, projectPath),
ProjectTypeGuid = projectTypeGuid,
ProjectGuid = projectGuid,
ProjectType = DetermineProjectType(projectTypeGuid, projectPath)
};
structure.SolutionProjects.Add(solutionProject);
}
}
Console.WriteLine($"[SOLUTION ANALYSIS] Found {structure.SolutionProjects.Count} projects in solution file");
}
catch (Exception ex)
{
Console.WriteLine($"[WARNING] Failed to parse solution file: {ex.Message}");
}
}
private async Task DiscoverProjects(SolutionStructure structure)
{
// Find all .csproj files in the directory tree
var allProjectFiles = Directory.GetFiles(structure.SolutionPath, "*.csproj", SearchOption.AllDirectories);
foreach (var projectFile in allProjectFiles)
{
// Check if this project is already in the solution file
var existingProject = structure.SolutionProjects.FirstOrDefault(p =>
Path.GetFullPath(p.FullPath).Equals(Path.GetFullPath(projectFile), StringComparison.OrdinalIgnoreCase));
if (existingProject != null)
{
// Enhance existing project with file system info
await AnalyzeProjectFile(existingProject);
}
else
{
// This is a project not listed in the solution file (orphaned)
var orphanedProject = new SolutionProject
{
Name = Path.GetFileNameWithoutExtension(projectFile),
FullPath = projectFile,
RelativePath = Path.GetRelativePath(structure.SolutionPath, projectFile),
ProjectType = ProjectType.Other,
IsOrphaned = true
};
await AnalyzeProjectFile(orphanedProject);
structure.OrphanedProjects.Add(orphanedProject);
}
}
Console.WriteLine($"[DISCOVERY] Found {allProjectFiles.Length} total .csproj files");
Console.WriteLine($"[DISCOVERY] {structure.OrphanedProjects.Count} orphaned projects (not in .sln)");
}
private async Task AnalyzeProjectFile(SolutionProject project)
{
try
{
if (!File.Exists(project.FullPath))
{
project.HasIssues = true;
project.Issues.Add($"Project file not found: {project.FullPath}");
return;
}
var projectContent = await File.ReadAllTextAsync(project.FullPath);
var projectXml = XDocument.Parse(projectContent);
// Analyze project properties
var properties = projectXml.Descendants("PropertyGroup").Elements().ToList();
foreach (var prop in properties)
{
project.Properties[prop.Name.LocalName] = prop.Value;
}
// Determine if it's a MAUI project
await AnalyzeMauiSpecificProperties(project, projectXml);
// Find C# files
var projectDir = Path.GetDirectoryName(project.FullPath)!;
var csFiles = Directory.GetFiles(projectDir, "*.cs", SearchOption.AllDirectories);
project.CSharpFiles = csFiles.Where(f => !ShouldExcludeFile(f, project)).ToList();
project.TotalFiles = csFiles.Length;
project.ProcessableFiles = project.CSharpFiles.Count;
// Check for MAUI platform folders
await AnalyzeMauiPlatformStructure(project, projectDir);
}
catch (Exception ex)
{
project.HasIssues = true;
project.Issues.Add($"Failed to analyze project: {ex.Message}");
}
}
private async Task AnalyzeMauiSpecificProperties(SolutionProject project, XDocument projectXml)
{
// Check for MAUI-specific properties
var isMaui = project.Properties.ContainsKey("UseMaui") &&
project.Properties["UseMaui"].Equals("true", StringComparison.OrdinalIgnoreCase);
if (isMaui)
{
project.ProjectType = ProjectType.MobileApp;
project.MauiInfo = new MauiProjectInfo();
// Analyze MAUI-specific settings
if (project.Properties.ContainsKey("TargetFrameworks"))
{
project.MauiInfo.TargetFrameworks = project.Properties["TargetFrameworks"]
.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(tf => tf.Trim())
.ToList();
}
// Check for platform-specific configurations
var itemGroups = projectXml.Descendants("ItemGroup");
foreach (var itemGroup in itemGroups)
{
var condition = itemGroup.Attribute("Condition")?.Value;
if (!string.IsNullOrEmpty(condition))
{
project.MauiInfo.PlatformSpecificConfigurations.Add(condition);
}
}
// Check for MAUI dependencies
var packageReferences = projectXml.Descendants("PackageReference");
foreach (var package in packageReferences)
{
var packageName = package.Attribute("Include")?.Value;
if (packageName != null && IsMauiRelatedPackage(packageName))
{
project.MauiInfo.MauiPackages.Add(packageName);
}
}
Console.WriteLine($"[MAUI] Detected MAUI project: {project.Name}");
Console.WriteLine($" Target Frameworks: {string.Join(", ", project.MauiInfo.TargetFrameworks)}");
}
await Task.CompletedTask;
}
private async Task AnalyzeMauiPlatformStructure(SolutionProject project, string projectDir)
{
if (project.ProjectType != ProjectType.MobileApp)
return;
// Check for MAUI platform folders
var platformFolders = new[] { "Platforms", "Platform" };
var specificPlatforms = new[] { "Android", "iOS", "MacCatalyst", "Windows", "Tizen" };
foreach (var platformFolder in platformFolders)
{
var platformPath = Path.Combine(projectDir, platformFolder);
if (Directory.Exists(platformPath))
{
project.MauiInfo!.HasPlatformsFolder = true;
// Check for specific platform folders
foreach (var platform in specificPlatforms)
{
var specificPlatformPath = Path.Combine(platformPath, platform);
if (Directory.Exists(specificPlatformPath))
{
var platformFiles = Directory.GetFiles(specificPlatformPath, "*.cs", SearchOption.AllDirectories);
project.MauiInfo.PlatformSpecificFiles[platform] = platformFiles.ToList();
Console.WriteLine($"[MAUI] Found {platform} platform code: {platformFiles.Length} files");
}
}
}
}
await Task.CompletedTask;
}
private async Task AnalyzeMauiProjects(SolutionStructure structure)
{
var mauiProjects = structure.SolutionProjects
.Where(p => p.ProjectType == ProjectType.MobileApp)
.ToList();
if (!mauiProjects.Any())
return;
Console.WriteLine($"\n[MAUI ANALYSIS] Found {mauiProjects.Count} MAUI project(s)");
foreach (var mauiProject in mauiProjects)
{
Console.WriteLine($"\n[MAUI PROJECT] {mauiProject.Name}");
if (mauiProject.MauiInfo != null)
{
Console.WriteLine($" Target Frameworks: {string.Join(", ", mauiProject.MauiInfo.TargetFrameworks)}");
Console.WriteLine($" MAUI Packages: {mauiProject.MauiInfo.MauiPackages.Count}");
Console.WriteLine($" Platform Folders: {(mauiProject.MauiInfo.HasPlatformsFolder ? " Yes" : " No")}");
if (mauiProject.MauiInfo.PlatformSpecificFiles.Any())
{
Console.WriteLine(" Platform-Specific Code:");
foreach (var platform in mauiProject.MauiInfo.PlatformSpecificFiles)
{
Console.WriteLine($" {platform.Key}: {platform.Value.Count} files");
}
}
// Analyze refactoring recommendations for MAUI
await GenerateMauiRefactoringRecommendations(mauiProject);
}
}
await Task.CompletedTask;
}
private async Task GenerateMauiRefactoringRecommendations(SolutionProject mauiProject)
{
mauiProject.MauiInfo!.RefactoringRecommendations = new List<string>();
// Check for common MAUI anti-patterns
if (mauiProject.MauiInfo.TargetFrameworks.Count > 3)
{
mauiProject.MauiInfo.RefactoringRecommendations.Add(
"Consider reducing target frameworks - too many platforms can complicate maintenance");
}
if (!mauiProject.MauiInfo.HasPlatformsFolder && mauiProject.MauiInfo.TargetFrameworks.Count > 1)
{
mauiProject.MauiInfo.RefactoringRecommendations.Add(
"Consider using Platforms folder structure for platform-specific code organization");
}
// Check for large shared code files that might need platform-specific splitting
var sharedFiles = mauiProject.CSharpFiles
.Where(f => !IsInPlatformFolder(f))
.ToList();
var largeSharedFiles = new List<string>();
foreach (var file in sharedFiles)
{
try
{
var content = await File.ReadAllTextAsync(file);
var lineCount = content.Split('\n').Length;
if (lineCount > 200)
{
largeSharedFiles.Add(Path.GetFileName(file));
}
}
catch
{
// Skip files that can't be read
}
}
if (largeSharedFiles.Any())
{
mauiProject.MauiInfo.RefactoringRecommendations.Add(
$"Large shared files detected ({string.Join(", ", largeSharedFiles)}) - consider extracting platform-specific logic");
}
// Check for dependency injection setup
var mainFiles = mauiProject.CSharpFiles
.Where(f => Path.GetFileName(f).Contains("Main", StringComparison.OrdinalIgnoreCase) ||
Path.GetFileName(f).Contains("App", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var mainFile in mainFiles)
{
try
{
var content = await File.ReadAllTextAsync(mainFile);
if (content.Contains("ConfigureServices") && content.Length > 500)
{
mauiProject.MauiInfo.RefactoringRecommendations.Add(
$"Large service configuration in {Path.GetFileName(mainFile)} - consider extracting to separate configuration class");
}
}
catch
{
// Skip files that can't be read
}
}
if (mauiProject.MauiInfo.RefactoringRecommendations.Any())
{
Console.WriteLine(" 🔧 Refactoring Recommendations:");
foreach (var recommendation in mauiProject.MauiInfo.RefactoringRecommendations)
{
Console.WriteLine($" • {recommendation}");
}
}
else
{
Console.WriteLine(" ✅ No specific MAUI refactoring issues detected");
}
}
private bool IsInPlatformFolder(string filePath)
{
var pathParts = filePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return pathParts.Any(part =>
part.Equals("Platforms", StringComparison.OrdinalIgnoreCase) ||
part.Equals("Platform", StringComparison.OrdinalIgnoreCase) ||
part.Equals("Android", StringComparison.OrdinalIgnoreCase) ||
part.Equals("iOS", StringComparison.OrdinalIgnoreCase) ||
part.Equals("MacCatalyst", StringComparison.OrdinalIgnoreCase) ||
part.Equals("Windows", StringComparison.OrdinalIgnoreCase) ||
part.Equals("Tizen", StringComparison.OrdinalIgnoreCase));
}
private bool IsMauiRelatedPackage(string packageName)
{
var mauiPackages = new[]
{
"Microsoft.Maui",
"Microsoft.Maui.Controls",
"Microsoft.Maui.Graphics",
"Microsoft.Maui.Essentials",
"Microsoft.Extensions.Logging.Debug",
"CommunityToolkit.Maui"
};
return mauiPackages.Any(mp => packageName.StartsWith(mp, StringComparison.OrdinalIgnoreCase));
}
private ProjectType DetermineProjectType(string projectTypeGuid, string projectPath)
{
// Standard Visual Studio project type GUIDs
return projectTypeGuid.ToUpper() switch
{
"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}" => ProjectType.Library, // .NET SDK-style project
"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" => ProjectType.Library, // Legacy C# project
"{2150E333-8FDC-42A3-9474-1A3956D46DE8}" => ProjectType.Solution, // Solution folder
"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}" => ProjectType.Other, // C++ project
_ => DetermineProjectTypeFromPath(projectPath)
};
}
private ProjectType DetermineProjectTypeFromPath(string projectPath)
{
var fileName = Path.GetFileName(projectPath).ToLower();
if (fileName.Contains("test"))
return ProjectType.Tests;
else if (fileName.Contains("benchmark"))
return ProjectType.Tests; // Benchmark projects are a type of test
else if (fileName.Contains("tool"))
return ProjectType.Tool;
else if (fileName.Contains("maui"))
return ProjectType.MobileApp;
else
return ProjectType.Library;
}
private bool ShouldExcludeFile(string filePath, SolutionProject project)
{
var fileName = Path.GetFileName(filePath);
var excludePatterns = new[]
{
".Designer.cs", ".generated.cs", ".g.cs", "AssemblyInfo.cs",
"GlobalAssemblyInfo.cs", "Reference.cs", "TemporaryGeneratedFile",
".AssemblyAttributes.cs"
};
if (excludePatterns.Any(pattern => fileName.Contains(pattern, StringComparison.OrdinalIgnoreCase)))
return true;
// MAUI-specific exclusions
if (project.ProjectType == ProjectType.MobileApp)
{
var mauiExclusions = new[]
{
"MauiProgram.cs", // Usually minimal and platform-specific
"App.xaml.cs", // Usually minimal
"AppShell.xaml.cs", // Usually minimal
"MainPage.xaml.cs" // Usually minimal unless it's grown too large
};
// Only exclude these if they're small (< 50 lines)
if (mauiExclusions.Any(exclusion => fileName.Equals(exclusion, StringComparison.OrdinalIgnoreCase)))
{
try
{
var content = File.ReadAllText(filePath);
var lineCount = content.Split('\n').Length;
if (lineCount < 50)
{
return true; // Skip small MAUI infrastructure files
}
}
catch
{
// If we can't read the file, don't exclude it
}
}
}
return false;
}
}
// Supporting classes for MAUI-aware solution analysis
public class SolutionStructure
{
public string SolutionPath { get; set; } = string.Empty;
public string SolutionName { get; set; } = string.Empty;
public string SolutionFile { get; set; } = string.Empty;
public List<SolutionProject> SolutionProjects { get; set; } = new List<SolutionProject>();
public List<SolutionProject> OrphanedProjects { get; set; } = new List<SolutionProject>();
public Dictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
}
public class SolutionProject
{
public string Name { get; set; } = string.Empty;
public string FullPath { get; set; } = string.Empty;
public string RelativePath { get; set; } = string.Empty;
public string ProjectTypeGuid { get; set; } = string.Empty;
public string ProjectGuid { get; set; } = string.Empty;
public ProjectType ProjectType { get; set; }
public bool IsOrphaned { get; set; }
public bool HasIssues { get; set; }
public List<string> Issues { get; set; } = new List<string>();
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
public List<string> CSharpFiles { get; set; } = new List<string>();
public int TotalFiles { get; set; }
public int ProcessableFiles { get; set; }
public MauiProjectInfo? MauiInfo { get; set; }
}
public class MauiProjectInfo
{
public List<string> TargetFrameworks { get; set; } = new List<string>();
public List<string> MauiPackages { get; set; } = new List<string>();
public List<string> PlatformSpecificConfigurations { get; set; } = new List<string>();
public Dictionary<string, List<string>> PlatformSpecificFiles { get; set; } = new Dictionary<string, List<string>>();
public bool HasPlatformsFolder { get; set; }
public List<string> RefactoringRecommendations { get; set; } = new List<string>();
}
// Using ProjectType from MarketAlly.ProjectDetector