diff --git a/DotNetSolutionTools.App/Models/LocalStateDto.cs b/DotNetSolutionTools.App/Models/LocalStateDto.cs new file mode 100644 index 0000000..293176d --- /dev/null +++ b/DotNetSolutionTools.App/Models/LocalStateDto.cs @@ -0,0 +1,8 @@ +namespace DotNetSolutionTools.App.Models; + +public class LocalStateDto +{ + public string SolutionFolderPath { get; set; } = string.Empty; + public string SolutionFilePath { get; set; } = string.Empty; + public string CsprojFilePath { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotNetSolutionTools.App/ViewModels/MainWindowViewModel.cs b/DotNetSolutionTools.App/ViewModels/MainWindowViewModel.cs index 192c451..be799ac 100644 --- a/DotNetSolutionTools.App/ViewModels/MainWindowViewModel.cs +++ b/DotNetSolutionTools.App/ViewModels/MainWindowViewModel.cs @@ -1,6 +1,8 @@ using System.Collections.ObjectModel; +using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DotNetSolutionTools.App.Models; using DotNetSolutionTools.App.Services; using DotNetSolutionTools.Core; @@ -25,13 +27,21 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private async Task ExecuteParityChecker(CancellationToken token) { - var results = SolutionProjectParity.CompareSolutionAndCSharpProjects( - SolutionFolderPath, - SolutionFilePath - ); ParityResults.Clear(); - foreach (var result in results) - ParityResults.Add(result); + ErrorMessages?.Clear(); + try + { + var results = SolutionProjectParity.CompareSolutionAndCSharpProjects( + SolutionFolderPath, + SolutionFilePath + ); + foreach (var result in results) + ParityResults.Add(result); + } + catch (Exception e) + { + ErrorMessages?.Add(e.Message); + } } [RelayCommand] @@ -43,31 +53,57 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private async Task FormatAllCsprojFilesInSolutionFile(CancellationToken token) { - var csprojList = SolutionProjectParity.RetrieveAllCSharpProjectFullPathsFromFolder( - SolutionFolderPath - ); - foreach (var csproj in csprojList) + ErrorMessages?.Clear(); + try { - FormatCsproj.FormatCsprojFile(csproj); + var csprojList = SolutionProjectParity.RetrieveAllCSharpProjectFullPathsFromFolder( + SolutionFolderPath + ); + foreach (var csproj in csprojList) + { + FormatCsproj.FormatCsprojFile(csproj); + } + } + catch (Exception e) + { + ErrorMessages?.Add(e.Message); } } [RelayCommand] private async Task FormatAllCsprojFilesInSolutionFolder(CancellationToken token) { - var csprojList = SolutionProjectParity.RetrieveAllCSharpProjectFullPathsFromFolder( - SolutionFolderPath - ); - foreach (var csproj in csprojList) + ErrorMessages?.Clear(); + try { - FormatCsproj.FormatCsprojFile(csproj); + var csprojList = SolutionProjectParity.RetrieveAllCSharpProjectFullPathsFromFolder( + SolutionFolderPath + ); + foreach (var csproj in csprojList) + { + FormatCsproj.FormatCsprojFile(csproj); + } + } + catch (Exception e) + { + ErrorMessages?.Add(e.Message); } } [RelayCommand] private async Task CheckForMissingImplicitUsingsInSolutionFile(CancellationToken token) { - ImplicitUsings.FindCSharpProjectsMissingImplicitUsings(SolutionFilePath); + ErrorMessages?.Clear(); + ParityResults.Clear(); + try + { + var result = ImplicitUsings.FindCSharpProjectsMissingImplicitUsings(SolutionFilePath); + result.ForEach(s => ParityResults.Add(s)); + } + catch (Exception e) + { + ErrorMessages?.Add(e.Message); + } } [RelayCommand] @@ -81,6 +117,7 @@ public partial class MainWindowViewModel : ViewModelBase return; SolutionFilePath = file.Path.AbsolutePath; + await SaveLoadedState(); } catch (Exception e) { @@ -92,6 +129,7 @@ public partial class MainWindowViewModel : ViewModelBase private async Task ClearSolutionFile(CancellationToken token) { SolutionFilePath = string.Empty; + await SaveLoadedState(); } [RelayCommand] @@ -105,6 +143,7 @@ public partial class MainWindowViewModel : ViewModelBase return; SolutionFolderPath = folder.Path.AbsolutePath; + await SaveLoadedState(); } catch (Exception e) { @@ -116,6 +155,7 @@ public partial class MainWindowViewModel : ViewModelBase private async Task ClearSolutionFolder(CancellationToken token) { SolutionFolderPath = string.Empty; + await SaveLoadedState(); } [RelayCommand] @@ -129,6 +169,7 @@ public partial class MainWindowViewModel : ViewModelBase return; CsprojFilePath = folder.Path.AbsolutePath; + await SaveLoadedState(); } catch (Exception e) { @@ -140,5 +181,43 @@ public partial class MainWindowViewModel : ViewModelBase private async Task ClearCsprojFile(CancellationToken token) { CsprojFilePath = string.Empty; + await SaveLoadedState(); + } + + private async Task SaveLoadedState() + { + var dto = new LocalStateDto + { + SolutionFolderPath = SolutionFolderPath, + SolutionFilePath = SolutionFilePath, + CsprojFilePath = CsprojFilePath + }; + var json = JsonSerializer.Serialize(dto); + await File.WriteAllTextAsync("./localState.json", json); + } + + private async Task LoadSavedState() + { + try + { + var json = await File.ReadAllTextAsync("./localState.json"); + if (string.IsNullOrEmpty(json)) + return; + var dto = JsonSerializer.Deserialize(json); + if (dto is null) + return; + SolutionFolderPath = dto.SolutionFolderPath; + SolutionFilePath = dto.SolutionFilePath; + CsprojFilePath = dto.CsprojFilePath; + } + catch + { + // ignored + } + } + + public MainWindowViewModel() + { + LoadSavedState().ConfigureAwait(false); } } diff --git a/DotNetSolutionTools.CLI/Commands/TreatWarningsAsErrorsCommand.cs b/DotNetSolutionTools.CLI/Commands/TreatWarningsAsErrorsCommand.cs new file mode 100644 index 0000000..1e835d5 --- /dev/null +++ b/DotNetSolutionTools.CLI/Commands/TreatWarningsAsErrorsCommand.cs @@ -0,0 +1,66 @@ +using System.ComponentModel; +using DotNetSolutionTools.Core; +using Spectre.Console.Cli; + +namespace DotNetSolutionTools.CLI.Commands; + +public class TreatWarningsAsErrorsCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandArgument(1, "")] + public required string SolutionFilePath { get; set; } + + [CommandOption("-m|--add-missing")] + [DefaultValue(false)] + public bool AddMissing { get; set; } = false; + } + + public override int Execute(CommandContext context, Settings settings) + { + var pathToSolutionFile = settings.SolutionFilePath; + Console.WriteLine($"Retrieving Solution from {pathToSolutionFile}"); + + var solutionFile = SolutionProjectParity.ParseSolutionFileFromPath(pathToSolutionFile); + if (solutionFile == null) + { + Console.WriteLine( + "Failed to parse solution file. The file was either not found or malformed." + ); + return 1; + } + var cSharpProjects = SolutionProjectParity.GetCSharpProjectObjectsFromSolutionFile( + solutionFile + ); + Console.WriteLine($"Found {cSharpProjects.Count} C# Projects"); + Console.WriteLine("=================================================="); + + // Get the list of projects + var projectsMissingImplicitUsings = WarningsAsErrors.FindCSharpProjectsMissingTreatWarningsAsErrors( + cSharpProjects + ); + + Console.WriteLine( + $"{projectsMissingImplicitUsings.Count} C# Projects have missing Treat Warnings As Errors" + ); + + if (settings.AddMissing) + { + Console.WriteLine("=================================================="); + Console.WriteLine("Adding missing Warnings As Errors"); + WarningsAsErrors.AddMissingTreatWarningsAsErrors(projectsMissingImplicitUsings); + var updatedProjects = SolutionProjectParity.GetCSharpProjectObjectsFromSolutionFile( + solutionFile + ); + var projectsWithMissing = WarningsAsErrors.FindCSharpProjectsMissingTreatWarningsAsErrors( + updatedProjects + ); + Console.WriteLine( + $"There are now {projectsWithMissing.Count} C# Projects missing Treat Warnings As Errors" + ); + } + Console.WriteLine("=================================================="); + Console.WriteLine("Done!"); + return 0; + } +} diff --git a/DotNetSolutionTools.CLI/Program.cs b/DotNetSolutionTools.CLI/Program.cs index 0a871d1..1efe2a7 100644 --- a/DotNetSolutionTools.CLI/Program.cs +++ b/DotNetSolutionTools.CLI/Program.cs @@ -10,6 +10,7 @@ app.Configure(config => config.AddCommand("compare"); config.AddCommand("implicit-usings"); config.AddCommand("format-csproj"); + config.AddCommand("warnings-as-errors"); }); return await app.RunAsync(args); diff --git a/DotNetSolutionTools.Core/SolutionProjectParity.cs b/DotNetSolutionTools.Core/SolutionProjectParity.cs index 24a9dfa..38e6aa2 100644 --- a/DotNetSolutionTools.Core/SolutionProjectParity.cs +++ b/DotNetSolutionTools.Core/SolutionProjectParity.cs @@ -21,7 +21,6 @@ public static class SolutionProjectParity { var csprojList = RetrieveAllCSharpProjectFullPathsFromFolder(solutionFolderPath); - csprojList = csprojList.Select(x => x.Replace(solutionFolderPath, "")).ToArray(); return csprojList; } @@ -57,7 +56,7 @@ public static class SolutionProjectParity foreach (var project in csprojList) { - var projectInSolution = projects.FirstOrDefault(x => x.RelativePath == project); + var projectInSolution = projects.FirstOrDefault(x => NormalizePath(x.AbsolutePath) == NormalizePath(project)); if (projectInSolution == null) { @@ -79,4 +78,10 @@ public static class SolutionProjectParity return projectList; } + + private static string NormalizePath(string path) + { + return Path.GetFullPath(new Uri(path).LocalPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } } diff --git a/DotNetSolutionTools.Core/WarningsAsErrors.cs b/DotNetSolutionTools.Core/WarningsAsErrors.cs new file mode 100644 index 0000000..414e96e --- /dev/null +++ b/DotNetSolutionTools.Core/WarningsAsErrors.cs @@ -0,0 +1,78 @@ +using Microsoft.Build.Construction; + +namespace DotNetSolutionTools.Core; + +public static class WarningsAsErrors +{ + public static List FindCSharpProjectsMissingTreatWarningsAsErrors(string solutionFilePath) + { + var solutionFile = SolutionFile.Parse(solutionFilePath); + var csprojList = SolutionProjectParity.GetCSharpProjectObjectsFromSolutionFile( + solutionFile + ); + var projectsMissingImplicitUsings = FindCSharpProjectsMissingTreatWarningsAsErrors(csprojList); + var projectsMissingImplicitUsingsStringList = projectsMissingImplicitUsings + .Select(x => x.FullPath) + .ToList(); + + return projectsMissingImplicitUsingsStringList; + } + + public static List FindCSharpProjectsMissingTreatWarningsAsErrors( + List projectList + ) + { + var projectsMissingTreatWarningsAsErrors = new List(); + + foreach (var project in projectList) + { + var treatWarningsAsErrors = project.PropertyGroups + .SelectMany(x => x.Properties) + .FirstOrDefault(x => x.Name == "TreatWarningsAsErrors"); + if (treatWarningsAsErrors is null || treatWarningsAsErrors.Value is not "true") + { + projectsMissingTreatWarningsAsErrors.Add(project); + } + } + + return projectsMissingTreatWarningsAsErrors; + } + + public static void AddMissingTreatWarningsAsErrors( + List projectsMissingImplicitUsings + ) + { + foreach (var project in projectsMissingImplicitUsings) + { + if (ProjectIsMissingTreatWarningsAsErrors(project)) + { + + project.AddProperty("TreatWarningsAsErrors", "true"); + project.Save(); + FormatCsproj.FormatCsprojFile(project.FullPath); + } + } + } + + public static bool ProjectIsMissingTreatWarningsAsErrors(ProjectRootElement project) + { + var implicitUsings = project.PropertyGroups + .SelectMany(x => x.Properties) + .FirstOrDefault(x => x.Name == "TreatWarningsAsErrors"); + if (implicitUsings is null) + { + return true; + } + + return false; + } + + public static bool ProjectBuildSuccessfully(ProjectRootElement project) + { + // build the project + var buildProject = new Microsoft.Build.Evaluation.Project(project); + // retrieve warnings + var buildResult = buildProject.Build(); + return buildResult; + } +}