509 lines
16 KiB
C#
Executable File
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 |