From 526ff1b43d9726b8076b93ce2065e1ae66ee9084 Mon Sep 17 00:00:00 2001
From: "Matthew Parker [SSW]" <61717342+MattParkerDev@users.noreply.github.com>
Date: Wed, 30 Aug 2023 18:59:41 +1000
Subject: [PATCH] Add implicit usings analyzer and csproj formatter
---
.../ViewModels/MainWindowViewModel.cs | 6 ++
.../Views/MainWindow.axaml | 1 +
.../Commands/FormatCsprojCommand.cs | 26 ++++++
.../Commands/ImplicitUsingsCommand.cs | 83 +++++++++++++++++++
SolutionParityChecker.CLI/Program.cs | 2 +
SolutionParityChecker/FormatCsproj.cs | 37 +++++++++
SolutionParityChecker/ImplicitUsings.cs | 60 ++++++++++++++
.../SolutionParityChecker.cs | 11 +++
8 files changed, 226 insertions(+)
create mode 100644 SolutionParityChecker.CLI/Commands/FormatCsprojCommand.cs
create mode 100644 SolutionParityChecker.CLI/Commands/ImplicitUsingsCommand.cs
create mode 100644 SolutionParityChecker/FormatCsproj.cs
create mode 100644 SolutionParityChecker/ImplicitUsings.cs
diff --git a/SolutionParityChecker.App/ViewModels/MainWindowViewModel.cs b/SolutionParityChecker.App/ViewModels/MainWindowViewModel.cs
index c56147a..acc706d 100644
--- a/SolutionParityChecker.App/ViewModels/MainWindowViewModel.cs
+++ b/SolutionParityChecker.App/ViewModels/MainWindowViewModel.cs
@@ -43,6 +43,12 @@ public partial class MainWindowViewModel : ViewModelBase
ParityResults.Add(result);
}
}
+
+ [RelayCommand]
+ private async Task FormatCsProjFile(CancellationToken token)
+ {
+ FormatCsproj.FormatCsprojFile(SolutionFilePath);
+ }
[RelayCommand]
private async Task LoadSolutionFile(CancellationToken token)
diff --git a/SolutionParityChecker.App/Views/MainWindow.axaml b/SolutionParityChecker.App/Views/MainWindow.axaml
index aac9435..4b0655f 100644
--- a/SolutionParityChecker.App/Views/MainWindow.axaml
+++ b/SolutionParityChecker.App/Views/MainWindow.axaml
@@ -19,6 +19,7 @@
+
diff --git a/SolutionParityChecker.CLI/Commands/FormatCsprojCommand.cs b/SolutionParityChecker.CLI/Commands/FormatCsprojCommand.cs
new file mode 100644
index 0000000..836aa7d
--- /dev/null
+++ b/SolutionParityChecker.CLI/Commands/FormatCsprojCommand.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace SolutionParityChecker.CLI.Commands;
+
+public class FormatCsprojCommand : Command
+{
+ public sealed class Settings : CommandSettings
+ {
+ [CommandArgument(1, "")]
+ public required string CsprojFilePath { get; set; }
+
+ [CommandOption("-a|--enable-all")]
+ [Description("true to enable logging of all project files. Default is false.")]
+ [DefaultValue(false)]
+ public bool EnableAll { get; set; } = false;
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ var pathToCsprojFile = settings.CsprojFilePath;
+ Console.WriteLine($"Retrieving C# Project from {pathToCsprojFile}");
+ FormatCsproj.FormatCsprojFile(pathToCsprojFile);
+ return 0;
+ }
+}
diff --git a/SolutionParityChecker.CLI/Commands/ImplicitUsingsCommand.cs b/SolutionParityChecker.CLI/Commands/ImplicitUsingsCommand.cs
new file mode 100644
index 0000000..a2b137f
--- /dev/null
+++ b/SolutionParityChecker.CLI/Commands/ImplicitUsingsCommand.cs
@@ -0,0 +1,83 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace SolutionParityChecker.CLI.Commands;
+
+public class ImplicitUsingsCommand : Command
+{
+ public sealed class Settings : CommandSettings
+ {
+ [CommandArgument(1, "")]
+ public required string SolutionFilePath { get; set; }
+
+ [CommandOption("-m|--add-missing")]
+ [Description("Add Implicit Usings=true to projects missing them. Default is false.")]
+ [DefaultValue(false)]
+ public bool AddMissing { get; set; } = false;
+
+ [CommandOption("-d|--enable-disabled")]
+ [Description("Sets Implicit Usings to true for any projects with it disabled. Default is false.")]
+ [DefaultValue(false)]
+ public bool EnableDisabled { get; set; } = false;
+
+ [CommandOption("-a|--enable-all")]
+ [Description("Enables Implicit Usings for all projects. Default is false.")]
+ [DefaultValue(false)]
+ public bool EnableAll { get; set; } = false;
+ }
+
+ public override int Execute(CommandContext context, Settings settings)
+ {
+ var pathToSolutionFile = settings.SolutionFilePath;
+ Console.WriteLine($"Retrieving Solution from {pathToSolutionFile}");
+
+ var solutionFile = SolutionParityChecker.ParseSolutionFileFromPath(pathToSolutionFile);
+ if (solutionFile == null)
+ {
+ Console.WriteLine(
+ "Failed to parse solution file. The file was either not found or malformed."
+ );
+ return 1;
+ }
+ var cSharpProjects = SolutionParityChecker.GetCSharpProjectObjectsFromSolutionFile(solutionFile);
+ Console.WriteLine($"Found {cSharpProjects.Count} C# Projects");
+ Console.WriteLine("==================================================");
+
+ // Get the list of projects
+ var projectsMissingImplicitUsings = ImplicitUsings.FindCSharpProjectsMissingImplicitUsings(cSharpProjects);
+
+ Console.WriteLine(
+ $"{projectsMissingImplicitUsings.Count} C# Projects have missing or disabled implicit usings"
+ );
+
+ foreach (var project in projectsMissingImplicitUsings)
+ {
+ Console.WriteLine(project.DirectoryPath);
+ }
+
+ if (settings.AddMissing)
+ {
+ Console.WriteLine("==================================================");
+ Console.WriteLine("Adding missing implicit usings");
+ ImplicitUsings.AddMissingImplicitUsings(projectsMissingImplicitUsings);
+ var updatedProjects = SolutionParityChecker.GetCSharpProjectObjectsFromSolutionFile(solutionFile);
+ var projectsWithMissing = ImplicitUsings.FindCSharpProjectsMissingImplicitUsings(updatedProjects);
+ Console.WriteLine($"There are now {projectsWithMissing.Count} C# Projects missing/disabled implicit usings");
+ }
+ if (settings.EnableDisabled)
+ {
+ Console.WriteLine("==================================================");
+ Console.WriteLine("Enabling disabled implicit usings");
+ ImplicitUsings.EnableDisabledImplicitUsings(projectsMissingImplicitUsings);
+ }
+ if (settings.EnableAll)
+ {
+ Console.WriteLine("==================================================");
+ Console.WriteLine("Enabling all implicit usings");
+ ImplicitUsings.EnableAllImplicitUsings(projectsMissingImplicitUsings);
+ }
+ Console.WriteLine("==================================================");
+ Console.WriteLine("Done!");
+ return 0;
+ }
+}
diff --git a/SolutionParityChecker.CLI/Program.cs b/SolutionParityChecker.CLI/Program.cs
index d1ed55b..23effaa 100644
--- a/SolutionParityChecker.CLI/Program.cs
+++ b/SolutionParityChecker.CLI/Program.cs
@@ -8,6 +8,8 @@ app.Configure(config =>
config.ValidateExamples();
config.AddCommand("compare");
+ config.AddCommand("implicit-usings");
+ config.AddCommand("format-csproj");
});
return await app.RunAsync(args);
diff --git a/SolutionParityChecker/FormatCsproj.cs b/SolutionParityChecker/FormatCsproj.cs
new file mode 100644
index 0000000..8083fe4
--- /dev/null
+++ b/SolutionParityChecker/FormatCsproj.cs
@@ -0,0 +1,37 @@
+using System.Text;
+using System.Xml;
+
+namespace SolutionParityChecker;
+
+public static class FormatCsproj
+{
+ public static void FormatCsprojFile(string csprojFilePath)
+ {
+ using TextReader rd = new StreamReader(csprojFilePath, Encoding.Default);
+
+ XmlDocument doc = new XmlDocument();
+ doc.Load(rd);
+
+ if (rd != Console.In)
+ {
+ rd.Close();
+ }
+
+ using var wr = new StreamWriter(csprojFilePath, false, Encoding.Default);
+
+ var settings =
+ new XmlWriterSettings
+ {
+ Indent = true,
+ IndentChars = "\t",
+ NewLineOnAttributes = false,
+ OmitXmlDeclaration = true
+ };
+
+ using (var writer = XmlWriter.Create(wr, settings))
+ {
+ doc.WriteContentTo(writer);
+ writer.Close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SolutionParityChecker/ImplicitUsings.cs b/SolutionParityChecker/ImplicitUsings.cs
new file mode 100644
index 0000000..d06021b
--- /dev/null
+++ b/SolutionParityChecker/ImplicitUsings.cs
@@ -0,0 +1,60 @@
+using Microsoft.Build.Construction;
+
+namespace SolutionParityChecker;
+
+public static class ImplicitUsings
+{
+ public static List FindCSharpProjectsMissingImplicitUsings(List projectList)
+ {
+ var projectsMissingImplicitUsings = new List();
+
+ foreach (var project in projectList)
+ {
+ var implicitUsings = project.PropertyGroups
+ .SelectMany(x => x.Properties)
+ .FirstOrDefault(x => x.Name == "ImplicitUsings");
+ if (implicitUsings is null || implicitUsings.Value is not "enable")
+ {
+ projectsMissingImplicitUsings.Add(project);
+ }
+ }
+
+ return projectsMissingImplicitUsings;
+ }
+
+ public static void AddMissingImplicitUsings(List projectsMissingImplicitUsings)
+ {
+ foreach (var project in projectsMissingImplicitUsings)
+ {
+ if (ProjectIsMissingImplicitUsings(project))
+ {
+ project.AddProperty("ImplicitUsings", "enable");
+ project.Save();
+ FormatCsproj.FormatCsprojFile(project.FullPath);
+ }
+ }
+ }
+
+ public static void EnableDisabledImplicitUsings(List projectsMissingImplicitUsings)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void EnableAllImplicitUsings(List projectsMissingImplicitUsings)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static bool ProjectIsMissingImplicitUsings(ProjectRootElement project)
+ {
+ var implicitUsings = project.PropertyGroups
+ .SelectMany(x => x.Properties)
+ .FirstOrDefault(x => x.Name == "ImplicitUsings");
+ if (implicitUsings is null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/SolutionParityChecker/SolutionParityChecker.cs b/SolutionParityChecker/SolutionParityChecker.cs
index 5f5e338..c71f4f2 100644
--- a/SolutionParityChecker/SolutionParityChecker.cs
+++ b/SolutionParityChecker/SolutionParityChecker.cs
@@ -60,4 +60,15 @@ public static class SolutionParityChecker
return projectsMissingFromSolution;
}
+ public static List GetCSharpProjectObjectsFromSolutionFile(
+ SolutionFile solutionFile
+ )
+ {
+ var projectList = solutionFile.ProjectsByGuid
+ .Where(x => x.Value.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat)
+ .Select(s => ProjectRootElement.Open(s.Value.AbsolutePath))
+ .ToList();
+
+ return projectList;
+ }
}