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

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

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>