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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/Tasks/CollectDeclaredReferencesTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -225,7 +225,18 @@ private Dictionary<string, PackageInfo> GetPackageInfos()
var packageInfoBuilders = new Dictionary<string, PackageInfoBuilder>(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<string, PackageInfo>(StringComparer.OrdinalIgnoreCase);
}

var packageFolders = lockFile.PackageFolders.Select(item => item.Path).ToList();
Log.LogMessage(MessageImportance.Low, "Package folders: {0}", string.Join("; ", packageFolders));

Expand Down
161 changes: 161 additions & 0 deletions src/Tests/CollectDeclaredReferencesTaskTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> Errors { get; } = new();
public List<string> Warnings { get; } = new();
public List<string> 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<string, string> _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<string, string>(_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;
}
}
1 change: 1 addition & 0 deletions src/Tests/ReferenceTrimmer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Analyzer\ReferenceTrimmer.Analyzer.csproj" />
<ProjectReference Include="..\Tasks\ReferenceTrimmer.Tasks.csproj" />
<ProjectReference Include="..\Package\ReferenceTrimmer.Package.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<Targets>Build;Pack</Targets>
Expand Down
Loading