Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions src/Analyzer/ReferenceTrimmerAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Copy link
Owner

@dfederm dfederm Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the whole point of the change, to have a slightly different error message?

Its also unclear on how this relates to #119

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a proposed implementation to reuse RT0003. I'm open for creating RT0004 for this case

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem isn't the diagnostic number, the problem is that I dont understand what this change does beyond add this suffix to the message.

Can you add a PR description to explain things?

"ReferenceTrimmer",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
Expand Down Expand Up @@ -140,6 +140,7 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context)
}

Dictionary<string, List<string>> packageAssembliesDict = new(StringComparer.OrdinalIgnoreCase);
Dictionary<string, List<string>> topLevelPackageAssembliesDict = new(StringComparer.OrdinalIgnoreCase);
foreach (DeclaredReference declaredReference in ReadDeclaredReferences(sourceText))
{
switch (declaredReference.Kind)
Expand All @@ -166,11 +167,23 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context)
{
if (!packageAssembliesDict.TryGetValue(declaredReference.Spec, out List<string> packageAssemblies))
{
packageAssemblies = new List<string>();
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<string> topLevelPackageAssemblies))
{
topLevelPackageAssemblies = [];
topLevelPackageAssembliesDict.Add(declaredReference.Spec, topLevelPackageAssemblies);
}

topLevelPackageAssemblies.Add(declaredReference.AssemblyPath);
}
break;
}
}
Expand All @@ -183,7 +196,11 @@ private static void DumpUsedReferencesCore(CompilationAnalysisContext context)
List<string> 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)"));
}
}
}
Expand Down Expand Up @@ -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<DeclaredReference> ReadDeclaredReferences(SourceText sourceText)
{
Expand All @@ -250,6 +267,7 @@ private static IEnumerable<DeclaredReference> ReadDeclaredReferences(SourceText

int firstTab = -1;
int secondTab = -1;
int thirdTab = -1;
for (int i = start; i < end; i++)
{
if (sourceText[i] == '\t')
Expand All @@ -258,21 +276,26 @@ private static IEnumerable<DeclaredReference> 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).
Expand All @@ -295,7 +318,7 @@ private static IEnumerable<DeclaredReference> ReadDeclaredReferences(SourceText
continue;
}

yield return new DeclaredReference(assemblyPath, kind, spec);
yield return new DeclaredReference(assemblyPath, kind, spec, additionalSpec);
}
}
}
2 changes: 1 addition & 1 deletion src/Shared/DeclaredReferences.cs
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's "additional spec"? There's only one "spec" (Include value)


internal enum DeclaredReferenceKind { Reference, ProjectReference, PackageReference }
26 changes: 14 additions & 12 deletions src/Tasks/CollectDeclaredReferencesTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}
}
Expand Down Expand Up @@ -347,7 +347,7 @@ private Dictionary<string, PackageInfo> GetPackageInfos()
packageInfoBuilders.Add(packageId, packageInfoBuilder);
}

packageInfoBuilder.AddCompileTimeAssemblies(nugetLibraryAssemblies);
packageInfoBuilder.AddCompileTimeAssemblies(nugetLibrary.Name, nugetLibraryAssemblies);
packageInfoBuilder.AddBuildFiles(buildFiles);

// Recurse though dependents
Expand Down Expand Up @@ -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<DeclaredReference> declaredReferences, string filePath)
{
Expand All @@ -497,6 +497,8 @@ private static void SaveDeclaredReferences(IReadOnlyList<DeclaredReference> decl
writer.Append(kindString);
writer.Append(fieldDelimiter);
writer.Append(reference.Spec);
writer.Append(fieldDelimiter);
writer.Append(reference.AdditionalSpec);
writer.AppendLine();
}

Expand All @@ -515,18 +517,18 @@ private static void SaveDeclaredReferences(IReadOnlyList<DeclaredReference> decl

private sealed class PackageInfoBuilder
{
private List<string>? _compileTimeAssemblies;
private List<Tuple<string, string>>? _compileTimeAssemblies;
private List<string>? _buildFiles;

public void AddCompileTimeAssemblies(List<string> compileTimeAssemblies)
public void AddCompileTimeAssemblies(string packageName, List<string> compileTimeAssemblies)
{
if (compileTimeAssemblies.Count == 0)
{
return;
}

_compileTimeAssemblies ??= new(compileTimeAssemblies.Count);
_compileTimeAssemblies.AddRange(compileTimeAssemblies);
_compileTimeAssemblies.AddRange(compileTimeAssemblies.Select(assemblyPath => Tuple.Create(packageName, assemblyPath)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid the allocation. A value tuple would be best.

}

public void AddBuildFiles(List<string> buildFiles)
Expand All @@ -542,11 +544,11 @@ public void AddBuildFiles(List<string> buildFiles)

public PackageInfo ToPackageInfo()
=> new(
(IReadOnlyCollection<string>?)_compileTimeAssemblies ?? Array.Empty<string>(),
(IReadOnlyCollection<string>?)_buildFiles ?? Array.Empty<string>());
(IReadOnlyCollection<Tuple<string, string>>?)_compileTimeAssemblies ?? [],
(IReadOnlyCollection<string>?)_buildFiles ?? []);
}

private readonly record struct PackageInfo(
IReadOnlyCollection<string> CompileTimeAssemblies,
IReadOnlyCollection<Tuple<string, string>> CompileTimeAssemblies,
IReadOnlyCollection<string> BuildFiles);
}
19 changes: 19 additions & 0 deletions src/Tests/E2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Frozen;

namespace Test
{
public class Foo
{
public static FrozenSet<int> SomeSet() => FrozenSet.ToFrozenSet(Array.Empty<int>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="MSTest.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ExplicitUsings>enable</ExplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" Version="4.1.0" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Castle.Core.Logging;

namespace Test
{
public class Foo
{
public static ILogger Logger() => NullLogger.Instance;
}
}
12 changes: 12 additions & 0 deletions src/Tests/TestData/UnusedPackageReferenceWithSdk/Test/Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="MSTest.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ExplicitUsings>enable</ExplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>

</Project>