diff --git a/src/Tasks/CollectDeclaredReferencesTask.cs b/src/Tasks/CollectDeclaredReferencesTask.cs index 63ed8f4..bcf61c9 100644 --- a/src/Tasks/CollectDeclaredReferencesTask.cs +++ b/src/Tasks/CollectDeclaredReferencesTask.cs @@ -109,8 +109,8 @@ public override bool Execute() } else { - var resolvedReference = ResolvedReferences.SingleOrDefault(rr => string.Equals(rr.GetMetadata("OriginalItemSpec"), referenceSpec, StringComparison.OrdinalIgnoreCase)); - referencePath = resolvedReference is null ? null : resolvedReference.ItemSpec; + var resolvedReference = ResolvedReferences?.SingleOrDefault(rr => string.Equals(rr.GetMetadata("OriginalItemSpec"), referenceSpec, StringComparison.OrdinalIgnoreCase)); + referencePath = resolvedReference?.ItemSpec; } // If the reference is under the nuget package root, it's likely a Reference added in a package's props or targets. @@ -225,7 +225,18 @@ private Dictionary GetPackageInfos() var packageInfoBuilders = new Dictionary(StringComparer.OrdinalIgnoreCase); Log.LogMessage(MessageImportance.Low, "Loading lock file from '{0}'", ProjectAssetsFile); - var lockFile = LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance); + var lockFile = string.IsNullOrEmpty(ProjectAssetsFile) + ? null + : LockFileUtilities.GetLockFile(ProjectAssetsFile, NullLogger.Instance); + if (lockFile is null) + { + // ProjectAssetsFile may be null/missing for projects that don't use NuGet restore (e.g., legacy or non-SDK projects + // that nevertheless have items in @(PackageReference)). Without a lock file we can't resolve package contents, so + // skip package processing rather than NRE on the null lockFile below. + Log.LogMessage(MessageImportance.Low, "Lock file '{0}' is null or could not be loaded; skipping PackageReference processing", ProjectAssetsFile); + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList(); Log.LogMessage(MessageImportance.Low, "Package folders: {0}", string.Join("; ", packageFolders)); diff --git a/src/Tests/CollectDeclaredReferencesTaskTests.cs b/src/Tests/CollectDeclaredReferencesTaskTests.cs new file mode 100644 index 0000000..53e20b0 --- /dev/null +++ b/src/Tests/CollectDeclaredReferencesTaskTests.cs @@ -0,0 +1,161 @@ +using System.Collections; +using Microsoft.Build.Framework; +using ReferenceTrimmer.Tasks; + +namespace ReferenceTrimmer.Tests; + +[TestClass] +public sealed class CollectDeclaredReferencesTaskTests +{ + [TestMethod] + public void ExecuteWithAllNullCollectionInputsDoesNotThrow() + { + string outputFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".tsv"); + try + { + var engine = new MockBuildEngine(); + var task = new CollectDeclaredReferencesTask + { + BuildEngine = engine, + OutputFile = outputFile, + References = null, + ResolvedReferences = null, + ProjectReferences = null, + PackageReferences = null, + IgnorePackageBuildFiles = null, + TargetFrameworkDirectories = null, + }; + + bool result = task.Execute(); + + Assert.IsTrue(result, "Task should succeed when all collection inputs are null. Errors: " + string.Join("; ", engine.Errors)); + Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + } + finally + { + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + } + } + + [TestMethod] + public void ExecuteWithNullResolvedReferencesAndUnresolvableReferenceDoesNotThrow() + { + // Repros the vcxproj NRE: a Reference whose path can't be found locally falls into the + // ResolvedReferences.SingleOrDefault branch, but ResolvedReferences is null because + // ReferencePathWithRefAssemblies is empty/uninitialized for vcxproj projects. + string outputFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".tsv"); + try + { + var engine = new MockBuildEngine(); + var task = new CollectDeclaredReferencesTask + { + BuildEngine = engine, + OutputFile = outputFile, + References = new ITaskItem[] + { + new MockTaskItem("SomeUnresolvableReference"), + }, + ResolvedReferences = null, + }; + + bool result = task.Execute(); + + Assert.IsTrue(result, "Task should succeed even when ResolvedReferences is null. Errors: " + string.Join("; ", engine.Errors)); + Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + } + finally + { + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + } + } + + [TestMethod] + public void ExecuteWithPackageReferencesAndNullProjectAssetsFileDoesNotThrow() + { + // Guards against NRE inside GetPackageInfos when PackageReferences is non-null but + // ProjectAssetsFile is null/missing (no NuGet restore performed for this project). + string outputFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".tsv"); + try + { + var engine = new MockBuildEngine(); + var task = new CollectDeclaredReferencesTask + { + BuildEngine = engine, + OutputFile = outputFile, + PackageReferences = new ITaskItem[] + { + new MockTaskItem("SomePackage"), + }, + ProjectAssetsFile = null, + }; + + bool result = task.Execute(); + + Assert.IsTrue(result, "Task should succeed when ProjectAssetsFile is null. Errors: " + string.Join("; ", engine.Errors)); + Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + } + finally + { + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + } + } + + private sealed class MockBuildEngine : IBuildEngine + { + public List Errors { get; } = new(); + public List Warnings { get; } = new(); + public List Messages { get; } = new(); + + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => string.Empty; + + public void LogErrorEvent(BuildErrorEventArgs e) => Errors.Add(e.Message ?? string.Empty); + public void LogWarningEvent(BuildWarningEventArgs e) => Warnings.Add(e.Message ?? string.Empty); + public void LogMessageEvent(BuildMessageEventArgs e) => Messages.Add(e.Message ?? string.Empty); + public void LogCustomEvent(CustomBuildEventArgs e) { } + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => false; + } + + private sealed class MockTaskItem : ITaskItem + { + private readonly Dictionary _metadata = new(StringComparer.OrdinalIgnoreCase); + + public MockTaskItem(string itemSpec) + { + ItemSpec = itemSpec; + } + + public string ItemSpec { get; set; } + + public ICollection MetadataNames => _metadata.Keys; + + public int MetadataCount => _metadata.Count; + + public IDictionary CloneCustomMetadata() => new Dictionary(_metadata); + + public void CopyMetadataTo(ITaskItem destinationItem) + { + foreach (var kvp in _metadata) + { + destinationItem.SetMetadata(kvp.Key, kvp.Value); + } + } + + public string GetMetadata(string metadataName) => _metadata.TryGetValue(metadataName, out var value) ? value : string.Empty; + + public void RemoveMetadata(string metadataName) => _metadata.Remove(metadataName); + + public void SetMetadata(string metadataName, string metadataValue) => _metadata[metadataName] = metadataValue; + } +} diff --git a/src/Tests/ReferenceTrimmer.Tests.csproj b/src/Tests/ReferenceTrimmer.Tests.csproj index 3548cf8..68072e3 100644 --- a/src/Tests/ReferenceTrimmer.Tests.csproj +++ b/src/Tests/ReferenceTrimmer.Tests.csproj @@ -8,6 +8,7 @@ + false Build;Pack