diff --git a/README.md b/README.md index 088be4b..c73b54f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ Easily identify which dependencies can be removed from an MSBuild project. +## Rules +| Id | Description | +|--------|-------------| +| RT0000 | Enable documentation generation for accuracy of used references detection | +| RT0001 | Unnecessary reference | +| RT0002 | Unnecessary project reference | +| RT0003 | Unnecessary package reference | + ## How to use Add a package reference to the [ReferenceTrimmer](https://www.nuget.org/packages/ReferenceTrimmer) package in your projects. The package contains build logic to emit warnings when unused dependencies are detected. @@ -43,19 +51,18 @@ You'll need to enable C# documentation XML generation to ensure good analysis re Note: To get better results, enable the [`IDE0005`](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005) unnecessary `using` rule. This avoids the C# compiler seeing a false positive assembly usage from unneeded `using` directives causing it to miss a removable dependency. See also the note for why IDE0005 code analysis rule requires `` property to be enabled. Documentation generation is also required for accuracy of used references detection (based on https://github.com/dotnet/roslyn/issues/66188). +## Disabling a rule on a reference +To turn off a rule on a specific project or package reference, add the relevant RTxxxx code to a NoWarn metadata attribute. For example: + +```xml + +``` + ## Configuration `$(EnableReferenceTrimmer)` - Controls whether the build logic should run for a given project. Defaults to `true`. -## Rules -| Id | Description | -|--------|-------------| -| RT0000 | Enable documentation generation for accuracy of used references detection | -| RT0001 | Unnecessary reference | -| RT0002 | Unnecessary project reference | -| RT0003 | Unnecessary package reference | - ## How does it work? -There are two main pieces to the package. First there is an MSBuild task which collects all refernces passed to the compiler. There is also a Roslyn Analyzer which uses the [`GetUsedAssemblyReferences`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.compilation.getusedassemblyreferences) analyzer API which is available starting with Roslyn compiler that shipped with Visual Studio 2019 version 16.10, .NET 5. (see https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning). This is the compiler telling us exactly what references were needed as part of compilation. The analyzer then compares the set of references the Task gathered with the references the compiler says were used. +There are two main pieces to the package. First there is an MSBuild task which collects all references passed to the compiler. There is also a Roslyn Analyzer which uses the [`GetUsedAssemblyReferences`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.compilation.getusedassemblyreferences) analyzer API which is available starting with Roslyn compiler that shipped with Visual Studio 2019 version 16.10, .NET 5. (see https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning). This is the compiler telling us exactly what references were needed as part of compilation. The analyzer then compares the set of references the Task gathered with the references the compiler says were used. ## Future development The outcome of https://github.com/dotnet/sdk/issues/10414 may be of use for `ReferenceTrimmer` future updates. diff --git a/src/E2ETests/E2ETests.cs b/src/E2ETests/E2ETests.cs index 185779a..982f2de 100644 --- a/src/E2ETests/E2ETests.cs +++ b/src/E2ETests/E2ETests.cs @@ -47,6 +47,14 @@ public void UnusedProjectReference() }); } + [TestMethod] + public void UnusedProjectReferenceNoWarn() + { + RunMSBuild( + projectFile: "Library/Library.csproj", + expectedWarnings: Array.Empty()); + } + [TestMethod] public void UnusedTransitiveProjectReference() { @@ -114,6 +122,19 @@ public void UnusedReferenceHintPath() }); } + [TestMethod] + public void UnusedReferenceHintPathNoWarn() + { + // For direct references, MSBuild can't determine build order so we need to ensure the dependency is already built + RunMSBuild( + projectFile: "Dependency/Dependency.csproj", + expectedWarnings: Array.Empty()); + + RunMSBuild( + projectFile: "Library/Library.csproj", + expectedWarnings: Array.Empty()); + } + [TestMethod] public void UnusedReferenceItemSpec() { @@ -134,6 +155,19 @@ public void UnusedReferenceItemSpec() }); } + [TestMethod] + public void UnusedReferenceItemSpecNoWarn() + { + // For direct references, MSBuild can't determine build order so we need to ensure the dependency is already built + RunMSBuild( + projectFile: "Dependency/Dependency.csproj", + expectedWarnings: Array.Empty()); + + RunMSBuild( + projectFile: "Library/Library.csproj", + expectedWarnings: Array.Empty()); + } + [TestMethod] public void UsedPackageReference() { @@ -161,6 +195,14 @@ public void UnusedPackageReference() }); } + [TestMethod] + public void UnusedPackageReferenceNoWarn() + { + RunMSBuild( + projectFile: "Library/Library.csproj", + expectedWarnings: Array.Empty()); + } + [TestMethod] public void UnusedPackageReferenceDocDisabled() { diff --git a/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.cs b/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.cs new file mode 100644 index 0000000..958c389 --- /dev/null +++ b/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.cs @@ -0,0 +1,7 @@ +namespace Library +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.csproj b/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.csproj new file mode 100644 index 0000000..3261f14 --- /dev/null +++ b/src/E2ETests/TestData/UnusedPackageReferenceNoWarn/Library/Library.csproj @@ -0,0 +1,12 @@ + + + + Library + net472 + + + + + + + diff --git a/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.cs b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.cs new file mode 100644 index 0000000..ad07022 --- /dev/null +++ b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.cs @@ -0,0 +1,7 @@ +namespace Dependency +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.csproj b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.cs b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.cs new file mode 100644 index 0000000..958c389 --- /dev/null +++ b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.cs @@ -0,0 +1,7 @@ +namespace Library +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.csproj b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.csproj new file mode 100644 index 0000000..7787015 --- /dev/null +++ b/src/E2ETests/TestData/UnusedProjectReferenceNoWarn/Library/Library.csproj @@ -0,0 +1,12 @@ + + + + Library + net472 + + + + + + + diff --git a/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.cs b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.cs new file mode 100644 index 0000000..ad07022 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.cs @@ -0,0 +1,7 @@ +namespace Dependency +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.csproj b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.cs b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.cs new file mode 100644 index 0000000..958c389 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.cs @@ -0,0 +1,7 @@ +namespace Library +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.csproj b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.csproj new file mode 100644 index 0000000..d7c55f5 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceHintPathNoWarn/Library/Library.csproj @@ -0,0 +1,15 @@ + + + + Library + net472 + + + + + ..\Dependency\$(OutputPath)\Dependency.dll + RT0001 + + + + diff --git a/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.cs b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.cs new file mode 100644 index 0000000..ad07022 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.cs @@ -0,0 +1,7 @@ +namespace Dependency +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.csproj b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.csproj new file mode 100644 index 0000000..33af96b --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Dependency/Dependency.csproj @@ -0,0 +1,8 @@ + + + + Library + net472 + + + diff --git a/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.cs b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.cs new file mode 100644 index 0000000..958c389 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.cs @@ -0,0 +1,7 @@ +namespace Library +{ + public static class Foo + { + public static string Bar() => "Baz"; + } +} diff --git a/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.csproj b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.csproj new file mode 100644 index 0000000..19146d3 --- /dev/null +++ b/src/E2ETests/TestData/UnusedReferenceItemSpecNoWarn/Library/Library.csproj @@ -0,0 +1,12 @@ + + + + Library + net472 + + + + + + + diff --git a/src/Tasks/CollectDeclaredReferencesTask.cs b/src/Tasks/CollectDeclaredReferencesTask.cs index 4791902..fb98ead 100644 --- a/src/Tasks/CollectDeclaredReferencesTask.cs +++ b/src/Tasks/CollectDeclaredReferencesTask.cs @@ -22,6 +22,8 @@ public sealed class CollectDeclaredReferencesTask : MSBuildTask "NuGet.Versioning", }; + private const string NoWarn = "NoWarn"; + [Required] public string? OutputFile { get; set; } @@ -78,6 +80,12 @@ public override bool Execute() continue; } + // Ignore suppressions + if (reference.GetMetadata(NoWarn).Contains("RT0001")) + { + continue; + } + var referenceSpec = reference.ItemSpec; var referenceHintPath = reference.GetMetadata("HintPath"); @@ -124,6 +132,12 @@ public override bool Execute() { foreach (ITaskItem projectReference in ProjectReferences) { + // Ignore suppressions + if (projectReference.GetMetadata(NoWarn).Contains("RT0002")) + { + continue; + } + // Weirdly, NuGet restore is actually how transitive project references are determined and they're // added to to project.assets.json and collected via the IncludeTransitiveProjectReferences target. // This also adds the NuGetPackageId metadata, so use that as a signal that it's transitive. @@ -146,6 +160,12 @@ public override bool Execute() Dictionary packageInfos = GetPackageInfos(); foreach (ITaskItem packageReference in PackageReferences) { + // Ignore suppressions + if (packageReference.GetMetadata(NoWarn).Contains("RT0003")) + { + continue; + } + if (!packageInfos.TryGetValue(packageReference.ItemSpec, out PackageInfo packageInfo)) { // These are likely Analyzers, tools, etc.