diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs index 0fa619f..b469fe7 100644 --- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs +++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs @@ -40,7 +40,7 @@ public class ReferenceTrimmerAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor RT0003Descriptor = new( "RT0003", "Unnecessary package reference", - "PackageReference {0} can be removed", + "PackageReference {0} can be removed{1}", "ReferenceTrimmer", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -140,6 +140,7 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context) } Dictionary> packageAssembliesDict = new(StringComparer.OrdinalIgnoreCase); + Dictionary> topLevelPackageAssembliesDict = new(StringComparer.OrdinalIgnoreCase); foreach (DeclaredReference declaredReference in ReadDeclaredReferences(sourceText)) { switch (declaredReference.Kind) @@ -166,11 +167,23 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context) { if (!packageAssembliesDict.TryGetValue(declaredReference.Spec, out List packageAssemblies)) { - packageAssemblies = new List(); + packageAssemblies = []; packageAssembliesDict.Add(declaredReference.Spec, packageAssemblies); } packageAssemblies.Add(declaredReference.AssemblyPath); + + bool isTopLevelPackageAssembly = string.Equals(declaredReference.Spec, declaredReference.AdditionalSpec, StringComparison.OrdinalIgnoreCase); + if (isTopLevelPackageAssembly) + { + if (!topLevelPackageAssembliesDict.TryGetValue(declaredReference.Spec, out List topLevelPackageAssemblies)) + { + topLevelPackageAssemblies = []; + topLevelPackageAssembliesDict.Add(declaredReference.Spec, topLevelPackageAssemblies); + } + + topLevelPackageAssemblies.Add(declaredReference.AssemblyPath); + } break; } } @@ -183,7 +196,11 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context) List packageAssemblies = kvp.Value; if (!usedReferences.Overlaps(packageAssemblies)) { - context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName)); + context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName, string.Empty)); + } + else if (!topLevelPackageAssembliesDict[packageName].Any(usedReferences.Contains)) + { + context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName, " (though some of its transitive dependent packages may be used)")); } } } @@ -232,7 +249,7 @@ private static void WriteFile(string filePath, string text) return null; } - // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line. + // File format: tab-separated fields (AssemblyPath, Kind, Spec, AdditionalSpec), one reference per line. // Keep in sync with SaveDeclaredReferences in CollectDeclaredReferencesTask.cs. private static IEnumerable ReadDeclaredReferences(SourceText sourceText) { @@ -250,6 +267,7 @@ private static IEnumerable ReadDeclaredReferences(SourceText int firstTab = -1; int secondTab = -1; + int thirdTab = -1; for (int i = start; i < end; i++) { if (sourceText[i] == '\t') @@ -258,21 +276,26 @@ private static IEnumerable ReadDeclaredReferences(SourceText { firstTab = i; } - else + else if (secondTab == -1) { secondTab = i; + } + else + { + thirdTab = i; break; } } } - if (firstTab == -1 || secondTab == -1) + if (firstTab == -1 || secondTab == -1 || thirdTab == -1) { yield break; } string assemblyPath = sourceText.ToString(TextSpan.FromBounds(start, firstTab)); - string spec = sourceText.ToString(TextSpan.FromBounds(secondTab + 1, end)); + string spec = sourceText.ToString(TextSpan.FromBounds(secondTab + 1, thirdTab)); + string additionalSpec = sourceText.ToString(TextSpan.FromBounds(thirdTab + 1, end)); // Determine kind without allocating a string. The three possible values are // "Reference" (len 9), "ProjectReference" (len 16), "PackageReference" (len 16). @@ -295,7 +318,7 @@ private static IEnumerable ReadDeclaredReferences(SourceText continue; } - yield return new DeclaredReference(assemblyPath, kind, spec); + yield return new DeclaredReference(assemblyPath, kind, spec, additionalSpec); } } } diff --git a/src/Shared/DeclaredReferences.cs b/src/Shared/DeclaredReferences.cs index fa9aa05..b21fd57 100644 --- a/src/Shared/DeclaredReferences.cs +++ b/src/Shared/DeclaredReferences.cs @@ -1,5 +1,5 @@ namespace ReferenceTrimmer.Shared; -internal readonly record struct DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec); +internal readonly record struct DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec, string AdditionalSpec); internal enum DeclaredReferenceKind { Reference, ProjectReference, PackageReference } \ No newline at end of file diff --git a/src/Tasks/CollectDeclaredReferencesTask.cs b/src/Tasks/CollectDeclaredReferencesTask.cs index 00e3c6a..02bdd36 100644 --- a/src/Tasks/CollectDeclaredReferencesTask.cs +++ b/src/Tasks/CollectDeclaredReferencesTask.cs @@ -129,7 +129,7 @@ public override bool Execute() if (referencePath is not null) { - declaredReferences.Add(new DeclaredReference(referencePath, DeclaredReferenceKind.Reference, referenceSpec)); + declaredReferences.Add(new DeclaredReference(referencePath, DeclaredReferenceKind.Reference, referenceSpec, string.Empty)); } } } @@ -162,7 +162,7 @@ public override bool Execute() string projectReferenceAssemblyPath = Path.GetFullPath(projectReference.ItemSpec); string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec"); - declaredReferences.Add(new DeclaredReference(projectReferenceAssemblyPath, DeclaredReferenceKind.ProjectReference, referenceProjectFile)); + declaredReferences.Add(new DeclaredReference(projectReferenceAssemblyPath, DeclaredReferenceKind.ProjectReference, referenceProjectFile, string.Empty)); } } else @@ -199,9 +199,9 @@ public override bool Execute() continue; } - foreach (string assemblyPath in packageInfo.CompileTimeAssemblies) + foreach (var assemblyPath in packageInfo.CompileTimeAssemblies) { - declaredReferences.Add(new DeclaredReference(assemblyPath, DeclaredReferenceKind.PackageReference, packageReference.ItemSpec)); + declaredReferences.Add(new DeclaredReference(assemblyPath.Item2, DeclaredReferenceKind.PackageReference, packageReference.ItemSpec, assemblyPath.Item1)); } } } @@ -347,7 +347,7 @@ private Dictionary GetPackageInfos() packageInfoBuilders.Add(packageId, packageInfoBuilder); } - packageInfoBuilder.AddCompileTimeAssemblies(nugetLibraryAssemblies); + packageInfoBuilder.AddCompileTimeAssemblies(nugetLibrary.Name, nugetLibraryAssemblies); packageInfoBuilder.AddBuildFiles(buildFiles); // Recurse though dependents @@ -476,7 +476,7 @@ private static bool IsSuppressed(ITaskItem item, string warningId) return false; } - // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line. + // File format: tab-separated fields (AssemblyPath, Kind, Spec, AdditionalSpec), one reference per line. // Keep in sync with ReadDeclaredReferences in ReferenceTrimmerAnalyzer.cs. private static void SaveDeclaredReferences(IReadOnlyList declaredReferences, string filePath) { @@ -497,6 +497,8 @@ private static void SaveDeclaredReferences(IReadOnlyList decl writer.Append(kindString); writer.Append(fieldDelimiter); writer.Append(reference.Spec); + writer.Append(fieldDelimiter); + writer.Append(reference.AdditionalSpec); writer.AppendLine(); } @@ -515,10 +517,10 @@ private static void SaveDeclaredReferences(IReadOnlyList decl private sealed class PackageInfoBuilder { - private List? _compileTimeAssemblies; + private List>? _compileTimeAssemblies; private List? _buildFiles; - public void AddCompileTimeAssemblies(List compileTimeAssemblies) + public void AddCompileTimeAssemblies(string packageName, List compileTimeAssemblies) { if (compileTimeAssemblies.Count == 0) { @@ -526,7 +528,7 @@ public void AddCompileTimeAssemblies(List compileTimeAssemblies) } _compileTimeAssemblies ??= new(compileTimeAssemblies.Count); - _compileTimeAssemblies.AddRange(compileTimeAssemblies); + _compileTimeAssemblies.AddRange(compileTimeAssemblies.Select(assemblyPath => Tuple.Create(packageName, assemblyPath))); } public void AddBuildFiles(List buildFiles) @@ -542,11 +544,11 @@ public void AddBuildFiles(List buildFiles) public PackageInfo ToPackageInfo() => new( - (IReadOnlyCollection?)_compileTimeAssemblies ?? Array.Empty(), - (IReadOnlyCollection?)_buildFiles ?? Array.Empty()); + (IReadOnlyCollection>?)_compileTimeAssemblies ?? [], + (IReadOnlyCollection?)_buildFiles ?? []); } private readonly record struct PackageInfo( - IReadOnlyCollection CompileTimeAssemblies, + IReadOnlyCollection> CompileTimeAssemblies, IReadOnlyCollection BuildFiles); } diff --git a/src/Tests/E2ETests.cs b/src/Tests/E2ETests.cs index 38e52da..a8172ed 100644 --- a/src/Tests/E2ETests.cs +++ b/src/Tests/E2ETests.cs @@ -307,6 +307,25 @@ public Task UnusedPackageReference() }); } + [TestMethod] + public Task UnusedPackageReferenceWithSdk() + { + return RunMSBuildAsync( + projectFile: "Test/Test.csproj", + expectedWarnings: + [ + new Warning("RT0003: PackageReference Moq can be removed (though some of its transitive dependent packages may be used)", "Test/Test.csproj") + ]); + } + + [TestMethod] + public Task UnusedPackageReferenceWithMetaPackage() + { + return RunMSBuildAsync( + projectFile: "Test/Test.csproj", + expectedWarnings: []); + } + [TestMethod] public Task UnusedPackageReferenceNoWarn() { diff --git a/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.cs b/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.cs new file mode 100644 index 0000000..f22919d --- /dev/null +++ b/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Frozen; + +namespace Test +{ + public class Foo + { + public static FrozenSet SomeSet() => FrozenSet.ToFrozenSet(Array.Empty()); + } +} diff --git a/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.csproj b/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.csproj new file mode 100644 index 0000000..deb0030 --- /dev/null +++ b/src/Tests/TestData/UnusedPackageReferenceWithMetaPackage/Test/Test.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + + + + + + + diff --git a/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.cs b/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.cs new file mode 100644 index 0000000..af2e3f7 --- /dev/null +++ b/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.cs @@ -0,0 +1,9 @@ +using Castle.Core.Logging; + +namespace Test +{ + public class Foo + { + public static ILogger Logger() => NullLogger.Instance; + } +} diff --git a/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.csproj b/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.csproj new file mode 100644 index 0000000..0cd2624 --- /dev/null +++ b/src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + + + + + + +