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");
});