├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.cake ├── dotnet-tools.json ├── global.json └── src ├── Snitch.Tests.Fixtures ├── Bar │ ├── Bar.csproj │ └── Class1.cs ├── Baz │ ├── Baz.csproj │ └── Class1.cs ├── FSharp │ └── FSharp.fsproj ├── Foo │ ├── Class1.cs │ └── Foo.csproj ├── Quuux │ ├── Class1.cs │ └── Quuux.csproj ├── Quux │ ├── Class1.cs │ └── Quux.csproj ├── Qux │ ├── Class1.cs │ └── Qux.csproj ├── Snitch.Tests.Fixtures.sln ├── Thud │ ├── Class1.cs │ └── Thud.csproj ├── Thuuud │ ├── Class1.cs │ └── Thuuud.csproj └── Zap │ ├── Class1.cs │ └── Zap.csproj ├── Snitch.Tests ├── Expectations │ ├── Baz.Default.verified.txt │ ├── Baz.Exclude_Autofac.verified.txt │ ├── Baz.Skip_Bar.verified.txt │ ├── Baz.Skip_Bar_NoPreRelease.verified.txt │ ├── Baz.netstandard2.0.verified.txt │ ├── Baz.netstandard2.0_Strict.verified.txt │ ├── Baz.netstandard2.Strict.NoPreRelease.verified.txt │ ├── FSharp.Default.verified.txt │ ├── Solution.Default.verified.txt │ ├── Thud.netstandard2.Strict.NoPreRelease.verified.txt │ ├── Thuuud.netstandard2.NoPreRelease.verified.txt │ └── Thuuud.netstandard2.Strict.NoPreRelease.verified.txt ├── ProgramTests.cs ├── Snitch.Tests.csproj └── VerifyConfiguration.cs ├── Snitch.sln ├── Snitch ├── Analysis │ ├── Models │ │ ├── Package.cs │ │ ├── PackageToRemove.cs │ │ ├── Project.cs │ │ ├── ProjectComparer.cs │ │ └── ProjectPackage.cs │ ├── ProjectAnalyzer.cs │ ├── ProjectAnalyzerResult.cs │ ├── ProjectBuildResult.cs │ ├── ProjectBuilder.cs │ ├── ProjectReporter.cs │ └── Utilities │ │ └── PathUtility.cs ├── Commands │ ├── AnalyzeCommand.cs │ └── VersionCommand.cs ├── Program.cs ├── Snitch.csproj └── Utilities │ ├── Extensions │ ├── AnalyzerResultExtensions.cs │ ├── EnumerableExtensions.cs │ └── PackageExtensions.cs │ ├── TypeRegistrar.cs │ └── TypeResolver.cs ├── icon.png └── mdsnippets.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = CRLF 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true 10 | 11 | [*.sln] 12 | indent_style = tab 13 | 14 | [*.{csproj,vbproj,vcxproj,vcxproj.filters}] 15 | indent_size = 2 16 | 17 | [*.{xml,config,props,targets,nuspec,ruleset}] 18 | indent_size = 2 19 | 20 | [*.json] 21 | indent_size = 2 22 | 23 | [*.yaml] 24 | indent_size = 2 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | 29 | [*.sh] 30 | end_of_line = lf 31 | [*.cs] 32 | 33 | # SA1200: Using directives should be placed correctly 34 | dotnet_diagnostic.SA1200.severity = none 35 | 36 | # SA1600: Elements should be documented 37 | dotnet_diagnostic.SA1600.severity = none 38 | 39 | # SA1633: File should have header 40 | dotnet_diagnostic.SA1633.severity = none 41 | 42 | # SA1201: Elements should appear in the correct order 43 | dotnet_diagnostic.SA1201.severity = none 44 | 45 | # SA1101: Prefix local calls with this 46 | dotnet_diagnostic.SA1101.severity = none 47 | 48 | # SA1516: Elements should be separated by blank line 49 | dotnet_diagnostic.SA1516.severity = none 50 | 51 | # SA1309: Field names should not begin with underscore 52 | dotnet_diagnostic.SA1309.severity = none 53 | 54 | # CA1303: Do not pass literals as localized parameters 55 | dotnet_diagnostic.CA1303.severity = none 56 | 57 | # CA1034: Nested types should not be visible 58 | dotnet_diagnostic.CA1034.severity = none 59 | 60 | # CA1819: Properties should not return arrays 61 | dotnet_diagnostic.CA1819.severity = none 62 | 63 | # CA1062: Validate arguments of public methods 64 | dotnet_diagnostic.CA1062.severity = silent 65 | 66 | # CA2007: Consider calling ConfigureAwait on the awaited task 67 | dotnet_diagnostic.CA2007.severity = none 68 | 69 | # SA1204: Static elements should appear before instance elements 70 | dotnet_diagnostic.SA1204.severity = none 71 | 72 | # SA1649: File name should match first type name 73 | dotnet_diagnostic.SA1649.severity = none 74 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # fix commited files from crlf to lf 2 | 729b1bd03e0fd3ef8cb99734a0a8bd5351f2f7a2 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf working-tree-encoding=UTF-8 2 | *.png binary 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: Continuous Integration 3 | on: pull_request 4 | 5 | env: 6 | # Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages 7 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 8 | # Disable sending usage data to Microsoft 9 | DOTNET_CLI_TELEMETRY_OPTOUT: true 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 15 | runs-on: ubuntu-latest} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: 'Get Git tags' 23 | run: git fetch --tags 24 | shell: bash 25 | 26 | - name: Setup .NET SDK 27 | uses: actions/setup-dotnet@v3 28 | 29 | - name: Build 30 | shell: bash 31 | run: | 32 | dotnet tool restore 33 | dotnet cake 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc folders 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Tt]emp/ 5 | [Pp]ackages/ 6 | /.artifacts/ 7 | /[Tt]ools/ 8 | 9 | # Cakeup 10 | cakeup-x86_64-latest.exe 11 | 12 | # .NET Core CLI 13 | /.dotnet/ 14 | /.packages/ 15 | dotnet-install.sh* 16 | *.lock.json 17 | 18 | # Visual Studio 19 | .vs/ 20 | .vscode/ 21 | launchSettings.json 22 | *.sln.ide/ 23 | 24 | # Rider 25 | src/.idea/**/workspace.xml 26 | src/.idea/**/tasks.xml 27 | src/.idea/dictionaries 28 | src/.idea/**/dataSources/ 29 | src/.idea/**/dataSources.ids 30 | src/.idea/**/dataSources.xml 31 | src/.idea/**/dataSources.local.xml 32 | src/.idea/**/sqlDataSources.xml 33 | src/.idea/**/dynamic.xml 34 | src/.idea/**/uiDesigner.xml 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | *.userprefs 44 | *.GhostDoc.xml 45 | *StyleCop.Cache 46 | 47 | # Build results 48 | [Dd]ebug/ 49 | [Rr]elease/ 50 | x64/ 51 | *_i.c 52 | *_p.c 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # ReSharper is a .NET coding add-in 77 | _ReSharper* 78 | 79 | # NCrunch 80 | *.ncrunch* 81 | .*crunch*.local.xml 82 | _NCrunch_* 83 | 84 | # NuGet Packages Directory 85 | packages 86 | 87 | # Windows 88 | Thumbs.db 89 | 90 | # Rider 91 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Spectre Systems AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR 15 | A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snitch 2 | 3 | [![NuGet Status](https://img.shields.io/nuget/v/Snitch.svg)](https://www.nuget.org/packages/Snitch/) 4 | 5 | A tool that help you find transitive package references that can be removed. 6 | 7 | ## Example 8 | 9 | ``` 10 | > snitch --tfm net462 11 | ``` 12 | 13 | Results in: 14 | 15 | 16 | 17 | ```txt 18 | Analyzing... 19 | Analyzing Snitch.Tests.Fixtures.sln 20 | Analyzing Foo... 21 | Analyzing Bar... 22 | Analyzing Baz... 23 | Analyzing Qux... 24 | Analyzing Zap... 25 | Analyzing Quux... 26 | Analyzing Quuux... 27 | Analyzing Thud... 28 | Analyzing Thuuud... 29 | 30 | ╭─────────────────────────────────────────────────────────────────╮ 31 | │ Packages that can be removed from Bar: │ 32 | │ ┌──────────────────────┬──────────────────────────────────────┐ │ 33 | │ │ Package │ Referenced by │ │ 34 | │ ├──────────────────────┼──────────────────────────────────────┤ │ 35 | │ │ Autofac │ Foo │ │ 36 | │ └──────────────────────┴──────────────────────────────────────┘ │ 37 | │ │ 38 | │ Packages that can be removed from Baz: │ 39 | │ ┌──────────────────────┬──────────────────────────────────────┐ │ 40 | │ │ Package │ Referenced by │ │ 41 | │ ├──────────────────────┼──────────────────────────────────────┤ │ 42 | │ │ Autofac │ Foo │ │ 43 | │ └──────────────────────┴──────────────────────────────────────┘ │ 44 | │ │ 45 | │ Packages that might be removed from Qux: │ 46 | │ ┌───────────┬───────────┬─────────────────────────────────────┐ │ 47 | │ │ Package │ Version │ Reason │ │ 48 | │ ├───────────┼───────────┼─────────────────────────────────────┤ │ 49 | │ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │ 50 | │ └───────────┴───────────┴─────────────────────────────────────┘ │ 51 | │ │ 52 | │ Packages that might be removed from Zap: │ 53 | │ ┌──────────────────┬──────────┬───────────────────────────────┐ │ 54 | │ │ Package │ Version │ Reason │ │ 55 | │ ├──────────────────┼──────────┼───────────────────────────────┤ │ 56 | │ │ Newtonsoft.Json │ 12.0.3 │ Updated from 12.0.1 in Foo │ │ 57 | │ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │ 58 | │ └──────────────────┴──────────┴───────────────────────────────┘ │ 59 | │ │ 60 | │ Packages that might be removed from Thuuud: │ 61 | │ ┌─────────────────┬──────────────┬────────────────────────────┐ │ 62 | │ │ Package │ Version │ Reason │ │ 63 | │ ├─────────────────┼──────────────┼────────────────────────────┤ │ 64 | │ │ Newtonsoft.Json │ 13.0.2-beta2 │ Updated from 12.0.1 in Foo │ │ 65 | │ └─────────────────┴──────────────┴────────────────────────────┘ │ 66 | ╰─────────────────────────────────────────────────────────────────╯ 67 | ``` 68 | snippet source | anchor 69 | 70 | 71 | ## Installation 72 | 73 | ``` 74 | > dotnet tool install -g snitch 75 | ``` 76 | 77 | ## Usage 78 | 79 | _Examine a specific project or solution using the first built 80 | target framework._ 81 | 82 | ``` 83 | > snitch MyProject.csproj 84 | ``` 85 | 86 | _Examine a specific project using a specific 87 | target framework moniker._ 88 | 89 | ``` 90 | > snitch MyProject.csproj --tfm net462 91 | ``` 92 | 93 | _Examine a specific project using a specific target framework moniker 94 | and return exit code 0 only if there was no transitive package collisions. 95 | Useful for continuous integration._ 96 | 97 | ``` 98 | > snitch MyProject.csproj --tfm net462 --strict 99 | ``` 100 | 101 | _Examine a specific project using a specific target framework moniker 102 | and make sure that the packages Foo and Bar are excluded from the result._ 103 | 104 | ``` 105 | > snitch MyProject.csproj --tfm net462 --exclude Foo --exclude Bar 106 | ``` 107 | 108 | _Examine a specific project using a specific target framework moniker 109 | and exclude the project OtherProject from analysis._ 110 | 111 | ``` 112 | > snitch MyProject.csproj --tfm net462 --skip OtherProject 113 | ``` 114 | 115 | _Examine a specific project or solution to make sure there are no pre-release package references._ 116 | 117 | ``` 118 | > snitch MyProject.csproj --no-prerelease 119 | ``` 120 | 121 | ## Building Snitch from source 122 | 123 | ``` 124 | > dotnet tool restore 125 | > dotnet cake 126 | ``` 127 | 128 | ## Icon 129 | 130 | [Hollow](https://thenounproject.com/term/stitch/1571973/) designed by [Ben Davis](https://thenounproject.com/smashicons/) from [The Noun Project](https://thenounproject.com). 131 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | var semanticVersion = Argument("packageversion", "1.0.0"); 2 | var version = semanticVersion.Split(new[] { '-' }).FirstOrDefault() ?? semanticVersion; 3 | 4 | Information("Version: {0}", semanticVersion); 5 | Information("Legacy version: {0}", version); 6 | 7 | Task("Clean") 8 | .Does(() => 9 | { 10 | CleanDirectory("./.artifacts"); 11 | }); 12 | 13 | Task("Build") 14 | .IsDependentOn("Clean") 15 | .Does(() => 16 | { 17 | DotNetBuild("./src/Snitch.sln", new DotNetBuildSettings 18 | { 19 | Configuration = "Release", 20 | MSBuildSettings = new DotNetMSBuildSettings() 21 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 22 | .WithProperty("Version", version) 23 | .WithProperty("AssemblyVersion", version) 24 | .WithProperty("FileVersion", version) 25 | }); 26 | }); 27 | 28 | Task("Run-Tests") 29 | .IsDependentOn("Build") 30 | .Does(() => 31 | { 32 | DotNetTest("./src/Snitch.Tests/Snitch.Tests.csproj", new DotNetTestSettings 33 | { 34 | Configuration = "Release" 35 | }); 36 | }); 37 | 38 | Task("Pack") 39 | .IsDependentOn("Run-Tests") 40 | .Does(() => 41 | { 42 | DotNetPack("./src/Snitch.sln", new DotNetPackSettings 43 | { 44 | Configuration = "Release", 45 | NoRestore = true, 46 | NoBuild = true, 47 | OutputDirectory = "./.artifacts", 48 | MSBuildSettings = new DotNetMSBuildSettings() 49 | .WithProperty("PackageVersion", semanticVersion) 50 | }); 51 | }); 52 | 53 | RunTarget("Pack") -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "3.2.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src" ], 3 | "sdk": { 4 | "version": "8.0.100", 5 | "rollForward": "latestPatch" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Bar/Bar.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Bar/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Bar 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Baz/Baz.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Baz/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Baz 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/FSharp/FSharp.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Foo/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foo 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Foo/Foo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Quuux/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Quuux 2 | { 3 | public class Class1 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Quuux/Quuux.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Quux/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Quux 2 | { 3 | public class Class1 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Quux/Quux.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Qux/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Qux 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Qux/Qux.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Snitch.Tests.Fixtures.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29326.143 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foo", "Foo\Foo.csproj", "{27C335C5-0DF2-49DB-AD3B-6284309BB534}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bar", "Bar\Bar.csproj", "{D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Baz", "Baz\Baz.csproj", "{16675791-AE34-41EF-8060-7B1F635C8EE1}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qux", "Qux\Qux.csproj", "{05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zap", "Zap\Zap.csproj", "{18C1243F-6B3A-45DA-8746-11946205EA35}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Quux", "Quux\Quux.csproj", "{0B419579-20DD-48F4-92B8-C715D96A63A6}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Quuux", "Quuux\Quuux.csproj", "{E82441AE-9A28-4472-B506-4078FA4DB70B}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thud", "Thud\Thud.csproj", "{22334552-7CE0-4E06-AC9C-974080E2FD99}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thuuud", "Thuuud\Thuuud.csproj", "{42E62FFD-F1CD-41D3-9DCB-93638D77F33B}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Debug|x64 = Debug|x64 28 | Debug|x86 = Debug|x86 29 | Release|Any CPU = Release|Any CPU 30 | Release|x64 = Release|x64 31 | Release|x86 = Release|x86 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|x64.Build.0 = Debug|Any CPU 38 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Debug|x86.Build.0 = Debug|Any CPU 40 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|x64.ActiveCfg = Release|Any CPU 43 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|x64.Build.0 = Release|Any CPU 44 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|x86.ActiveCfg = Release|Any CPU 45 | {27C335C5-0DF2-49DB-AD3B-6284309BB534}.Release|x86.Build.0 = Release|Any CPU 46 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|x64.ActiveCfg = Debug|Any CPU 49 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|x64.Build.0 = Debug|Any CPU 50 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|x86.ActiveCfg = Debug|Any CPU 51 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Debug|x86.Build.0 = Debug|Any CPU 52 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|x64.ActiveCfg = Release|Any CPU 55 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|x64.Build.0 = Release|Any CPU 56 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|x86.ActiveCfg = Release|Any CPU 57 | {D8552EAB-5C3A-4F7D-82C6-8E882A1032E8}.Release|x86.Build.0 = Release|Any CPU 58 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|x64.ActiveCfg = Debug|Any CPU 61 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|x64.Build.0 = Debug|Any CPU 62 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|x86.ActiveCfg = Debug|Any CPU 63 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Debug|x86.Build.0 = Debug|Any CPU 64 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|x64.ActiveCfg = Release|Any CPU 67 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|x64.Build.0 = Release|Any CPU 68 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|x86.ActiveCfg = Release|Any CPU 69 | {16675791-AE34-41EF-8060-7B1F635C8EE1}.Release|x86.Build.0 = Release|Any CPU 70 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|x64.ActiveCfg = Debug|Any CPU 73 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|x64.Build.0 = Debug|Any CPU 74 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|x86.ActiveCfg = Debug|Any CPU 75 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Debug|x86.Build.0 = Debug|Any CPU 76 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 77 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|Any CPU.Build.0 = Release|Any CPU 78 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|x64.ActiveCfg = Release|Any CPU 79 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|x64.Build.0 = Release|Any CPU 80 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|x86.ActiveCfg = Release|Any CPU 81 | {05E36179-ABC8-4EBC-B57F-0E6C1D7249F0}.Release|x86.Build.0 = Release|Any CPU 82 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 83 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|Any CPU.Build.0 = Debug|Any CPU 84 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|x64.ActiveCfg = Debug|Any CPU 85 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|x64.Build.0 = Debug|Any CPU 86 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|x86.ActiveCfg = Debug|Any CPU 87 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Debug|x86.Build.0 = Debug|Any CPU 88 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|Any CPU.ActiveCfg = Release|Any CPU 89 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|Any CPU.Build.0 = Release|Any CPU 90 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|x64.ActiveCfg = Release|Any CPU 91 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|x64.Build.0 = Release|Any CPU 92 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|x86.ActiveCfg = Release|Any CPU 93 | {18C1243F-6B3A-45DA-8746-11946205EA35}.Release|x86.Build.0 = Release|Any CPU 94 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 95 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 96 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|x64.ActiveCfg = Debug|Any CPU 97 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|x64.Build.0 = Debug|Any CPU 98 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|x86.ActiveCfg = Debug|Any CPU 99 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Debug|x86.Build.0 = Debug|Any CPU 100 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 101 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|Any CPU.Build.0 = Release|Any CPU 102 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|x64.ActiveCfg = Release|Any CPU 103 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|x64.Build.0 = Release|Any CPU 104 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|x86.ActiveCfg = Release|Any CPU 105 | {0B419579-20DD-48F4-92B8-C715D96A63A6}.Release|x86.Build.0 = Release|Any CPU 106 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 107 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|Any CPU.Build.0 = Debug|Any CPU 108 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|x64.ActiveCfg = Debug|Any CPU 109 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|x64.Build.0 = Debug|Any CPU 110 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|x86.ActiveCfg = Debug|Any CPU 111 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Debug|x86.Build.0 = Debug|Any CPU 112 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|Any CPU.ActiveCfg = Release|Any CPU 113 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|Any CPU.Build.0 = Release|Any CPU 114 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|x64.ActiveCfg = Release|Any CPU 115 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|x64.Build.0 = Release|Any CPU 116 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|x86.ActiveCfg = Release|Any CPU 117 | {E82441AE-9A28-4472-B506-4078FA4DB70B}.Release|x86.Build.0 = Release|Any CPU 118 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 119 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|Any CPU.Build.0 = Debug|Any CPU 120 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|x64.ActiveCfg = Debug|Any CPU 121 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|x64.Build.0 = Debug|Any CPU 122 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|x86.ActiveCfg = Debug|Any CPU 123 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Debug|x86.Build.0 = Debug|Any CPU 124 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|Any CPU.ActiveCfg = Release|Any CPU 125 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|Any CPU.Build.0 = Release|Any CPU 126 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|x64.ActiveCfg = Release|Any CPU 127 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|x64.Build.0 = Release|Any CPU 128 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|x86.ActiveCfg = Release|Any CPU 129 | {22334552-7CE0-4E06-AC9C-974080E2FD99}.Release|x86.Build.0 = Release|Any CPU 130 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 131 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|Any CPU.Build.0 = Debug|Any CPU 132 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|x64.ActiveCfg = Debug|Any CPU 133 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|x64.Build.0 = Debug|Any CPU 134 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|x86.ActiveCfg = Debug|Any CPU 135 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Debug|x86.Build.0 = Debug|Any CPU 136 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|Any CPU.ActiveCfg = Release|Any CPU 137 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|Any CPU.Build.0 = Release|Any CPU 138 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|x64.ActiveCfg = Release|Any CPU 139 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|x64.Build.0 = Release|Any CPU 140 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|x86.ActiveCfg = Release|Any CPU 141 | {42E62FFD-F1CD-41D3-9DCB-93638D77F33B}.Release|x86.Build.0 = Release|Any CPU 142 | EndGlobalSection 143 | GlobalSection(SolutionProperties) = preSolution 144 | HideSolutionNode = FALSE 145 | EndGlobalSection 146 | GlobalSection(ExtensibilityGlobals) = postSolution 147 | SolutionGuid = {8BEC268B-CF39-4103-B514-DC9897D72C14} 148 | EndGlobalSection 149 | EndGlobal 150 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Thud/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Thud 4 | { 5 | public class Class1 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Thud/Thud.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Thuuud/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Thud 4 | { 5 | public class Class1 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Thuuud/Thuuud.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Thud 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Zap/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Corgi 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Snitch.Tests.Fixtures/Zap/Zap.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.Default.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | ╭─────────────────────────────────────────╮ 8 | │ Packages that can be removed from Baz: │ 9 | │ ┌──────────────┬──────────────────────┐ │ 10 | │ │ Package │ Referenced by │ │ 11 | │ ├──────────────┼──────────────────────┤ │ 12 | │ │ Autofac │ Foo │ │ 13 | │ └──────────────┴──────────────────────┘ │ 14 | ╰─────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.Exclude_Autofac.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | Everything looks good! -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.Skip_Bar.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz... 4 | 5 | Everything looks good! -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.Skip_Bar_NoPreRelease.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz... 4 | 5 | Everything looks good! -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.netstandard2.0.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz (netstandard2.0)... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | ╭─────────────────────────────────────────╮ 8 | │ Packages that can be removed from Baz: │ 9 | │ ┌──────────────┬──────────────────────┐ │ 10 | │ │ Package │ Referenced by │ │ 11 | │ ├──────────────┼──────────────────────┤ │ 12 | │ │ Autofac │ Foo │ │ 13 | │ └──────────────┴──────────────────────┘ │ 14 | ╰─────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.netstandard2.0_Strict.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz (netstandard2.0)... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | ╭─────────────────────────────────────────╮ 8 | │ Packages that can be removed from Baz: │ 9 | │ ┌──────────────┬──────────────────────┐ │ 10 | │ │ Package │ Referenced by │ │ 11 | │ ├──────────────┼──────────────────────┤ │ 12 | │ │ Autofac │ Foo │ │ 13 | │ └──────────────┴──────────────────────┘ │ 14 | ╰─────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Baz.netstandard2.Strict.NoPreRelease.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Baz.csproj 3 | Analyzing Baz... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | ╭─────────────────────────────────────────╮ 8 | │ Packages that can be removed from Baz: │ 9 | │ ┌──────────────┬──────────────────────┐ │ 10 | │ │ Package │ Referenced by │ │ 11 | │ ├──────────────┼──────────────────────┤ │ 12 | │ │ Autofac │ Foo │ │ 13 | │ └──────────────┴──────────────────────┘ │ 14 | ╰─────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/FSharp.Default.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing FSharp.fsproj 3 | Analyzing FSharp... 4 | * Analyzing Bar (netstandard2.0)... 5 | * Analyzing Foo (netstandard2.0)... 6 | 7 | ╭────────────────────────────────────────────╮ 8 | │ Packages that can be removed from FSharp: │ 9 | │ ┌───────────────┬────────────────────────┐ │ 10 | │ │ Package │ Referenced by │ │ 11 | │ ├───────────────┼────────────────────────┤ │ 12 | │ │ Autofac │ Foo │ │ 13 | │ └───────────────┴────────────────────────┘ │ 14 | ╰────────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Solution.Default.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Snitch.Tests.Fixtures.sln 3 | Analyzing Foo... 4 | Analyzing Bar... 5 | Analyzing Baz... 6 | Analyzing Qux... 7 | Analyzing Zap... 8 | Analyzing Quux... 9 | Analyzing Quuux... 10 | Analyzing Thud... 11 | Analyzing Thuuud... 12 | 13 | ╭─────────────────────────────────────────────────────────────────╮ 14 | │ Packages that can be removed from Bar: │ 15 | │ ┌──────────────────────┬──────────────────────────────────────┐ │ 16 | │ │ Package │ Referenced by │ │ 17 | │ ├──────────────────────┼──────────────────────────────────────┤ │ 18 | │ │ Autofac │ Foo │ │ 19 | │ └──────────────────────┴──────────────────────────────────────┘ │ 20 | │ │ 21 | │ Packages that can be removed from Baz: │ 22 | │ ┌──────────────────────┬──────────────────────────────────────┐ │ 23 | │ │ Package │ Referenced by │ │ 24 | │ ├──────────────────────┼──────────────────────────────────────┤ │ 25 | │ │ Autofac │ Foo │ │ 26 | │ └──────────────────────┴──────────────────────────────────────┘ │ 27 | │ │ 28 | │ Packages that might be removed from Qux: │ 29 | │ ┌───────────┬───────────┬─────────────────────────────────────┐ │ 30 | │ │ Package │ Version │ Reason │ │ 31 | │ ├───────────┼───────────┼─────────────────────────────────────┤ │ 32 | │ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │ 33 | │ └───────────┴───────────┴─────────────────────────────────────┘ │ 34 | │ │ 35 | │ Packages that might be removed from Zap: │ 36 | │ ┌──────────────────┬──────────┬───────────────────────────────┐ │ 37 | │ │ Package │ Version │ Reason │ │ 38 | │ ├──────────────────┼──────────┼───────────────────────────────┤ │ 39 | │ │ Newtonsoft.Json │ 12.0.3 │ Updated from 12.0.1 in Foo │ │ 40 | │ │ Autofac │ 4.9.3 │ Downgraded from 4.9.4 in Foo │ │ 41 | │ └──────────────────┴──────────┴───────────────────────────────┘ │ 42 | │ │ 43 | │ Packages that might be removed from Thuuud: │ 44 | │ ┌─────────────────┬──────────────┬────────────────────────────┐ │ 45 | │ │ Package │ Version │ Reason │ │ 46 | │ ├─────────────────┼──────────────┼────────────────────────────┤ │ 47 | │ │ Newtonsoft.Json │ 13.0.2-beta2 │ Updated from 12.0.1 in Foo │ │ 48 | │ └─────────────────┴──────────────┴────────────────────────────┘ │ 49 | ╰─────────────────────────────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Thud.netstandard2.Strict.NoPreRelease.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Thud.csproj 3 | Analyzing Thud... 4 | 5 | ╭────────────────────────────────────────────────╮ 6 | │ │ 7 | │ Projects with pre-release package references: │ 8 | │ ┌──────────┬──────────────────┬──────────────┐ │ 9 | │ │ Project │ Package │ Version │ │ 10 | │ ├──────────┼──────────────────┼──────────────┤ │ 11 | │ │ Thud │ Newtonsoft.Json │ 13.0.2-beta2 │ │ 12 | │ └──────────┴──────────────────┴──────────────┘ │ 13 | ╰────────────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Thuuud.netstandard2.NoPreRelease.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Thuuud.csproj 3 | Analyzing Thuuud... 4 | * Analyzing Foo (netstandard2.0)... 5 | 6 | ╭─────────────────────────────────────────────────────────────────╮ 7 | │ Packages that might be removed from Thuuud: │ 8 | │ ┌─────────────────┬──────────────┬────────────────────────────┐ │ 9 | │ │ Package │ Version │ Reason │ │ 10 | │ ├─────────────────┼──────────────┼────────────────────────────┤ │ 11 | │ │ Newtonsoft.Json │ 13.0.2-beta2 │ Updated from 12.0.1 in Foo │ │ 12 | │ └─────────────────┴──────────────┴────────────────────────────┘ │ 13 | │ │ 14 | │ Projects with pre-release package references: │ 15 | │ ┌─────────────┬──────────────────────────┬────────────────────┐ │ 16 | │ │ Project │ Package │ Version │ │ 17 | │ ├─────────────┼──────────────────────────┼────────────────────┤ │ 18 | │ │ Thuuud │ Newtonsoft.Json │ 13.0.2-beta2 │ │ 19 | │ └─────────────┴──────────────────────────┴────────────────────┘ │ 20 | ╰─────────────────────────────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/Expectations/Thuuud.netstandard2.Strict.NoPreRelease.verified.txt: -------------------------------------------------------------------------------- 1 | Analyzing... 2 | Analyzing Thuuud.csproj 3 | Analyzing Thuuud... 4 | * Analyzing Foo (netstandard2.0)... 5 | 6 | ╭─────────────────────────────────────────────────────────────────╮ 7 | │ Packages that might be removed from Thuuud: │ 8 | │ ┌─────────────────┬──────────────┬────────────────────────────┐ │ 9 | │ │ Package │ Version │ Reason │ │ 10 | │ ├─────────────────┼──────────────┼────────────────────────────┤ │ 11 | │ │ Newtonsoft.Json │ 13.0.2-beta2 │ Updated from 12.0.1 in Foo │ │ 12 | │ └─────────────────┴──────────────┴────────────────────────────┘ │ 13 | │ │ 14 | │ Projects with pre-release package references: │ 15 | │ ┌─────────────┬──────────────────────────┬────────────────────┐ │ 16 | │ │ Project │ Package │ Version │ │ 17 | │ ├─────────────┼──────────────────────────┼────────────────────┤ │ 18 | │ │ Thuuud │ Newtonsoft.Json │ 13.0.2-beta2 │ │ 19 | │ └─────────────┴──────────────────────────┴────────────────────┘ │ 20 | ╰─────────────────────────────────────────────────────────────────╯ -------------------------------------------------------------------------------- /src/Snitch.Tests/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using Snitch; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Spectre.Console.Cli; 7 | using Spectre.Console.Testing; 8 | using VerifyTests; 9 | using Xunit; 10 | using VerifyXunit; 11 | 12 | namespace Sntich.Tests 13 | { 14 | [UsesVerify] 15 | public class ProgramTests 16 | { 17 | [Fact] 18 | [Expectation("Baz", "Default")] 19 | public async Task Should_Return_Expected_Result_For_Baz_Not_Specifying_Framework() 20 | { 21 | // Given 22 | var fixture = new Fixture(); 23 | var project = Fixture.GetPath("Baz/Baz.csproj"); 24 | 25 | // When 26 | var (exitCode, output) = await Fixture.Run(project); 27 | 28 | // Then 29 | exitCode.ShouldBe(0); 30 | await Verifier.Verify(output); 31 | } 32 | 33 | [Fact] 34 | [Expectation("Solution", "Default")] 35 | public async Task Should_Return_Expected_Result_For_Solution_Not_Specifying_Framework() 36 | { 37 | // Given 38 | var fixture = new Fixture(); 39 | var solution = Fixture.GetPath("Snitch.Tests.Fixtures.sln"); 40 | 41 | // When 42 | var (exitCode, output) = await Fixture.Run(solution); 43 | 44 | // Then 45 | exitCode.ShouldBe(0); 46 | await Verifier.Verify(output); 47 | } 48 | 49 | [Fact] 50 | [Expectation("Baz", "netstandard2.0")] 51 | public async Task Should_Return_Expected_Result_For_Baz_Specifying_Framework() 52 | { 53 | // Given 54 | var fixture = new Fixture(); 55 | var project = Fixture.GetPath("Baz/Baz.csproj"); 56 | 57 | // When 58 | var (exitCode, output) = await Fixture.Run(project, "--tfm", "netstandard2.0"); 59 | 60 | // Then 61 | exitCode.ShouldBe(0); 62 | await Verifier.Verify(output); 63 | } 64 | 65 | [Fact] 66 | [Expectation("Baz", "netstandard2.0_Strict")] 67 | public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Strict() 68 | { 69 | // Given 70 | var fixture = new Fixture(); 71 | var project = Fixture.GetPath("Baz/Baz.csproj"); 72 | 73 | // When 74 | var (exitCode, output) = await Fixture.Run(project, "--tfm", "netstandard2.0", "--strict"); 75 | 76 | // Then 77 | exitCode.ShouldBe(-1); 78 | await Verifier.Verify(output); 79 | } 80 | 81 | [Fact] 82 | [Expectation("Baz", "Exclude_Autofac")] 83 | public async Task Should_Return_Expected_Result_For_Baz_When_Excluding_Library() 84 | { 85 | // Given 86 | var fixture = new Fixture(); 87 | var project = Fixture.GetPath("Baz/Baz.csproj"); 88 | 89 | // When 90 | var (exitCode, output) = await Fixture.Run(project, "--exclude", "Autofac"); 91 | 92 | // Then 93 | exitCode.ShouldBe(0); 94 | await Verifier.Verify(output); 95 | } 96 | 97 | [Fact] 98 | [Expectation("Baz", "Skip_Bar")] 99 | public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project() 100 | { 101 | // Given 102 | var fixture = new Fixture(); 103 | var project = Fixture.GetPath("Baz/Baz.csproj"); 104 | 105 | // When 106 | var (exitCode, output) = await Fixture.Run(project, "--skip", "Bar"); 107 | 108 | // Then 109 | exitCode.ShouldBe(0); 110 | await Verifier.Verify(output); 111 | } 112 | 113 | [Fact] 114 | [Expectation("Baz", "Skip_Bar_NoPreRelease")] 115 | public async Task Should_Return_Expected_Result_For_Baz_When_Skipping_Project_And_NoReleases() 116 | { 117 | // Given 118 | var fixture = new Fixture(); 119 | var project = Fixture.GetPath("Baz/Baz.csproj"); 120 | 121 | // When 122 | var (exitCode, output) = await Fixture.Run(project, "--skip", "Bar", "--no-prerelease"); 123 | 124 | // Then 125 | exitCode.ShouldBe(0); 126 | await Verifier.Verify(output); 127 | } 128 | 129 | [Fact] 130 | [Expectation("Baz", "netstandard2.Strict.NoPreRelease")] 131 | public async Task Should_Return_Non_Zero_Exit_Code_For_Baz_When_Running_With_Strict_And_NoPreRelease() 132 | { 133 | // Given 134 | var fixture = new Fixture(); 135 | var project = Fixture.GetPath("Baz/Baz.csproj"); 136 | 137 | // When 138 | var (exitCode, output) = await Fixture.Run(project, "--no-prerelease", "--strict"); 139 | 140 | // Then 141 | exitCode.ShouldBe(-1); 142 | await Verifier.Verify(output); 143 | } 144 | 145 | [Fact] 146 | [Expectation("Thud", "netstandard2.Strict.NoPreRelease")] 147 | public async Task Should_Return_Non_Zero_Exit_Code_For_Thud_When_Running_With_Strict_And_NoPreRelease() 148 | { 149 | // Given 150 | var fixture = new Fixture(); 151 | var project = Fixture.GetPath("Thud/Thud.csproj"); 152 | 153 | // When 154 | var (exitCode, output) = await Fixture.Run(project, "--no-prerelease", "--strict"); 155 | 156 | // Then 157 | exitCode.ShouldBe(-1); 158 | await Verifier.Verify(output); 159 | } 160 | 161 | [Fact] 162 | [Expectation("Thuuud", "netstandard2.Strict.NoPreRelease")] 163 | public async Task Should_Return_Non_Zero_Exit_Code_For_Thuuud_When_Running_With_Strict_And_NoPreRelease() 164 | { 165 | // Given 166 | var fixture = new Fixture(); 167 | var project = Fixture.GetPath("Thuuud/Thuuud.csproj"); 168 | 169 | // When 170 | var (exitCode, output) = await Fixture.Run(project, "--no-prerelease", "--strict"); 171 | 172 | // Then 173 | exitCode.ShouldBe(-1); 174 | await Verifier.Verify(output); 175 | } 176 | 177 | [Fact] 178 | [Expectation("Thuuud", "netstandard2.NoPreRelease")] 179 | public async Task Should_Return_Zero_Exit_Code_For_Thuuud_When_Running_With_NoPreRelease() 180 | { 181 | // Given 182 | var fixture = new Fixture(); 183 | var project = Fixture.GetPath("Thuuud/Thuuud.csproj"); 184 | 185 | // When 186 | var (exitCode, output) = await Fixture.Run(project, "--no-prerelease"); 187 | 188 | // Then 189 | exitCode.ShouldBe(0); 190 | await Verifier.Verify(output); 191 | } 192 | 193 | public sealed class Fixture 194 | { 195 | public static string GetPath(string path) 196 | { 197 | var workingDirectory = Environment.CurrentDirectory; 198 | var solutionDirectory = Path.GetFullPath(Path.Combine(workingDirectory, "../../../../Snitch.Tests.Fixtures")); 199 | return Path.GetFullPath(Path.Combine(solutionDirectory, path)); 200 | } 201 | 202 | public static async Task<(int exitCode, string output)> Run(params string[] args) 203 | { 204 | var console = new TestConsole { EmitAnsiSequences = false }; 205 | var exitCode = await Program.Run(args, c => c.ConfigureConsole(console)); 206 | return (exitCode, console.Output.Trim()); 207 | } 208 | } 209 | 210 | [Fact] 211 | [Expectation("FSharp", "Default")] 212 | public async Task Should_Return_Expected_Result_For_FSharp_Not_Specifying_Framework() 213 | { 214 | // Given 215 | var fixture = new Fixture(); 216 | var project = Fixture.GetPath("FSharp/FSharp.fsproj"); 217 | 218 | // When 219 | var (exitCode, output) = await Fixture.Run(project); 220 | 221 | // Then 222 | exitCode.ShouldBe(0); 223 | await Verifier.Verify(output); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Snitch.Tests/Snitch.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Snitch.Tests/VerifyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using VerifyTests; 3 | using VerifyXunit; 4 | 5 | namespace Sntich.Tests 6 | { 7 | public static class VerifyConfiguration 8 | { 9 | [ModuleInitializer] 10 | public static void Init() 11 | { 12 | Verifier.DerivePathInfo(Expectations.Initialize); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Snitch.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29326.143 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snitch", "Snitch\Snitch.csproj", "{945553B5-F075-4455-88CF-D59A5F7F1A81}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snitch.Tests", "Snitch.Tests\Snitch.Tests.csproj", "{07CAD001-A0AE-45EF-9069-A14AEE6F99AF}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|x64.ActiveCfg = Debug|Any CPU 23 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|x64.Build.0 = Debug|Any CPU 24 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|x86.ActiveCfg = Debug|Any CPU 25 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Debug|x86.Build.0 = Debug|Any CPU 26 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|x64.ActiveCfg = Release|Any CPU 29 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|x64.Build.0 = Release|Any CPU 30 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|x86.ActiveCfg = Release|Any CPU 31 | {945553B5-F075-4455-88CF-D59A5F7F1A81}.Release|x86.Build.0 = Release|Any CPU 32 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|x64.ActiveCfg = Debug|Any CPU 35 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|x64.Build.0 = Debug|Any CPU 36 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|x86.ActiveCfg = Debug|Any CPU 37 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Debug|x86.Build.0 = Debug|Any CPU 38 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|x64.ActiveCfg = Release|Any CPU 41 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|x64.Build.0 = Release|Any CPU 42 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|x86.ActiveCfg = Release|Any CPU 43 | {07CAD001-A0AE-45EF-9069-A14AEE6F99AF}.Release|x86.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {8BEC268B-CF39-4103-B514-DC9897D72C14} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /src/Snitch/Analysis/Models/Package.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using NuGet.Versioning; 4 | 5 | namespace Snitch.Analysis 6 | { 7 | [DebuggerDisplay("{Name,nq} ({Version,nq})")] 8 | internal sealed class Package 9 | { 10 | public string Name { get; } 11 | public NuGetVersion? Version { get; set; } 12 | public VersionRange? Range { get; } 13 | 14 | public string? PrivateAssets { get; } 15 | 16 | public bool IsGreaterThan(Package package, out bool indeterminate) 17 | { 18 | indeterminate = true; 19 | 20 | if (Version != null && package.Version != null) 21 | { 22 | // Version > Version 23 | indeterminate = false; 24 | return new VersionComparer().Compare(Version, package.Version) > 0; 25 | } 26 | else if (Version != null && package.Range != null) 27 | { 28 | // Version > Range 29 | return package.Range.Satisfies(Version); 30 | } 31 | else if (Range != null && package.Range != null) 32 | { 33 | // Range > Range 34 | indeterminate = false; 35 | return new VersionComparer().Compare(Range.MaxVersion, package.Range.MaxVersion) > 0; 36 | } 37 | else if (Range != null && package.Version != null) 38 | { 39 | // Range > Version 40 | return Range.Satisfies(package.Version); 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public bool IsSameVersion(Package package) 47 | { 48 | if (Version != null && package.Version != null) 49 | { 50 | // Version == Version 51 | return new VersionComparer().Equals(Version, package.Version); 52 | } 53 | else if (Range != null && package.Range != null) 54 | { 55 | // Range == Range 56 | new VersionRangeComparer().Equals(Range, package.Range); 57 | } 58 | 59 | return false; 60 | } 61 | 62 | public string GetVersionString() 63 | { 64 | if (Version != null) 65 | { 66 | return Version.ToString(); 67 | } 68 | 69 | return Range?.OriginalString ?? "?"; 70 | } 71 | 72 | public Package(string name, string version, string? privateAssets) 73 | { 74 | Name = name ?? throw new ArgumentNullException(nameof(name)); 75 | PrivateAssets = privateAssets; 76 | 77 | if (NuGetVersion.TryParse(version, out var semanticVersion)) 78 | { 79 | Version = semanticVersion; 80 | Range = null; 81 | } 82 | else 83 | { 84 | if (!VersionRange.TryParse(version, out var range)) 85 | { 86 | throw new ArgumentException($"Version '{version}' for package '{name}' is not valid.", nameof(version)); 87 | } 88 | 89 | Version = null; 90 | Range = range; 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/Models/PackageToRemove.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Snitch.Analysis 5 | { 6 | [DebuggerDisplay("{PackageDescription(),nq}")] 7 | internal sealed class PackageToRemove 8 | { 9 | public Project Project { get; } 10 | public Package Package { get; } 11 | public ProjectPackage Original { get; } 12 | 13 | public bool CanBeRemoved => Package.IsSameVersion(Original.Package); 14 | public bool VersionMismatch => !Package.IsSameVersion(Original.Package); 15 | 16 | public PackageToRemove(Project project, Package package, ProjectPackage original) 17 | { 18 | Project = project ?? throw new ArgumentNullException(nameof(project)); 19 | Package = package ?? throw new ArgumentNullException(nameof(package)); 20 | Original = original ?? throw new ArgumentNullException(nameof(original)); 21 | } 22 | 23 | private string PackageDescription() 24 | { 25 | return $"{Project.Name}: {Package.Name} ({Original.Project.Name})"; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/Models/Project.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Snitch.Analysis 6 | { 7 | [DebuggerDisplay("{GetProjectName(),nq}")] 8 | internal sealed class Project 9 | { 10 | public string Path { get; } 11 | public string File { get; } 12 | public string Name { get; } 13 | public string TargetFramework { get; set; } 14 | public string? LockFilePath { get; set; } 15 | public List ProjectReferences { get; } 16 | public List Packages { get; } 17 | 18 | public Project(string path) 19 | { 20 | Path = path ?? throw new ArgumentNullException(nameof(path)); 21 | File = System.IO.Path.GetFileName(Path); 22 | Name = System.IO.Path.GetFileNameWithoutExtension(Path); 23 | TargetFramework = string.Empty; 24 | ProjectReferences = new List(); 25 | Packages = new List(); 26 | } 27 | 28 | public void RemovePackages(IEnumerable packages) 29 | { 30 | if (packages != null) 31 | { 32 | foreach (var package in packages) 33 | { 34 | RemovePackage(package); 35 | } 36 | } 37 | } 38 | 39 | private void RemovePackage(string package) 40 | { 41 | Packages.RemoveAll(p => p.Name.Equals(package, StringComparison.OrdinalIgnoreCase)); 42 | foreach (var parentProject in ProjectReferences) 43 | { 44 | parentProject.RemovePackage(package); 45 | } 46 | } 47 | 48 | private string GetProjectName() 49 | { 50 | return System.IO.Path.GetFileName(Path); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/Models/ProjectComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Snitch.Analysis 6 | { 7 | internal sealed class ProjectComparer : IEqualityComparer 8 | { 9 | public bool Equals([AllowNull] Project x, [AllowNull] Project y) 10 | { 11 | if (x == null && y == null) 12 | { 13 | return true; 14 | } 15 | 16 | if (x == null || y == null) 17 | { 18 | return false; 19 | } 20 | 21 | return x.Path.Equals(y.Path, StringComparison.OrdinalIgnoreCase); 22 | } 23 | 24 | public int GetHashCode([DisallowNull] Project obj) 25 | { 26 | return obj?.Path?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/Models/ProjectPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Snitch.Analysis 5 | { 6 | [DebuggerDisplay("{PackageDescription(),nq}")] 7 | internal sealed class ProjectPackage 8 | { 9 | public Project Project { get; } 10 | public Package Package { get; } 11 | 12 | public ProjectPackage(Project project, Package package) 13 | { 14 | Project = project ?? throw new ArgumentNullException(nameof(project)); 15 | Package = package ?? throw new ArgumentNullException(nameof(package)); 16 | } 17 | 18 | private string PackageDescription() 19 | { 20 | return $"{Project.Name}: {Package.Name}"; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/ProjectAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Policy; 5 | using NuGet.Frameworks; 6 | using NuGet.LibraryModel; 7 | using NuGet.ProjectModel; 8 | 9 | namespace Snitch.Analysis 10 | { 11 | internal sealed class ProjectAnalyzer 12 | { 13 | public ProjectAnalyzerResult Analyze(Project project) 14 | { 15 | if (project == null) 16 | { 17 | throw new ArgumentNullException(nameof(project)); 18 | } 19 | 20 | // Analyze the project. 21 | var result = new List(); 22 | AnalyzeProject(project, project, result); 23 | 24 | if (project.LockFilePath != null) 25 | { 26 | // Now prune stuff that we're not interested in removing 27 | // such as private package references and analyzers. 28 | result = PruneResults(project, result); 29 | } 30 | 31 | return new ProjectAnalyzerResult(project, result); 32 | } 33 | 34 | private List AnalyzeProject(Project root, Project project, List result) 35 | { 36 | var accumulated = new List(); 37 | result ??= new List(); 38 | 39 | if (project.ProjectReferences.Count > 0) 40 | { 41 | // Iterate through all project references. 42 | foreach (var child in project.ProjectReferences) 43 | { 44 | // Analyze the project recursively. 45 | foreach (var item in AnalyzeProject(root, child, result)) 46 | { 47 | // Didn't exist previously in the list of accumulated packages? 48 | if (!accumulated.ContainsPackage(item.Package)) 49 | { 50 | accumulated.Add(new ProjectPackage(item.Project, item.Package)); 51 | } 52 | } 53 | } 54 | 55 | // Was any package in the current project references 56 | // by one of the projects referenced by the project? 57 | foreach (var package in project.Packages) 58 | { 59 | var found = accumulated.FindProjectPackage(package); 60 | if (found != null) 61 | { 62 | if (!result.ContainsPackage(found.Package)) 63 | { 64 | if (project.Name.Equals(root.Name, StringComparison.OrdinalIgnoreCase)) 65 | { 66 | result.Add(new PackageToRemove(project, package, found)); 67 | } 68 | } 69 | } 70 | else 71 | { 72 | AddToAccumulated(package); 73 | } 74 | } 75 | } 76 | else 77 | { 78 | foreach (var item in project.Packages) 79 | { 80 | if (!accumulated.ContainsPackage(item)) 81 | { 82 | AddToAccumulated(item); 83 | } 84 | } 85 | } 86 | 87 | void AddToAccumulated(Package package) 88 | { 89 | if (package.PrivateAssets != null && package.PrivateAssets.Contains("compile")) 90 | { 91 | return; 92 | } 93 | 94 | // Add the package to the list of accumulated packages. 95 | accumulated.Add(new ProjectPackage(project, package)); 96 | } 97 | 98 | return accumulated; 99 | } 100 | 101 | private static List PruneResults(Project project, List packages) 102 | { 103 | // Read the lockfile. 104 | var lockfile = new LockFileFormat().Read(project.LockFilePath); 105 | 106 | // Find the expected target. 107 | var framework = NuGetFramework.Parse(project.TargetFramework); 108 | var target = lockfile.PackageSpec.TargetFrameworks.FirstOrDefault( 109 | x => x.FrameworkName.Framework.Equals(framework.Framework, StringComparison.OrdinalIgnoreCase)); 110 | 111 | // Could we not find the target? 112 | if (target == null) 113 | { 114 | throw new InvalidOperationException("Could not determine target framework"); 115 | } 116 | 117 | var result = new List(); 118 | foreach (var package in packages) 119 | { 120 | // Try to find the dependency. 121 | var dependency = target.Dependencies.FirstOrDefault( 122 | x => x.Name.Equals(package.Package.Name, StringComparison.OrdinalIgnoreCase)); 123 | 124 | if (dependency != null) 125 | { 126 | // Auto referenced or private package? 127 | if (dependency.AutoReferenced || 128 | dependency.SuppressParent == LibraryIncludeFlags.All) 129 | { 130 | continue; 131 | } 132 | } 133 | 134 | result.Add(package); 135 | } 136 | 137 | return result; 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/ProjectAnalyzerResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Snitch.Analysis 6 | { 7 | internal sealed class ProjectAnalyzerResult 8 | { 9 | private readonly Project _project; 10 | private readonly List _packages; 11 | 12 | public string Project => _project.Name; 13 | public IReadOnlyList CanBeRemoved { get; } 14 | public IReadOnlyList MightBeRemoved { get; } 15 | public IReadOnlyList PreReleasePackages { get; } 16 | 17 | public bool NoPackagesToRemove => CanBeRemoved.Count == 0 && MightBeRemoved.Count == 0; 18 | 19 | public bool HasPreReleases => PreReleasePackages.Count > 0; 20 | 21 | public ProjectAnalyzerResult(Project project, IEnumerable packages) 22 | { 23 | _project = project; 24 | _packages = new List(packages ?? throw new ArgumentNullException(nameof(packages))); 25 | 26 | CanBeRemoved = new List(packages.Where(p => p.CanBeRemoved)); 27 | MightBeRemoved = new List(packages.Where(p => p.VersionMismatch)); 28 | PreReleasePackages = new List(project.Packages.Where(p => p.Version != null && p.Version.IsPrerelease)); 29 | } 30 | 31 | public ProjectAnalyzerResult Filter(string[]? packages) 32 | { 33 | if (packages == null) 34 | { 35 | return this; 36 | } 37 | 38 | var filtered = _packages.Where(p => !packages.Contains(p.Package.Name, StringComparer.OrdinalIgnoreCase)); 39 | return new ProjectAnalyzerResult(_project, filtered); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/ProjectBuildResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Snitch.Analysis 5 | { 6 | internal sealed class ProjectBuildResult 7 | { 8 | public Project Project { get; } 9 | public IReadOnlyList Dependencies { get; } 10 | 11 | public ProjectBuildResult(Project project, IEnumerable dependencies) 12 | { 13 | Project = project; 14 | Dependencies = new List(dependencies ?? Enumerable.Empty()); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/ProjectBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Buildalyzer; 6 | using Snitch.Analysis.Utilities; 7 | using Spectre.Console; 8 | 9 | namespace Snitch.Analysis 10 | { 11 | internal sealed class ProjectBuilder 12 | { 13 | private readonly IAnsiConsole _console; 14 | 15 | public ProjectBuilder(IAnsiConsole console) 16 | { 17 | _console = console ?? throw new ArgumentNullException(nameof(console)); 18 | } 19 | 20 | public ProjectBuildResult Build( 21 | string path, 22 | string? tfm, 23 | string[]? skip, 24 | IEnumerable? cache = null) 25 | { 26 | var manager = new AnalyzerManager(); 27 | var built = cache?.ToDictionary(x => x.File, x => x, StringComparer.OrdinalIgnoreCase) 28 | ?? new Dictionary(StringComparer.OrdinalIgnoreCase); 29 | 30 | var project = Build(manager, path, tfm, skip, built); 31 | 32 | // Get all dependencies which are all built projects minus the project. 33 | var dependencies = new HashSet(built.Values, new ProjectComparer()); 34 | dependencies.Remove(project); 35 | 36 | return new ProjectBuildResult(project, dependencies); 37 | } 38 | 39 | private Project Build( 40 | AnalyzerManager manager, 41 | string path, 42 | string? tfm, 43 | string[]? skip, 44 | Dictionary built, 45 | int indentation = 0) 46 | { 47 | if (manager == null) 48 | { 49 | throw new ArgumentNullException(nameof(manager)); 50 | } 51 | 52 | if (built == null) 53 | { 54 | throw new ArgumentNullException(nameof(built)); 55 | } 56 | 57 | path = Path.GetFullPath(path); 58 | 59 | // Already built this project? 60 | if (built.TryGetValue(Path.GetFileName(path), out var project)) 61 | { 62 | return project; 63 | } 64 | 65 | project = new Project(path); 66 | 67 | var result = Build(manager, project, tfm, indentation); 68 | if (result == null) 69 | { 70 | throw new InvalidOperationException($"Could not build {path}."); 71 | } 72 | 73 | // Get the asset path. 74 | var assetPath = result.GetProjectAssetsFilePath(); 75 | if (!string.IsNullOrWhiteSpace(assetPath)) 76 | { 77 | if (!File.Exists(assetPath)) 78 | { 79 | // Todo: Make sure this exists in future 80 | throw new InvalidOperationException($"{assetPath} not found. Please restore the project's dependencies before running Snitch."); 81 | } 82 | } 83 | else 84 | { 85 | var prefix = new string(' ', indentation * 2); 86 | if (indentation > 0) 87 | { 88 | prefix += " "; 89 | } 90 | 91 | _console.MarkupLine($"{prefix}[yellow]WARN:[/] Old CSPROJ format can't be analyzed"); 92 | } 93 | 94 | // Set project information. 95 | project.TargetFramework = result.TargetFramework; 96 | project.LockFilePath = assetPath; 97 | 98 | // Add the project to the built list. 99 | built.Add(Path.GetFileName(path), project); 100 | 101 | // Get the package references. 102 | foreach (var packageReference in result.PackageReferences) 103 | { 104 | if (packageReference.Value.TryGetValue("Version", out var version)) 105 | { 106 | var privateAssets = packageReference.Value.GetValueOrDefault("PrivateAssets"); 107 | 108 | project.Packages.Add(new Package(packageReference.Key, version, privateAssets)); 109 | } 110 | } 111 | 112 | // Analyze all project references. 113 | foreach (var projectReference in result.ProjectReferences) 114 | { 115 | var projectReferencePath = PathUtility.GetPathRelativeToProject(project, projectReference); 116 | 117 | if (skip != null) 118 | { 119 | var projectName = Path.GetFileNameWithoutExtension(projectReferencePath); 120 | if (skip.Contains(projectName, StringComparer.OrdinalIgnoreCase)) 121 | { 122 | continue; 123 | } 124 | } 125 | 126 | if (!projectReferencePath.EndsWith("csproj", StringComparison.OrdinalIgnoreCase) && !projectReferencePath.EndsWith("fsproj", StringComparison.OrdinalIgnoreCase)) 127 | { 128 | _console.MarkupLine(string.IsNullOrWhiteSpace(tfm) 129 | ? $"Skipping Non .NET Project [aqua]{project.Name}[/]" 130 | : $"Skipping Non .NET Project [aqua]{project.Name}[/] [grey] ({tfm})[/]"); 131 | 132 | _console.WriteLine(); 133 | 134 | continue; 135 | } 136 | 137 | var analyzedProjectReference = Build(manager, projectReferencePath, project.TargetFramework, skip, built, indentation + 1); 138 | project.ProjectReferences.Add(analyzedProjectReference); 139 | } 140 | 141 | return project; 142 | } 143 | 144 | private IAnalyzerResult? Build(AnalyzerManager manager, Project project, string? tfm, int indentation) 145 | { 146 | var prefix = new string(' ', indentation * 2); 147 | if (indentation > 0) 148 | { 149 | prefix += "[grey]*[/] "; 150 | } 151 | 152 | var status = string.IsNullOrWhiteSpace(tfm) 153 | ? $"{prefix}Analyzing [aqua]{project.Name}[/]..." 154 | : $"{prefix}Analyzing [aqua]{project.Name}[/] [grey]({tfm})[/]..."; 155 | 156 | _console.MarkupLine(status); 157 | 158 | var projectAnalyzer = manager.GetProject(project.Path); 159 | var results = (IEnumerable)projectAnalyzer.Build(); 160 | 161 | if (!string.IsNullOrWhiteSpace(tfm)) 162 | { 163 | var closest = results.GetNearestFrameworkMoniker(tfm); 164 | results = results.Where(p => p.TargetFramework.Equals(closest, StringComparison.OrdinalIgnoreCase)); 165 | } 166 | 167 | return results.FirstOrDefault(); 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/ProjectReporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using Spectre.Console; 6 | 7 | namespace Snitch.Analysis 8 | { 9 | internal class ProjectReporter 10 | { 11 | private readonly IAnsiConsole _console; 12 | 13 | public ProjectReporter(IAnsiConsole console) 14 | { 15 | _console = console ?? throw new ArgumentNullException(nameof(console)); 16 | } 17 | 18 | public void WriteToConsole([NotNull] List results, bool noPreRelease) 19 | { 20 | var resultsWithPackageToRemove = results.Where(r => r.CanBeRemoved.Count > 0).ToList(); 21 | var resultsWithPackageMayBeRemove = results.Where(r => r.MightBeRemoved.Count > 0).ToList(); 22 | var resultsWithPreReleases = results.Where(r => r.PreReleasePackages.Count > 0).ToList(); 23 | 24 | if (results.All(x => x.NoPackagesToRemove) && (!noPreRelease || resultsWithPreReleases.Count == 0)) 25 | { 26 | // Output the result. 27 | _console.WriteLine(); 28 | _console.MarkupLine("[green]Everything looks good![/]"); 29 | _console.WriteLine(); 30 | return; 31 | } 32 | 33 | var report = new Grid(); 34 | report.AddColumn(); 35 | 36 | if (resultsWithPackageToRemove.Count > 0) 37 | { 38 | foreach (var (_, _, last, result) in resultsWithPackageToRemove.Enumerate()) 39 | { 40 | var table = new Table().BorderColor(Color.Grey).Expand(); 41 | table.AddColumns("[grey]Package[/]", "[grey]Referenced by[/]"); 42 | foreach (var item in result.CanBeRemoved) 43 | { 44 | table.AddRow( 45 | $"[green]{item.Package.Name}[/]", 46 | $"[aqua]{item.Original.Project.Name}[/]"); 47 | } 48 | 49 | report.AddRow($" [yellow]Packages that can be removed from[/] [aqua]{result.Project}[/]:"); 50 | report.AddRow(table); 51 | 52 | if (!last || (last && resultsWithPackageMayBeRemove.Count > 0)) 53 | { 54 | report.AddEmptyRow(); 55 | } 56 | } 57 | } 58 | 59 | if (resultsWithPackageMayBeRemove.Count > 0) 60 | { 61 | foreach (var (_, _, last, result) in resultsWithPackageMayBeRemove.Enumerate()) 62 | { 63 | var table = new Table().BorderColor(Color.Grey).Expand(); 64 | table.AddColumns("[grey]Package[/]", "[grey]Version[/]", "[grey]Reason[/]"); 65 | 66 | foreach (var item in result.MightBeRemoved) 67 | { 68 | if (item.Package.IsGreaterThan(item.Original.Package, out var indeterminate)) 69 | { 70 | var name = item.Original.Project.Name; 71 | var version = item.Original.Package.GetVersionString(); 72 | var verb = indeterminate ? "Might be updated from" : "Updated from"; 73 | var reason = $"[grey]{verb}[/] [silver]{version}[/] [grey]in[/] [aqua]{name}[/]"; 74 | 75 | table.AddRow( 76 | $"[green]{item.Package.Name}[/]", 77 | item.Package.GetVersionString(), 78 | reason); 79 | } 80 | else 81 | { 82 | var name = item.Original.Project.Name; 83 | var version = item.Original.Package.GetVersionString(); 84 | var verb = indeterminate ? "Does not match" : "Downgraded from"; 85 | var reason = $"[grey]{verb}[/] [silver]{version}[/] [grey]in[/] [aqua]{name}[/]"; 86 | 87 | table.AddRow( 88 | $"[green]{item.Package.Name}[/]", 89 | item.Package.GetVersionString(), 90 | reason); 91 | } 92 | } 93 | 94 | report.AddRow($" [yellow]Packages that [u]might[/] be removed from[/] [aqua]{result.Project}[/]:"); 95 | report.AddRow(table); 96 | 97 | if (!last) 98 | { 99 | report.AddEmptyRow(); 100 | } 101 | } 102 | } 103 | 104 | if (noPreRelease && resultsWithPreReleases.Count > 0) 105 | { 106 | report.AddEmptyRow(); 107 | report.AddRow($" [yellow]Projects with pre-release package references:[/]"); 108 | var packagesByProject = resultsWithPreReleases.SelectMany(x => x.PreReleasePackages, (project, package) => new 109 | { 110 | Project = project.Project, 111 | PackageName = package.Name, 112 | Version = package.Version, 113 | }) 114 | .OrderBy(o => o.Project) 115 | .ToList(); 116 | 117 | var table = new Table().BorderColor(Color.Grey).Expand(); 118 | table.AddColumns("[grey]Project[/]", "[grey]Package[/]", "[grey]Version[/]"); 119 | foreach (var item in packagesByProject) 120 | { 121 | table.AddRow( 122 | $"[green]{item.Project}[/]", 123 | $"[yellow]{item.PackageName}[/]", 124 | $"{item.Version}"); 125 | } 126 | 127 | report.AddRow(table); 128 | } 129 | 130 | _console.WriteLine(); 131 | _console.Write( 132 | new Panel(report) 133 | .RoundedBorder() 134 | .BorderColor(Color.Grey)); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/Snitch/Analysis/Utilities/PathUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Microsoft.Build.Construction; 6 | 7 | namespace Snitch.Analysis.Utilities 8 | { 9 | internal static class PathUtility 10 | { 11 | public static string GetPathRelativeToProject(Project root, string path) 12 | { 13 | var rootPath = Path.GetDirectoryName(root.Path); 14 | if (rootPath == null) 15 | { 16 | throw new InvalidOperationException("Could not get project root path."); 17 | } 18 | 19 | return Path.GetFullPath(Path.Combine(rootPath, path)); 20 | } 21 | 22 | public static List GetProjectPaths(string? path, out string entry) 23 | { 24 | if (!string.IsNullOrWhiteSpace(path)) 25 | { 26 | path = Path.GetFullPath(path); 27 | if (!File.Exists(path)) 28 | { 29 | if (Directory.Exists(path)) 30 | { 31 | return FindProjects(path, out entry); 32 | } 33 | 34 | throw new InvalidOperationException("Project or solution file do not exist."); 35 | } 36 | 37 | entry = path; 38 | return GetProjectsFromFile(path); 39 | } 40 | 41 | return FindProjects(null, out entry); 42 | } 43 | 44 | private static List GetProjectsFromFile(string path) 45 | { 46 | if (path.EndsWith("proj", StringComparison.InvariantCulture)) 47 | { 48 | return new List { path }; 49 | } 50 | 51 | if (path.EndsWith(".sln", StringComparison.InvariantCulture)) 52 | { 53 | return GetProjectsFromSolution(path); 54 | } 55 | 56 | throw new InvalidOperationException("Project or solution file do not exist."); 57 | } 58 | 59 | private static List FindProjects(string? root, out string entry) 60 | { 61 | root ??= Environment.CurrentDirectory; 62 | 63 | var slns = Directory.GetFiles(root, "*.sln"); 64 | 65 | if (slns.Length == 0) 66 | { 67 | var subProjects = Directory.GetFiles(root, "*.csproj"); 68 | if (subProjects.Length == 0) 69 | { 70 | throw new InvalidOperationException("No project or solution file found."); 71 | } 72 | else if (subProjects.Length > 1) 73 | { 74 | throw new InvalidOperationException("More than one project file found."); 75 | } 76 | 77 | entry = subProjects[0]; 78 | return new List(new[] { subProjects[0] }); 79 | } 80 | else if (slns.Length > 1) 81 | { 82 | throw new InvalidOperationException("More than one solution file found."); 83 | } 84 | else 85 | { 86 | entry = slns[0]; 87 | return GetProjectsFromSolution(slns[0]); 88 | } 89 | } 90 | 91 | private static List GetProjectsFromSolution(string solution) 92 | { 93 | var solutionFile = SolutionFile.Parse(solution); 94 | return solutionFile.ProjectsInOrder.Where(p => p.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat).Select(p => p.AbsolutePath).Distinct().ToList(); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/Snitch/Commands/AnalyzeCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Linq; 7 | using Snitch.Analysis; 8 | using Snitch.Analysis.Utilities; 9 | using Spectre.Console; 10 | using Spectre.Console.Cli; 11 | 12 | namespace Snitch.Commands 13 | { 14 | [Description("Shows transitive package dependencies that can be removed")] 15 | public sealed class AnalyzeCommand : Command 16 | { 17 | private readonly IAnsiConsole _console; 18 | private readonly ProjectBuilder _builder; 19 | private readonly ProjectAnalyzer _analyzer; 20 | private readonly ProjectReporter _reporter; 21 | 22 | public sealed class Settings : CommandSettings 23 | { 24 | [CommandArgument(0, "[PROJECT|SOLUTION]")] 25 | [Description("The project or solution you want to analyze.")] 26 | public string ProjectOrSolutionPath { get; set; } = string.Empty; 27 | 28 | [CommandOption("-t|--tfm ")] 29 | [Description("The target framework moniker to analyze.")] 30 | public string? TargetFramework { get; set; } 31 | 32 | [CommandOption("-e|--exclude ")] 33 | [Description("One or more packages to exclude.")] 34 | public string[]? Exclude { get; set; } 35 | 36 | [CommandOption("--skip ")] 37 | [Description("One or more project references to exclude.")] 38 | public string[]? Skip { get; set; } 39 | 40 | [CommandOption("-s|--strict")] 41 | [Description("Returns exit code 0 only if no conflicts were found.")] 42 | public bool Strict { get; set; } 43 | 44 | [CommandOption("--no-prerelease")] 45 | [Description("Verifies that all package references are not pre-releases.")] 46 | public bool NoPreRelease { get; set; } 47 | } 48 | 49 | public AnalyzeCommand(IAnsiConsole console) 50 | { 51 | _console = console ?? throw new ArgumentNullException(nameof(console)); 52 | _builder = new ProjectBuilder(console); 53 | _analyzer = new ProjectAnalyzer(); 54 | _reporter = new ProjectReporter(console); 55 | } 56 | 57 | public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) 58 | { 59 | var projectsToAnalyze = PathUtility.GetProjectPaths(settings.ProjectOrSolutionPath, out var entry); 60 | 61 | // Remove all projects that we want to skip. 62 | projectsToAnalyze.RemoveAll(p => 63 | { 64 | var projectName = Path.GetFileNameWithoutExtension(p); 65 | return settings.Skip?.Contains(projectName, StringComparer.OrdinalIgnoreCase) ?? false; 66 | }); 67 | 68 | var targetFramework = settings.TargetFramework; 69 | var analyzerResults = new List(); 70 | var projectCache = new HashSet(new ProjectComparer()); 71 | 72 | _console.WriteLine(); 73 | 74 | return _console.Status().Start($"Analyzing...", ctx => 75 | { 76 | ctx.Refresh(); 77 | 78 | _console.MarkupLine($"Analyzing [yellow]{Path.GetFileName(entry)}[/]"); 79 | 80 | foreach (var projectToAnalyze in projectsToAnalyze) 81 | { 82 | // Perform a design time build of the project. 83 | var buildResult = _builder.Build( 84 | projectToAnalyze, 85 | targetFramework, 86 | settings.Skip, 87 | projectCache); 88 | 89 | // Update the cache of built projects. 90 | projectCache.Add(buildResult.Project); 91 | foreach (var item in buildResult.Dependencies) 92 | { 93 | projectCache.Add(item); 94 | } 95 | 96 | // Analyze the project. 97 | var analyzeResult = _analyzer.Analyze(buildResult.Project); 98 | if (settings.Exclude?.Length > 0) 99 | { 100 | // Filter packages that should be excluded. 101 | analyzeResult = analyzeResult.Filter(settings.Exclude); 102 | } 103 | 104 | analyzerResults.Add(analyzeResult); 105 | } 106 | 107 | // Write the report to the console 108 | _reporter.WriteToConsole(analyzerResults, settings.NoPreRelease); 109 | 110 | // Return the correct exit code. 111 | return GetExitCode(settings, analyzerResults); 112 | }); 113 | } 114 | 115 | private static int GetExitCode(Settings settings, List result) 116 | { 117 | if (settings.Strict && (result.Any(r => !r.NoPackagesToRemove) || (settings.NoPreRelease && result.Any(r => r.HasPreReleases)))) 118 | { 119 | return -1; 120 | } 121 | 122 | return 0; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/Snitch/Commands/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Snitch.Commands 6 | { 7 | [Description("Prints the Snitch version number")] 8 | public sealed class VersionCommand : Command 9 | { 10 | public override int Execute(CommandContext context) 11 | { 12 | Console.WriteLine(typeof(VersionCommand).Assembly.GetName().Version); 13 | return 0; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Snitch/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Snitch.Commands; 4 | using Snitch.Utilities; 5 | using Spectre.Console.Cli; 6 | 7 | namespace Snitch 8 | { 9 | public static class Program 10 | { 11 | public static async Task Main(string[] args) 12 | { 13 | return await Run(args); 14 | } 15 | 16 | public static async Task Run(string[] args, Action? configator = null) 17 | { 18 | var typeRegistrar = new TypeRegistrar(); 19 | typeRegistrar.Register(typeof(AnalyzeCommand.Settings), typeof(AnalyzeCommand.Settings)); 20 | 21 | var app = new CommandApp(typeRegistrar); 22 | 23 | app.Configure(config => 24 | { 25 | config.SetApplicationName("snitch"); 26 | configator?.Invoke(config); 27 | 28 | config.UseStrictParsing(); 29 | config.ValidateExamples(); 30 | 31 | config.AddExample(new[] { "Project.csproj" }); 32 | config.AddExample(new[] { "Project.csproj", "-e", "Foo", "-e", "Bar" }); 33 | config.AddExample(new[] { "Project.csproj", "--tfm", "net462" }); 34 | config.AddExample(new[] { "Project.csproj", "--tfm", "net462", "--strict" }); 35 | 36 | config.AddExample(new[] { "Solution.sln" }); 37 | config.AddExample(new[] { "Solution.sln", "-e", "Foo", "-e", "Bar" }); 38 | config.AddExample(new[] { "Solution.sln", "--tfm", "net462" }); 39 | config.AddExample(new[] { "Solution.sln", "--tfm", "net462", "--strict" }); 40 | 41 | config.AddCommand("version"); 42 | }); 43 | 44 | return await app.RunAsync(args); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Snitch/Snitch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | 10.0 6 | net8.0 7 | 1701;1702;SA0001 8 | enable 9 | true 10 | snitch 11 | icon.png 12 | 13 | 14 | 15 | 16 | A tool that help you find transitive package references. 17 | Spectre Systems AB 18 | Spectre Systems AB 19 | Patrik Svensson 20 | MIT 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | All 37 | 38 | 39 | All 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Snitch/Utilities/Extensions/AnalyzerResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Buildalyzer; 4 | using NuGet.Frameworks; 5 | 6 | namespace Snitch.Analysis 7 | { 8 | internal static class AnalyzerResultExtensions 9 | { 10 | public static string? GetProjectAssetsFilePath(this IAnalyzerResult result) 11 | { 12 | return result?.GetProperty("ProjectAssetsFile"); 13 | } 14 | 15 | public static string GetNearestFrameworkMoniker(this IEnumerable source, string framework) 16 | { 17 | var current = NuGetFramework.Parse(framework, DefaultFrameworkNameProvider.Instance); 18 | return current.GetNearestFrameworkMoniker(source.Select(x => x.TargetFramework)); 19 | } 20 | 21 | private static string GetNearestFrameworkMoniker(this NuGetFramework framework, IEnumerable candidates) 22 | { 23 | var provider = DefaultFrameworkNameProvider.Instance; 24 | var reducer = new FrameworkReducer(); 25 | 26 | var mappings = new Dictionary( 27 | candidates.ToDictionary( 28 | x => NuGetFramework.Parse(x, provider), y => y, new NuGetFrameworkFullComparer())); 29 | 30 | return mappings[reducer.GetNearest(framework, mappings.Keys)]; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Snitch/Utilities/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Snitch 5 | { 6 | public static class EnumerableExtensions 7 | { 8 | public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate(this IEnumerable source) 9 | { 10 | if (source is null) 11 | { 12 | throw new ArgumentNullException(nameof(source)); 13 | } 14 | 15 | return Enumerate(source.GetEnumerator()); 16 | } 17 | 18 | public static IEnumerable<(int Index, bool First, bool Last, T Item)> Enumerate(this IEnumerator source) 19 | { 20 | if (source is null) 21 | { 22 | throw new ArgumentNullException(nameof(source)); 23 | } 24 | 25 | var first = true; 26 | var last = !source.MoveNext(); 27 | T current; 28 | 29 | for (var index = 0; !last; index++) 30 | { 31 | current = source.Current; 32 | last = !source.MoveNext(); 33 | yield return (index, first, last, current); 34 | first = false; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Snitch/Utilities/Extensions/PackageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Snitch.Analysis 6 | { 7 | internal static class PackageExtensions 8 | { 9 | public static bool ContainsPackage(this IEnumerable source, Package package) 10 | { 11 | return source.Any(x => x.Package.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); 12 | } 13 | 14 | public static ProjectPackage? FindProjectPackage(this IEnumerable source, Package package) 15 | { 16 | return source.FirstOrDefault(p => p.Package.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); 17 | } 18 | 19 | public static bool ContainsPackage(this IEnumerable source, Package package) 20 | { 21 | return source.Any(x => x.Package.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Snitch/Utilities/TypeRegistrar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Snitch.Utilities 6 | { 7 | internal sealed class TypeRegistrar : ITypeRegistrar 8 | { 9 | private readonly ContainerBuilder _builder; 10 | 11 | public TypeRegistrar() 12 | { 13 | _builder = new ContainerBuilder(); 14 | } 15 | 16 | public ITypeResolver Build() 17 | { 18 | return new TypeResolver(_builder.Build()); 19 | } 20 | 21 | public void Register(Type service, Type implementation) 22 | { 23 | _builder.RegisterType(implementation).As(service).SingleInstance(); 24 | } 25 | 26 | public void RegisterInstance(Type service, object implementation) 27 | { 28 | _builder.RegisterInstance(implementation).As(service); 29 | } 30 | 31 | public void RegisterLazy(Type service, Func factory) 32 | { 33 | _builder.Register(_ => factory()).As(service); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Snitch/Utilities/TypeResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autofac; 3 | using Spectre.Console.Cli; 4 | 5 | namespace Snitch.Utilities 6 | { 7 | internal sealed class TypeResolver : ITypeResolver 8 | { 9 | private readonly IContainer _scope; 10 | 11 | public TypeResolver(IContainer scope) 12 | { 13 | _scope = scope ?? throw new ArgumentNullException(nameof(scope)); 14 | } 15 | 16 | public object? Resolve(Type? type) 17 | { 18 | if (type is null) 19 | { 20 | throw new ArgumentNullException(nameof(type)); 21 | } 22 | 23 | return _scope.Resolve(type); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectresystems/snitch/21fb94c97a001f6beb639ed0eda58198b15b9ac6/src/icon.png -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Convention": "InPlaceOverwrite" 3 | } --------------------------------------------------------------------------------