diff --git a/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs b/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs index 4af69e9204e..1164b09d5dc 100644 --- a/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs +++ b/src/Build.OM.UnitTests/Instance/ProjectItemInstance_Tests.cs @@ -79,6 +79,37 @@ public void AccessorsWithoutMetadata() Assert.False(item.Metadata.GetEnumerator().MoveNext()); } + [Fact] + public void LocationTracksOriginatingItemElement() + { + using ProjectRootElementFromString projectRootElementFromString = new( + """ + + + + + + """); + + ProjectRootElement xml = projectRootElementFromString.Project; + ProjectItemElement itemElement = xml.Items.Single(); + ProjectItemInstance item = new ProjectInstance(xml).GetItems("i").Single(); + + TaskItemLocation? itemLocation = item.Location; + Assert.True(itemLocation.HasValue); + TaskItemLocation location = itemLocation.Value; + Assert.Equal(itemElement.Location.File, location.File); + Assert.Equal(itemElement.Location.Line, location.Line); + Assert.Equal(itemElement.Location.Column, location.Column); + Assert.Equal(location, ((ITaskItem3)item).Location.Value); + + var copiedItem = new Utilities.TaskItem(item); + TaskItemLocation copiedLocation = copiedItem.Location.Value; + Assert.Equal(location.File, copiedLocation.File); + Assert.Equal(location.Line, copiedLocation.Line); + Assert.Equal(location.Column, copiedLocation.Column); + } + /// /// Basic ProjectItemInstance with metadata /// diff --git a/src/Build.UnitTests/Instance/TaskItem_Tests.cs b/src/Build.UnitTests/Instance/TaskItem_Tests.cs index 7e6fd0f64f4..7eb6443e549 100644 --- a/src/Build.UnitTests/Instance/TaskItem_Tests.cs +++ b/src/Build.UnitTests/Instance/TaskItem_Tests.cs @@ -50,7 +50,7 @@ public class TaskItem_Tests [Fact] public void Serialization() { - TaskItem item = new TaskItem("foo", "bar.proj"); + TaskItem item = new TaskItem("foo", "bar.proj", sourceLineNumber: 12, sourceColumnNumber: 34); item.SetMetadata("a", "b"); TranslationHelpers.GetWriteTranslator().Translate(ref item, TaskItem.FactoryForDeserialization); @@ -61,6 +61,7 @@ public void Serialization() Assert.Equal(item.MetadataCount, deserializedItem.MetadataCount); Assert.Equal(item.GetMetadata("a"), deserializedItem.GetMetadata("a")); Assert.Equal(item.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath), deserializedItem.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath)); + Assert.Equal(item.Location, deserializedItem.Location); } /// diff --git a/src/Build.UnitTests/InstanceFromRemote/FakeProjectItemElementLink.cs b/src/Build.UnitTests/InstanceFromRemote/FakeProjectItemElementLink.cs index a0d6b7ccbb6..21696626dc7 100644 --- a/src/Build.UnitTests/InstanceFromRemote/FakeProjectItemElementLink.cs +++ b/src/Build.UnitTests/InstanceFromRemote/FakeProjectItemElementLink.cs @@ -18,11 +18,13 @@ namespace Microsoft.Build.Engine.UnitTests.InstanceFromRemote internal sealed class FakeProjectItemElementLink : ProjectItemElementLink { private readonly string _filePath; + private readonly ElementLocation _location; public FakeProjectItemElementLink(string elementName, string filePath) { ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + _location = ElementLocation.Create(filePath, 1, 1); } public override int Count => throw new NotImplementedException(); @@ -45,7 +47,7 @@ public FakeProjectItemElementLink(string elementName, string filePath) public override ProjectElement NextSibling => throw new NotImplementedException(); - public override ElementLocation Location => throw new NotImplementedException(); + public override ElementLocation Location => _location; public override IReadOnlyCollection Attributes => throw new NotImplementedException(); diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs index ca316249300..a996a51faa5 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -426,7 +426,10 @@ private List ExpandItemIntoItems( // Split Include on any semicolons, and take each split in turn var includeSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedInclude); - ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(Project, originalItem.ItemType); + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(Project, originalItem.ItemType) + { + SourceLocation = originalItem.Location, + }; // EngineFileUtilities.GetFileListEscaped api invocation evaluates excludes by default. // If the code process any expression like "@(x)", we need to handle excludes explicitly using EvaluateExcludePaths(). @@ -473,7 +476,9 @@ private List ExpandItemIntoItems( null, null, originalItem.Location.File, - useItemDefinitionsWithoutModification: false)); + useItemDefinitionsWithoutModification: false, + originalItem.Location.Line, + originalItem.Location.Column)); } } } diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 5222a68a822..c4bfe704e42 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -1223,7 +1223,10 @@ private void InferTaskOutputs( // This is an output item. // Expand only with properties first, so that expressions like Include="@(foo)" will transfer the metadata of the "foo" items as well, not just their item specs. var outputItemSpecs = bucket.Expander.ExpandIntoStringListLeaveEscaped(taskParameterAttribute, ExpanderOptions.ExpandPropertiesAndMetadata, taskItemInstance.TaskParameterLocation); - ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(_buildRequestEntry.RequestConfiguration.Project, itemName); + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(_buildRequestEntry.RequestConfiguration.Project, itemName) + { + SourceLocation = taskItemInstance.Location, + }; foreach (string outputItemSpec in outputItemSpecs) { diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 46da48fa86f..a19e47c670f 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -1801,7 +1801,7 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN { // The common case -- all items involved are Microsoft.Build.Execution.ProjectItemInstance.TaskItems. // Furthermore, because that is true, we know by definition that they also implement ITaskItem2. - newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsProjectItem.IncludeEscaped, parameterLocationEscaped); + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsProjectItem.IncludeEscaped, parameterLocationEscaped, parameterLocation.Line, parameterLocation.Column); newItem.SetMetadata(outputAsProjectItem.MetadataCollection); // copy-on-write! } @@ -1810,7 +1810,7 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN if (output is ITaskItem2 outputAsITaskItem2) { // Probably a Microsoft.Build.Utilities.TaskItem. Not quite as good, but we can still preserve escaping. - newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsITaskItem2.EvaluatedIncludeEscaped, parameterLocationEscaped); + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsITaskItem2.EvaluatedIncludeEscaped, parameterLocationEscaped, parameterLocation.Line, parameterLocation.Column); // If found, directly pass the backing copy-on-write dictionary. // Otherwise, retrieve a cloned dictionary from the task item. @@ -1830,7 +1830,7 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN { // Not a ProjectItemInstance.TaskItem or even a ITaskItem2, so we have to fake it. // Setting an item spec expects the escaped value, as does setting metadata. - newItem = new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output.ItemSpec), parameterLocationEscaped); + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output.ItemSpec), parameterLocationEscaped, parameterLocation.Line, parameterLocation.Column); newItem.SetMetadataOnTaskOutput(EnumerateMetadata(output.CloneCustomMetadata())); @@ -1948,7 +1948,7 @@ private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string ou // attempting to put an empty string into an item is a no-op. if (output?.Length > 0) { - _batchBucket.Lookup.AddNewItem(new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output), EscapingUtilities.Escape(parameterLocation.File))); + _batchBucket.Lookup.AddNewItem(new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output), EscapingUtilities.Escape(parameterLocation.File), parameterLocation.Line, parameterLocation.Column)); } } diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index cf480f22cd6..72d63984e0c 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -3537,7 +3537,9 @@ private ProjectItemInstance InstantiateProjectItemInstance(ProjectItem item) directMetadata, inheritedItemDefinitions, item.Xml.ContainingProject.EscapedFullPath, - useItemDefinitionsWithoutModification: false); + useItemDefinitionsWithoutModification: false, + item.Xml.Location.Line, + item.Xml.Location.Column); return instance; } @@ -3597,7 +3599,9 @@ private static ProjectItemInstance InstantiateProjectItemInstanceFromImmutablePr directMetadata, inheritedItemDefinitions, item.Xml.ContainingProject.EscapedFullPath, - useItemDefinitionsWithoutModification: true); + useItemDefinitionsWithoutModification: true, + item.Xml.Location.Line, + item.Xml.Location.Column); return instance; } diff --git a/src/Build/Instance/ProjectItemInstance.cs b/src/Build/Instance/ProjectItemInstance.cs index fcb166f65f4..984c9b91c8f 100644 --- a/src/Build/Instance/ProjectItemInstance.cs +++ b/src/Build/Instance/ProjectItemInstance.cs @@ -29,13 +29,12 @@ namespace Microsoft.Build.Execution /// Wraps an evaluated item for build purposes /// /// - /// Does not store XML location information. That is not needed by the build process as all correctness checks - /// and evaluation has already been performed, so it is unnecessary bulk. + /// Stores the source line and column so tasks can associate diagnostics with the XML element that produced the item. /// [DebuggerDisplay("{ItemType}={EvaluatedInclude} #DirectMetadata={DirectMetadataCount})")] public class ProjectItemInstance : IItem, - ITaskItem2, + ITaskItem3, IMetadataTable, ITranslatable, IMetadataContainer, @@ -67,8 +66,8 @@ public class ProjectItemInstance : /// and during the build when tasks emit items. /// Mutability follows the project. /// - internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string definingFileEscaped) - : this(project, itemType, includeEscaped, includeEscaped, definingFileEscaped) + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string definingFileEscaped, int sourceLineNumber = 0, int sourceColumnNumber = 0) + : this(project, itemType, includeEscaped, includeEscaped, definingFileEscaped, sourceLineNumber, sourceColumnNumber) { } @@ -79,8 +78,8 @@ internal ProjectItemInstance(ProjectInstance project, string itemType, string in /// and during the build when tasks emit items. /// Mutability follows the project. /// - internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string includeBeforeWildcardExpansionEscaped, string definingFileEscaped) - : this(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, null /* no direct metadata */, null /* need to add item definition metadata */, definingFileEscaped, useItemDefinitionsWithoutModification: false) + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string includeBeforeWildcardExpansionEscaped, string definingFileEscaped, int sourceLineNumber = 0, int sourceColumnNumber = 0) + : this(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, null /* no direct metadata */, null /* need to add item definition metadata */, definingFileEscaped, useItemDefinitionsWithoutModification: false, sourceLineNumber, sourceColumnNumber) { } @@ -105,9 +104,11 @@ internal ProjectItemInstance( IReadOnlyDictionary directMetadata, IList itemDefinitions, string definingFileEscaped, - bool useItemDefinitionsWithoutModification) + bool useItemDefinitionsWithoutModification, + int sourceLineNumber = 0, + int sourceColumnNumber = 0) { - CommonConstructor(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, directMetadata, itemDefinitions, definingFileEscaped, useItemDefinitionsWithoutModification); + CommonConstructor(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, directMetadata, itemDefinitions, definingFileEscaped, useItemDefinitionsWithoutModification, sourceLineNumber, sourceColumnNumber); } /// @@ -121,7 +122,7 @@ internal ProjectItemInstance( /// /// Not public since the only creation scenario is setting on a project. /// - internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, IEnumerable> directMetadata, string definingFileEscaped) + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, IEnumerable> directMetadata, string definingFileEscaped, int sourceLineNumber = 0, int sourceColumnNumber = 0) { ImmutableDictionary metadata = null; @@ -131,7 +132,7 @@ internal ProjectItemInstance(ProjectInstance project, string itemType, string in .SetItems(directMetadata, ProjectMetadataInstance.VerifyThrowReservedName); } - CommonConstructor(project, itemType, includeEscaped, includeEscaped, metadata, null /* need to add item definition metadata */, definingFileEscaped, useItemDefinitionsWithoutModification: false); + CommonConstructor(project, itemType, includeEscaped, includeEscaped, metadata, null /* need to add item definition metadata */, definingFileEscaped, useItemDefinitionsWithoutModification: false, sourceLineNumber, sourceColumnNumber); } /// @@ -171,6 +172,11 @@ public ProjectInstance Project get { return _project; } } + /// + /// Gets the source location of the XML element that produced this item. + /// + public TaskItemLocation? Location => _taskItem.Location; + /// /// Item type, for example "Compile" /// @@ -732,7 +738,9 @@ private void CommonConstructor( IReadOnlyDictionary directMetadata, IList itemDefinitions, string definingFileEscaped, - bool useItemDefinitionsWithoutModification) + bool useItemDefinitionsWithoutModification, + int sourceLineNumber, + int sourceColumnNumber) { ArgumentNullException.ThrowIfNull(projectToUse, "project"); ArgumentException.ThrowIfNullOrEmpty(itemTypeToUse, "itemType"); @@ -771,7 +779,9 @@ private void CommonConstructor( inheritedItemDefinitions, _project.Directory, _project.IsImmutable, - definingFileEscaped); + definingFileEscaped, + sourceLineNumber, + sourceColumnNumber); } /// @@ -782,7 +792,7 @@ internal sealed class TaskItem : #if FEATURE_APPDOMAIN MarshalByRefObject, #endif - ITaskItem2, + ITaskItem3, IItem, ITranslatable, IEquatable, @@ -793,6 +803,10 @@ internal sealed class TaskItem : /// private string _definingFileEscaped; + private int _sourceLineNumber; + + private int _sourceColumnNumber; + /// /// Evaluated include, escaped as necessary. /// @@ -845,8 +859,8 @@ internal sealed class TaskItem : /// /// Creates an instance of this class given the item-spec. /// - internal TaskItem(string includeEscaped, string definingFileEscaped) - : this(includeEscaped, includeEscaped, null, null, null, immutable: false, definingFileEscaped) + internal TaskItem(string includeEscaped, string definingFileEscaped, int sourceLineNumber = 0, int sourceColumnNumber = 0) + : this(includeEscaped, includeEscaped, null, null, null, immutable: false, definingFileEscaped, sourceLineNumber, sourceColumnNumber) { } @@ -861,7 +875,9 @@ internal TaskItem( IList itemDefinitions, string projectDirectory, bool immutable, - string definingFileEscaped) // the actual project file (or import) that defines this item. + string definingFileEscaped, + int sourceLineNumber = 0, + int sourceColumnNumber = 0) // the actual project file (or import) that defines this item. { ArgumentException.ThrowIfNullOrEmpty(includeEscaped); ArgumentException.ThrowIfNullOrEmpty(includeBeforeWildcardExpansionEscaped); @@ -873,6 +889,8 @@ internal TaskItem( _projectDirectory = projectDirectory; _isImmutable = immutable; _definingFileEscaped = definingFileEscaped; + _sourceLineNumber = sourceLineNumber; + _sourceColumnNumber = sourceColumnNumber; } /// @@ -895,6 +913,8 @@ private TaskItem(TaskItem source, bool addOriginalItemSpec) source.CopyMetadataTo(this, addOriginalItemSpec); _cachedModifiers = source._cachedModifiers; _definingFileEscaped = source._definingFileEscaped; + _sourceLineNumber = source._sourceLineNumber; + _sourceColumnNumber = source._sourceColumnNumber; } /// @@ -913,6 +933,11 @@ private TaskItem(ITranslator translator, LookasideStringInterner interner) this.TranslateWithInterning(translator, interner); } + /// + public TaskItemLocation? Location => _sourceLineNumber == 0 + ? null + : new TaskItemLocation(EscapingUtilities.UnescapeAll(_definingFileEscaped), _sourceLineNumber, _sourceColumnNumber); + /// /// Gets or sets the unescaped include, or "name", for the item. /// @@ -1672,6 +1697,8 @@ void ITranslatable.Translate(ITranslator translator) translator.Translate(ref _includeBeforeWildcardExpansionEscaped); translator.Translate(ref _isImmutable); translator.Translate(ref _definingFileEscaped); + translator.Translate(ref _sourceLineNumber); + translator.Translate(ref _sourceColumnNumber); TranslatorHelpers.Translate( translator, @@ -1890,6 +1917,8 @@ internal void TranslateWithInterning(ITranslator translator, LookasideStringInte (capacity) => new List(capacity)); translator.Translate(ref _isImmutable); translator.Translate(ref _includeEscaped); + translator.Translate(ref _sourceLineNumber); + translator.Translate(ref _sourceColumnNumber); if (translator.Mode == TranslationDirection.WriteToStream) { @@ -2210,6 +2239,10 @@ internal class ProjectItemInstanceFactory : IItemFactory private ProjectInstance _project; + private int _sourceLineNumber; + + private int _sourceColumnNumber; + /// /// Constructor not taking an item type. /// This indicates that the user of this factory should set the item type @@ -2245,7 +2278,20 @@ public string ItemType /// public ProjectItemElement ItemElement { - set { ItemType = value.ItemType; } + set + { + ItemType = value.ItemType; + SourceLocation = value.Location; + } + } + + internal ElementLocation SourceLocation + { + set + { + _sourceLineNumber = value?.Line ?? 0; + _sourceColumnNumber = value?.Column ?? 0; + } } /// @@ -2258,7 +2304,7 @@ public ProjectItemInstance CreateItem(string include, string definingProject) { Assumed.NotNullOrEmpty(ItemType); - ProjectItemInstance item = new ProjectItemInstance(_project, ItemType, include, definingProject); + ProjectItemInstance item = new ProjectItemInstance(_project, ItemType, include, definingProject, _sourceLineNumber, _sourceColumnNumber); return item; } @@ -2291,7 +2337,7 @@ public ProjectItemInstance CreateItem(string evaluatedInclude, string evaluatedI { Assumed.NotNullOrEmpty(ItemType); - return new ProjectItemInstance(_project, ItemType, evaluatedInclude, evaluatedIncludeBeforeWildcardExpansion, definingProject); + return new ProjectItemInstance(_project, ItemType, evaluatedInclude, evaluatedIncludeBeforeWildcardExpansion, definingProject, _sourceLineNumber, _sourceColumnNumber); } /// @@ -2344,7 +2390,7 @@ private ProjectItemInstance CreateItem(string includeEscaped, string includeBefo itemDefinitionsClone.Add(sourceItemDefinition); } - return new ProjectItemInstance(_project, ItemType, includeEscaped, includeBeforeWildcardExpansionEscaped, source._taskItem.DirectMetadata, itemDefinitionsClone, definingProject, useItemDefinitionsWithoutModification: false); + return new ProjectItemInstance(_project, ItemType, includeEscaped, includeBeforeWildcardExpansionEscaped, source._taskItem.DirectMetadata, itemDefinitionsClone, definingProject, useItemDefinitionsWithoutModification: false, _sourceLineNumber, _sourceColumnNumber); } } diff --git a/src/Framework/ITaskItem3.cs b/src/Framework/ITaskItem3.cs new file mode 100644 index 00000000000..9f4e1f10f7e --- /dev/null +++ b/src/Framework/ITaskItem3.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework; + +/// +/// Extends with the source location of the XML element that produced the item. +/// +[ComVisible(false)] +public interface ITaskItem3 : ITaskItem2 +{ + /// + /// Gets the source location of the XML element that produced this item, or when unavailable. + /// + TaskItemLocation? Location { get; } +} diff --git a/src/Framework/TaskItemLocation.cs b/src/Framework/TaskItemLocation.cs new file mode 100644 index 00000000000..303ef340d66 --- /dev/null +++ b/src/Framework/TaskItemLocation.cs @@ -0,0 +1,53 @@ +// 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 Microsoft.Build.Shared; + +namespace Microsoft.Build.Framework; + +/// +/// Identifies the source location of an MSBuild item. +/// +[Serializable] +public readonly struct TaskItemLocation : IMSBuildElementLocation +{ + private readonly string? _file; + + /// + /// Initializes a new item source location. + /// + public TaskItemLocation(string? file, int line, int column) + { + if (line < 0) + { + throw new ArgumentOutOfRangeException(nameof(line)); + } + + if (column < 0) + { + throw new ArgumentOutOfRangeException(nameof(column)); + } + + _file = file; + Line = line; + Column = column; + } + + /// + public string File => _file ?? string.Empty; + + /// + public int Line { get; } + + /// + public int Column { get; } + + /// + public string LocationString => Line switch + { + > 0 when Column > 0 => $"{File} ({Line},{Column})", + > 0 => $"{File} ({Line})", + _ => File, + }; +} diff --git a/src/Framework/TaskItem_T.cs b/src/Framework/TaskItem_T.cs index b54706b1d21..54ab8057f75 100644 --- a/src/Framework/TaskItem_T.cs +++ b/src/Framework/TaskItem_T.cs @@ -18,7 +18,7 @@ namespace Microsoft.Build.Framework /// The identity (ItemSpec) is parsed using which handles value types, /// AbsolutePath, FileInfo, and DirectoryInfo. /// - public readonly struct TaskItem : ITaskItem, IEquatable> + public readonly struct TaskItem : ITaskItem, ITaskItem3, IEquatable> { private readonly ITaskItem? _backingItem; @@ -27,6 +27,9 @@ namespace Microsoft.Build.Framework /// public T Value { get; } + /// + public TaskItemLocation? Location => (_backingItem as ITaskItem3)?.Location; + /// /// Initializes a new instance of from a strongly-typed value. /// diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs index fd274ad31f7..e2f8e6bb2e3 100644 --- a/src/Utilities/TaskItem.cs +++ b/src/Utilities/TaskItem.cs @@ -38,7 +38,7 @@ public sealed class TaskItem : #if FEATURE_APPDOMAIN MarshalByRefObject, #endif - ITaskItem2, + ITaskItem3, IMetadataContainer // expose direct underlying metadata for fast access in binary logger { #region Member Data @@ -61,6 +61,10 @@ public sealed class TaskItem : /// private readonly string _definingProject; + private readonly int _sourceLineNumber; + + private readonly int _sourceColumnNumber; + #endregion #region Constructors @@ -162,6 +166,12 @@ public TaskItem( _definingProject = sourceItemAsITaskItem2.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath); } + if (sourceItem is ITaskItem3 taskItemWithLocation && taskItemWithLocation.Location is TaskItemLocation location) + { + _sourceLineNumber = location.Line; + _sourceColumnNumber = location.Column; + } + sourceItem.CopyMetadataTo(this); } @@ -244,6 +254,11 @@ public string ItemSpec } } + /// + public TaskItemLocation? Location => _sourceLineNumber == 0 + ? null + : new TaskItemLocation(EscapingUtilities.UnescapeAll(_definingProject), _sourceLineNumber, _sourceColumnNumber); + /// /// Gets or sets the escaped include, or "name", for the item. ///