diff --git a/README.md b/README.md index 6171a4d..c1f2f17 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ _Examine a specific project or solution to make sure there are no pre-release pa > snitch MyProject.csproj --no-prerelease ``` +_Examine a specific project or solution and export the result to a json file + +``` +> snitch MyProject.csproj --out c:\temp\snitch.json +``` + +That json file can be also used e.g. to auto-uninstall the detected packages in package manager console in Visual Studio: +``` +function Uninstall-FromSnitch { param($filename) (Get-Content $filename | ConvertFrom-Json) | %{ $p = $_.Project; foreach ($c in $_.CanBeRemoved) { Uninstall-Package $c.PackageName -ProjectName $p } } } +Uninstall-FromSnitch C:\temp\snitch.json +``` + + ## Building Snitch from source ``` diff --git a/src/Snitch.Tests.Fixtures/FooBar/Class1.cs b/src/Snitch.Tests.Fixtures/FooBar/Class1.cs new file mode 100644 index 0000000..8b7292a --- /dev/null +++ b/src/Snitch.Tests.Fixtures/FooBar/Class1.cs @@ -0,0 +1,8 @@ +using System; + +namespace Baz +{ + public class Class1 + { + } +} diff --git a/src/Snitch.Tests.Fixtures/FooBar/FooBar.csproj b/src/Snitch.Tests.Fixtures/FooBar/FooBar.csproj new file mode 100644 index 0000000..6b9d0d9 --- /dev/null +++ b/src/Snitch.Tests.Fixtures/FooBar/FooBar.csproj @@ -0,0 +1,8 @@ + + + + Silverlight + v5.0 + + + diff --git a/src/Snitch.Tests/Expectations/Baz.Json.verified.json b/src/Snitch.Tests/Expectations/Baz.Json.verified.json new file mode 100644 index 0000000..9c2697a --- /dev/null +++ b/src/Snitch.Tests/Expectations/Baz.Json.verified.json @@ -0,0 +1,14 @@ +[ + { + "Project": "Baz", + "CanBeRemoved": [ + { + "PackageName": "Autofac", + "PackageVersion": "4.9.4", + "ReferencedBy": "Foo" + } + ], + "MightBeRemoved": [], + "PreRelease": [] + } +] \ No newline at end of file diff --git a/src/Snitch.Tests/Expectations/FooBar.Default.verified.txt b/src/Snitch.Tests/Expectations/FooBar.Default.verified.txt new file mode 100644 index 0000000..4b61be2 --- /dev/null +++ b/src/Snitch.Tests/Expectations/FooBar.Default.verified.txt @@ -0,0 +1,6 @@ +Analyzing... +Analyzing FooBar.csproj +Analyzing FooBar... + ERROR: Value cannot be null. (Parameter 'folderName') + +Everything looks good! \ No newline at end of file diff --git a/src/Snitch.Tests/Expectations/FooBar.Strict.verified.txt b/src/Snitch.Tests/Expectations/FooBar.Strict.verified.txt new file mode 100644 index 0000000..c0a2d6a --- /dev/null +++ b/src/Snitch.Tests/Expectations/FooBar.Strict.verified.txt @@ -0,0 +1,4 @@ +Analyzing... +Analyzing FooBar.csproj +Analyzing FooBar... + ERROR: Value cannot be null. (Parameter 'folderName') \ No newline at end of file diff --git a/src/Snitch.Tests/Expectations/Solution.Json.verified.json b/src/Snitch.Tests/Expectations/Solution.Json.verified.json new file mode 100644 index 0000000..3c60f28 --- /dev/null +++ b/src/Snitch.Tests/Expectations/Solution.Json.verified.json @@ -0,0 +1,87 @@ +[ + { + "Project": "Bar", + "CanBeRemoved": [ + { + "PackageName": "Autofac", + "PackageVersion": "4.9.4", + "ReferencedBy": "Foo" + } + ], + "MightBeRemoved": [], + "PreRelease": [] + }, + { + "Project": "Baz", + "CanBeRemoved": [ + { + "PackageName": "Autofac", + "PackageVersion": "4.9.4", + "ReferencedBy": "Foo" + } + ], + "MightBeRemoved": [], + "PreRelease": [] + }, + { + "Project": "Qux", + "CanBeRemoved": [], + "MightBeRemoved": [ + { + "PackageName": "Autofac", + "PackageVersion": "4.9.3", + "ReferencedBy": "Foo", + "ReferencePackageVersion": "4.9.4" + } + ], + "PreRelease": [] + }, + { + "Project": "Zap", + "CanBeRemoved": [], + "MightBeRemoved": [ + { + "PackageName": "Newtonsoft.Json", + "PackageVersion": "12.0.3", + "ReferencedBy": "Foo", + "ReferencePackageVersion": "12.0.1" + }, + { + "PackageName": "Autofac", + "PackageVersion": "4.9.3", + "ReferencedBy": "Foo", + "ReferencePackageVersion": "4.9.4" + } + ], + "PreRelease": [] + }, + { + "Project": "Thud", + "CanBeRemoved": [], + "MightBeRemoved": [], + "PreRelease": [ + { + "PackageName": "Newtonsoft.Json", + "PackageVersion": "13.0.2-beta2" + } + ] + }, + { + "Project": "Thuuud", + "CanBeRemoved": [], + "MightBeRemoved": [ + { + "PackageName": "Newtonsoft.Json", + "PackageVersion": "13.0.2-beta2", + "ReferencedBy": "Foo", + "ReferencePackageVersion": "12.0.1" + } + ], + "PreRelease": [ + { + "PackageName": "Newtonsoft.Json", + "PackageVersion": "13.0.2-beta2" + } + ] + } +] \ No newline at end of file diff --git a/src/Snitch.Tests/ProgramTests.cs b/src/Snitch.Tests/ProgramTests.cs index f264bcd..117987f 100644 --- a/src/Snitch.Tests/ProgramTests.cs +++ b/src/Snitch.Tests/ProgramTests.cs @@ -1,5 +1,4 @@ using Shouldly; -using Snitch; using System; using System.IO; using System.Threading.Tasks; @@ -9,7 +8,7 @@ using Xunit; using VerifyXunit; -namespace Sntich.Tests +namespace Snitch.Tests { [UsesVerify] public class ProgramTests @@ -19,7 +18,6 @@ public class ProgramTests public async Task Should_Return_Expected_Result_For_Baz_Not_Specifying_Framework() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -30,12 +28,27 @@ public async Task Should_Return_Expected_Result_For_Baz_Not_Specifying_Framework await Verifier.Verify(output); } + [Fact] + [Expectation("Baz", "Json")] + public async Task Should_Return_Expected_Json_For_Baz_Not_Specifying_Framework() + { + // Given + var project = Fixture.GetPath("Baz/Baz.csproj"); + + // When + using var tempFileScope = new TempFileScope(); + var (exitCode, _) = await Fixture.Run(project, "--out", tempFileScope.FileName); + + // Then + exitCode.ShouldBe(0); + await Verifier.VerifyFile(tempFileScope.FileName); + } + [Fact] [Expectation("Solution", "Default")] public async Task Should_Return_Expected_Result_For_Solution_Not_Specifying_Framework() { // Given - var fixture = new Fixture(); var solution = Fixture.GetPath("Snitch.Tests.Fixtures.sln"); // When @@ -46,12 +59,27 @@ public async Task Should_Return_Expected_Result_For_Solution_Not_Specifying_Fram await Verifier.Verify(output); } + [Fact] + [Expectation("Solution", "Json")] + public async Task Should_Return_Expected_Json_For_Solution_Not_Specifying_Framework() + { + // Given + var solution = Fixture.GetPath("Snitch.Tests.Fixtures.sln"); + + // When + using var tempFileScope = new TempFileScope(); + var (exitCode, _) = await Fixture.Run(solution, "--out", tempFileScope.FileName); + + // Then + exitCode.ShouldBe(0); + await Verifier.VerifyFile(tempFileScope.FileName); + } + [Fact] [Expectation("Baz", "netstandard2.0")] public async Task Should_Return_Expected_Result_For_Baz_Specifying_Framework() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -67,7 +95,6 @@ public async Task Should_Return_Expected_Result_For_Baz_Specifying_Framework() public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Strict() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -83,7 +110,6 @@ public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Str public async Task Should_Return_Expected_Result_For_Baz_When_Excluding_Library() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -99,7 +125,6 @@ public async Task Should_Return_Expected_Result_For_Baz_When_Excluding_Library() public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -115,7 +140,6 @@ public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project() public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project_And_NoReleases() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -131,7 +155,6 @@ public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project_An public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Strict_And_NoPreRelease() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Baz/Baz.csproj"); // When @@ -147,7 +170,6 @@ public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Str public async Task Should_Return_Non_Zero_Exit_Code_For_Thud_When_Running_With_Strict_And_NoPreRelease() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Thud/Thud.csproj"); // When @@ -163,7 +185,6 @@ public async Task Should_Return_Non_Zero_Exit_Code_For_Thud_When_Running_With_St public async Task Should_Return_Non_Zero_Exit_Code_For_Thuuud_When_Running_With_Strict_And_NoPreRelease() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Thuuud/Thuuud.csproj"); // When @@ -179,7 +200,6 @@ public async Task Should_Return_Non_Zero_Exit_Code_For_Thuuud_When_Running_With_ public async Task Should_Return_Zero_Exit_Code_For_Thuuud_When_Running_With_NoPreRelease() { // Given - var fixture = new Fixture(); var project = Fixture.GetPath("Thuuud/Thuuud.csproj"); // When @@ -190,7 +210,52 @@ public async Task Should_Return_Zero_Exit_Code_For_Thuuud_When_Running_With_NoPr await Verifier.Verify(output); } - public sealed class Fixture + [Fact] + [Expectation("FooBar", "Default")] + public async Task Should_Print_Error_For_FooBar() + { + // Given + var project = Fixture.GetPath("FooBar/FooBar.csproj"); + + // When + var (exitCode, output) = await Fixture.Run(project); + + // Then + exitCode.ShouldBe(0); + await Verifier.Verify(output); + } + + [Fact] + [Expectation("FooBar", "Strict")] + public async Task Should_Return_NonZero_Exit_Code_For_FooBar_When_Running_With_Strict() + { + // Given + var project = Fixture.GetPath("FooBar/FooBar.csproj"); + + // When + var (exitCode, output) = await Fixture.Run(project, "--strict"); + + // Then + exitCode.ShouldBe(-1); + await Verifier.Verify(output); + } + + [Fact] + [Expectation("FSharp", "Default")] + public async Task Should_Return_Expected_Result_For_FSharp_Not_Specifying_Framework() + { + // Given + var project = Fixture.GetPath("FSharp/FSharp.fsproj"); + + // When + var (exitCode, output) = await Fixture.Run(project); + + // Then + exitCode.ShouldBe(0); + await Verifier.Verify(output); + } + + private static class Fixture { public static string GetPath(string path) { @@ -207,20 +272,22 @@ public static string GetPath(string path) } } - [Fact] - [Expectation("FSharp", "Default")] - public async Task Should_Return_Expected_Result_For_FSharp_Not_Specifying_Framework() + private sealed class TempFileScope : IDisposable { - // Given - var fixture = new Fixture(); - var project = Fixture.GetPath("FSharp/FSharp.fsproj"); + public TempFileScope() + { + this.FileName = Path.GetTempFileName() + ".json"; + } - // When - var (exitCode, output) = await Fixture.Run(project); + public string FileName { get; } - // Then - exitCode.ShouldBe(0); - await Verifier.Verify(output); + public void Dispose() + { + if (File.Exists(this.FileName)) + { + File.Delete(this.FileName); + } + } } } } diff --git a/src/Snitch.Tests/VerifyConfiguration.cs b/src/Snitch.Tests/VerifyConfiguration.cs index f775c83..7d49787 100644 --- a/src/Snitch.Tests/VerifyConfiguration.cs +++ b/src/Snitch.Tests/VerifyConfiguration.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using VerifyTests; -namespace Sntich.Tests +namespace Snitch.Tests { public static class VerifyConfiguration { diff --git a/src/Snitch/Analysis/ProjectBuilder.cs b/src/Snitch/Analysis/ProjectBuilder.cs index 84802ed..6fc9e4f 100644 --- a/src/Snitch/Analysis/ProjectBuilder.cs +++ b/src/Snitch/Analysis/ProjectBuilder.cs @@ -153,18 +153,26 @@ private Project Build( ? $"{prefix}Analyzing [aqua]{project.Name}[/]..." : $"{prefix}Analyzing [aqua]{project.Name}[/] [grey]({tfm})[/]..."; - _console.MarkupLine(status); + try + { + _console.MarkupLine(status); + + var projectAnalyzer = manager.GetProject(project.Path); + var results = (IEnumerable)projectAnalyzer.Build(); - var projectAnalyzer = manager.GetProject(project.Path); - var results = (IEnumerable)projectAnalyzer.Build(); + if (!string.IsNullOrWhiteSpace(tfm)) + { + var closest = results.GetNearestFrameworkMoniker(tfm); + results = results.Where(p => p.TargetFramework.Equals(closest, StringComparison.OrdinalIgnoreCase)); + } - if (!string.IsNullOrWhiteSpace(tfm)) + return results.FirstOrDefault(); + } + catch (Exception ex) { - var closest = results.GetNearestFrameworkMoniker(tfm); - results = results.Where(p => p.TargetFramework.Equals(closest, StringComparison.OrdinalIgnoreCase)); + _console.MarkupLine($"{prefix} [red]ERROR:[/] {ex.Message}"); + return null; } - - return results.FirstOrDefault(); } } } diff --git a/src/Snitch/Analysis/ProjectFileReporter.cs b/src/Snitch/Analysis/ProjectFileReporter.cs new file mode 100644 index 0000000..6a51108 --- /dev/null +++ b/src/Snitch/Analysis/ProjectFileReporter.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Spectre.Console; + +namespace Snitch.Analysis +{ + internal class ProjectFileReporter + { + private readonly IAnsiConsole _console; + + public ProjectFileReporter(IAnsiConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + internal void WriteToFile(List analyzerResults, string outputFileName, bool noPreRelease) + { + var results = + analyzerResults + .Where(x => x.CanBeRemoved.Count > 0 || x.MightBeRemoved.Count > 0 || x.HasPreReleases) + .Select(x => new + { + x.Project, + CanBeRemoved = x.CanBeRemoved.Select(y => new + { + PackageName = y.Package.Name, + PackageVersion = y.Package.Version?.OriginalVersion, + ReferencedBy = y.Original.Project.Name, + }), + MightBeRemoved = x.MightBeRemoved.Select(y => new + { + PackageName = y.Package.Name, + PackageVersion = y.Package.Version?.OriginalVersion, + ReferencedBy = y.Original.Project.Name, + ReferencePackageVersion = y.Original.Package.Version?.OriginalVersion, + }), + PreRelease = noPreRelease ? null : x.PreReleasePackages.Select(y => new + { + PackageName = y.Name, + PackageVersion = y.Version?.OriginalVersion, + }), + }); + + using FileStream createStream = File.Create(outputFileName); + JsonSerializer.Serialize(createStream, results, new JsonSerializerOptions() + { + WriteIndented = true, + }); + + _console.WriteLine(); + _console.MarkupLine($"[green]Results written to {outputFileName}![/]"); + _console.WriteLine(); + } + } +} diff --git a/src/Snitch/Analysis/ProjectReporter.cs b/src/Snitch/Analysis/ProjectReporter.cs index ac4eab2..7afd2eb 100644 --- a/src/Snitch/Analysis/ProjectReporter.cs +++ b/src/Snitch/Analysis/ProjectReporter.cs @@ -104,7 +104,7 @@ public void WriteToConsole([NotNull] List results, bool n if (noPreRelease && resultsWithPreReleases.Count > 0) { report.AddEmptyRow(); - report.AddRow($" [yellow]Projects with pre-release package references:[/]"); + report.AddRow(" [yellow]Projects with pre-release package references:[/]"); var packagesByProject = resultsWithPreReleases.SelectMany(x => x.PreReleasePackages, (project, package) => new { Project = project.Project, diff --git a/src/Snitch/Commands/AnalyzeCommand.cs b/src/Snitch/Commands/AnalyzeCommand.cs index 099d666..f6ba670 100644 --- a/src/Snitch/Commands/AnalyzeCommand.cs +++ b/src/Snitch/Commands/AnalyzeCommand.cs @@ -18,6 +18,7 @@ public sealed class AnalyzeCommand : Command private readonly ProjectBuilder _builder; private readonly ProjectAnalyzer _analyzer; private readonly ProjectReporter _reporter; + private readonly ProjectFileReporter _fileReporter; public sealed class Settings : CommandSettings { @@ -44,6 +45,10 @@ public sealed class Settings : CommandSettings [CommandOption("--no-prerelease")] [Description("Verifies that all package references are not pre-releases.")] public bool NoPreRelease { get; set; } + + [CommandOption("-o|--out ")] + [Description("The name of the json output file to write.")] + public string? OutputFileName { get; set; } } public AnalyzeCommand(IAnsiConsole console) @@ -52,6 +57,7 @@ public AnalyzeCommand(IAnsiConsole console) _builder = new ProjectBuilder(console); _analyzer = new ProjectAnalyzer(); _reporter = new ProjectReporter(console); + _fileReporter = new ProjectFileReporter(console); } public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) @@ -79,34 +85,61 @@ public override int Execute([NotNull] CommandContext context, [NotNull] Settings foreach (var projectToAnalyze in projectsToAnalyze) { - // Perform a design time build of the project. - var buildResult = _builder.Build( - projectToAnalyze, - targetFramework, - settings.Skip, - projectCache); - - // Update the cache of built projects. - projectCache.Add(buildResult.Project); - foreach (var item in buildResult.Dependencies) + if (!projectToAnalyze.EndsWith("csproj", StringComparison.OrdinalIgnoreCase) + && !projectToAnalyze.EndsWith("fsproj", StringComparison.OrdinalIgnoreCase)) { - projectCache.Add(item); + var projectName = Path.GetFileNameWithoutExtension(projectToAnalyze); + _console.MarkupLine($"Skipping Non .NET Project [aqua]{projectName}[/]"); + _console.WriteLine(); + + continue; } - // Analyze the project. - var analyzeResult = _analyzer.Analyze(buildResult.Project); - if (settings.Exclude?.Length > 0) + try { - // Filter packages that should be excluded. - analyzeResult = analyzeResult.Filter(settings.Exclude); + // Perform a design time build of the project. + var buildResult = _builder.Build( + projectToAnalyze, + targetFramework, + settings.Skip, + projectCache); + + // Update the cache of built projects. + projectCache.Add(buildResult.Project); + foreach (var item in buildResult.Dependencies) + { + projectCache.Add(item); + } + + // Analyze the project. + var analyzeResult = _analyzer.Analyze(buildResult.Project); + if (settings.Exclude?.Length > 0) + { + // Filter packages that should be excluded. + analyzeResult = analyzeResult.Filter(settings.Exclude); + } + + analyzerResults.Add(analyzeResult); + } + catch (Exception ex) + { + _console.MarkupLine($" [red]ERROR:[/] {ex.Message}"); + if (settings.Strict) + { + return -1; + } } - - analyzerResults.Add(analyzeResult); } // Write the report to the console _reporter.WriteToConsole(analyzerResults, settings.NoPreRelease); + if (settings.OutputFileName != null) + { + // Write the report to a file. + _fileReporter.WriteToFile(analyzerResults, settings.OutputFileName, settings.NoPreRelease); + } + // Return the correct exit code. return GetExitCode(settings, analyzerResults); }); diff --git a/src/Snitch/Program.cs b/src/Snitch/Program.cs index 65b2914..098da42 100644 --- a/src/Snitch/Program.cs +++ b/src/Snitch/Program.cs @@ -37,6 +37,7 @@ public static async Task Run(string[] args, Action? configat config.AddExample(new[] { "Solution.sln", "-e", "Foo", "-e", "Bar" }); config.AddExample(new[] { "Solution.sln", "--tfm", "net462" }); config.AddExample(new[] { "Solution.sln", "--tfm", "net462", "--strict" }); + config.AddExample(new[] { "Solution.sln", "--tfm", "net462", "--out", "c:\\temp\\snitch.json" }); config.AddCommand("version"); });