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.
///