diff --git a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs index 7748a189690..1e6b56edd0e 100644 --- a/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs @@ -596,6 +596,31 @@ public void NullMetadataOnLegacyOutputItems() logger.AssertLogContains("[foo: ]"); } + /// + /// If an item returned from a task has bare-minimum metadata implementation, we shouldn't crash. + /// + [Fact] + public void MinimalLegacyOutputItems() + { + string customTaskPath = Assembly.GetExecutingAssembly().Location; + + string projectContents = $""" + + + + + + + + + + + + """; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents, _testOutput, LoggerVerbosity.Diagnostic); + } + /// /// Regression test for https://github.com/dotnet/msbuild/issues/5080 /// diff --git a/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs b/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs new file mode 100644 index 00000000000..7f8eec32b2a --- /dev/null +++ b/src/Build.UnitTests/TaskThatReturnsMinimalItem.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; + +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Engine.UnitTests; + +/// +/// Task that emulates .NET 3.5 tasks. +/// +public sealed class TaskThatReturnsMinimalItem : ITask +{ + public IBuildEngine? BuildEngine { get; set; } + public ITaskHost? HostObject { get; set; } + + [Output] + public ITaskItem MinimalTaskItemOutput { get => new MinimalTaskItem(); } + + public bool Execute() => true; + + /// + /// Minimal implementation of that uses a for metadata, + /// like MSBuild 3 did. + /// + internal sealed class MinimalTaskItem : ITaskItem + { + public string ItemSpec { get => $"{nameof(MinimalTaskItem)}spec"; set => throw new NotImplementedException(); } + + public ICollection MetadataNames => throw new NotImplementedException(); + + public int MetadataCount => throw new NotImplementedException(); + + public IDictionary CloneCustomMetadata() + { + Hashtable t = new(); + t["key"] = "value"; + + return t; + } + public void CopyMetadataTo(ITaskItem destinationItem) => throw new NotImplementedException(); + public string GetMetadata(string metadataName) => "value"; + public void RemoveMetadata(string metadataName) => throw new NotImplementedException(); + public void SetMetadata(string metadataName, string metadataValue) => throw new NotImplementedException(); + } +} diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 69da00e3955..705ca12979e 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -1396,8 +1396,8 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN newItem = new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output.ItemSpec), parameterLocationEscaped); newItem.SetMetadataOnTaskOutput(output.CloneCustomMetadata() - .Cast>() - .Select(x => new KeyValuePair(x.Key, EscapingUtilities.Escape(x.Value)))); + .Cast() + .Select(x => new KeyValuePair((string)x.Key, EscapingUtilities.Escape((string)x.Value)))); } } diff --git a/src/Framework/ITaskItemExtensions.cs b/src/Framework/ITaskItemExtensions.cs index 7dc7dbdaf86..6ba56e1a880 100644 --- a/src/Framework/ITaskItemExtensions.cs +++ b/src/Framework/ITaskItemExtensions.cs @@ -35,7 +35,9 @@ public static IEnumerable> EnumerateMetadata(this I return enumerableMetadata; } - // In theory this should never be reachable. + // Fallback for + // * ITaskItem implementations from MSBuild 3.5 from the GAC + // * Custom ITaskItems that don't use Dictionary var list = new KeyValuePair[customMetadata.Count]; int i = 0;