Skip to content

Commit a04edfd

Browse files
authored
Ensure metadata location when created from xml element with known location (#8599)
Fixes #8579 Context MetadataElement of an ItemElement doesn't have line&col of Location set despite those are created from known xml. This way errors referencing the Location from metadata would end up pointing to location (0, 0) in the file. Reason This happens only to XmlElementWithLocation (as opposed to XmlAttributeWithLocation) during creating XmlElementWithLocation from scratch (during parsing phase that doesn't have the reader with locations already available) from an existing XmlAttributeWithLocation (that has the proper location intitialized from Load phase). Due to the need to keep the constructors contract (we override existing XmlElement), we cannot pass the additional info in the constructor. Changes Made Making the Location property of XmlElementWithLocation writable - so that it can be properly rewritten if constructed from an existing node with available location info. Testing Hand testing plus applying the changfe to the other PR: #8581, solved the issue with (0, 0) warnings location Note This or #8581 PR (whichever is merged later) should remove the workaround https://github.com/dotnet/msbuild/pull/8581/files#diff-e289ba4ce7fa0e72cf63049cce595eafcad1e7b2034ccb3305cd0f06c2f822b8R196-R197
1 parent 684efac commit a04edfd

File tree

10 files changed

+109
-20
lines changed

10 files changed

+109
-20
lines changed

src/Build.OM.UnitTests/Construction/ProjectItemElement_Tests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ public void ReadNoChildren(string project)
8282
Assert.Equal(0, Helpers.Count(item.Metadata));
8383
}
8484

85+
[Fact]
86+
public void ReadMetadataLocationPreserved()
87+
{
88+
string project = """
89+
<Project>
90+
<Target Name='t'>
91+
<ItemGroup>
92+
<i Include='i' MetadataA='123' MetadataB='xyz' />
93+
</ItemGroup>
94+
</Target>
95+
</Project>
96+
""";
97+
98+
ProjectItemElement item = GetItemFromContent(project);
99+
Assert.Equal(2, item.Metadata.Count);
100+
ProjectMetadataElement metadatum1 = item.Metadata.First();
101+
ProjectMetadataElement metadatum2 = item.Metadata.Skip(1).First();
102+
103+
Assert.Equal(4, metadatum1.Location.Line);
104+
Assert.Equal(4, metadatum2.Location.Line);
105+
Assert.Equal(27, metadatum1.Location.Column);
106+
Assert.Equal(43, metadatum2.Location.Column);
107+
}
108+
85109
/// <summary>
86110
/// Read item with no include
87111
/// </summary>

src/Build.UnitTests/BackEnd/MSBuild_Tests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO;
7-
87
using Microsoft.Build.Evaluation;
98
using Microsoft.Build.Execution;
109
using Microsoft.Build.Framework;
@@ -824,7 +823,8 @@ public void ItemsRecursionWithinTarget()
824823
</Target>
825824
</Project>
826825
""";
827-
var projectFile = env.CreateFile("test.proj", ObjectModelHelpers.CleanupFileContents(projectContent));
826+
string projFileName = "test.proj";
827+
var projectFile = env.CreateFile(projFileName, ObjectModelHelpers.CleanupFileContents(projectContent));
828828

829829
MockLogger logger = new MockLogger(_testOutput);
830830
ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile.Path, logger);
@@ -839,6 +839,10 @@ public void ItemsRecursionWithinTarget()
839839
logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Filename"));
840840
logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Extension"));
841841
logger.AssertMessageCount("MSB4120", 6);
842+
// The location of the offending attribute (TargetPath) is transferred - for both metadatums (%(Filename) and %(Extension)) on correct locations in xml
843+
logger.AssertMessageCount($"{projFileName}(4,34):", 2, false);
844+
logger.AssertMessageCount($"{projFileName}(5,34):", 2, false);
845+
logger.AssertMessageCount($"{projFileName}(6,34):", 2, false);
842846
Assert.Equal(0, logger.WarningCount);
843847
Assert.Equal(0, logger.ErrorCount);
844848
}

src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,16 @@ private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucke
185185
if (condition)
186186
{
187187
ExpanderOptions expanderOptions = ExpanderOptions.ExpandAll;
188-
ElementLocation location = metadataInstance.Location;
189188
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) &&
190189
// If multiple buckets were expanded - we do not want to repeat same error for same metadatum on a same line
191190
bucket.BucketSequenceNumber == 0 &&
192191
// Referring to unqualified metadata of other item (transform) is fine.
193192
child.Include.IndexOf("@(", StringComparison.Ordinal) == -1)
194193
{
195194
expanderOptions |= ExpanderOptions.LogOnItemMetadataSelfReference;
196-
// Temporary workaround of unavailability of full Location info on metadata: https://github.com/dotnet/msbuild/issues/8579
197-
location = child.Location;
198195
}
199196

200-
string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, expanderOptions, location, loggingContext);
197+
string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, expanderOptions, metadataInstance.Location, loggingContext);
201198

202199
// This both stores the metadata so we can add it to all the items we just created later, and
203200
// exposes this metadata to further metadata evaluations in subsequent loop iterations.

src/Build/Construction/ProjectMetadataElement.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,13 @@ public string Value
100100
/// Creates an unparented ProjectMetadataElement, wrapping an unparented XmlElement.
101101
/// Caller should then ensure the element is added to a parent.
102102
/// </summary>
103-
internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRootElement containingProject)
103+
internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRootElement containingProject, ElementLocation location = null)
104104
{
105105
XmlUtilities.VerifyThrowArgumentValidElementName(name);
106106
ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
107107
ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name);
108108

109-
XmlElementWithLocation element = containingProject.CreateElement(name);
109+
XmlElementWithLocation element = containingProject.CreateElement(name, location);
110110

111111
return new ProjectMetadataElement(element, containingProject);
112112
}

src/Build/Construction/ProjectRootElement.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,13 +1327,22 @@ public ProjectMetadataElement CreateMetadataElement(string name)
13271327
/// Caller must add it to the location of choice in the project.
13281328
/// </summary>
13291329
public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue)
1330+
{
1331+
return this.CreateMetadataElement(name, unevaluatedValue, null);
1332+
}
1333+
1334+
/// <summary>
1335+
/// Creates a metadata node.
1336+
/// Caller must add it to the location of choice in the project.
1337+
/// </summary>
1338+
public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue, ElementLocation location)
13301339
{
13311340
if (Link != null)
13321341
{
13331342
return RootLink.CreateMetadataElement(name, unevaluatedValue);
13341343
}
13351344

1336-
ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this);
1345+
ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this, location);
13371346

13381347
metadatum.Value = unevaluatedValue;
13391348

@@ -1785,14 +1794,23 @@ internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDicti
17851794
return projectRootElement;
17861795
}
17871796

1797+
/// <summary>
1798+
/// Creates a metadata node.
1799+
/// Caller must add it to the location of choice in the project.
1800+
/// </summary>
1801+
internal ProjectMetadataElement CreateMetadataElement(XmlAttributeWithLocation attribute)
1802+
{
1803+
return CreateMetadataElement(attribute.Name, attribute.Value, attribute.Location);
1804+
}
1805+
17881806
/// <summary>
17891807
/// Creates a XmlElement with the specified name in the document
17901808
/// containing this project.
17911809
/// </summary>
1792-
internal XmlElementWithLocation CreateElement(string name)
1810+
internal XmlElementWithLocation CreateElement(string name, ElementLocation location = null)
17931811
{
17941812
ErrorUtilities.VerifyThrow(Link == null, "External project");
1795-
return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace);
1813+
return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace, location);
17961814
}
17971815

17981816
/// <summary>

src/Build/ElementLocation/XmlDocumentWithLocation.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.IO;
6+
using System.Threading;
67
using System.Xml;
78
using Microsoft.Build.Internal;
89
using Microsoft.Build.Shared;
@@ -59,6 +60,13 @@ internal class XmlDocumentWithLocation : XmlDocument
5960
/// </summary>
6061
private bool? _loadAsReadOnly;
6162

63+
/// <summary>
64+
/// Location of the element to be created via 'CreateElement' call. So that we can
65+
/// receive and use location from the caller up the stack even if we are being called via
66+
/// <see cref="XmlDocument"/> internal methods.
67+
/// </summary>
68+
private readonly AsyncLocal<ElementLocation> _elementLocation = new AsyncLocal<ElementLocation>();
69+
6270
/// <summary>
6371
/// Constructor
6472
/// </summary>
@@ -180,6 +188,31 @@ public override void Load(string fullPath)
180188
}
181189
}
182190

191+
/// <summary>
192+
/// Called during parse, to add an element.
193+
/// </summary>
194+
/// <remarks>
195+
/// We create our own kind of element, that we can give location information to.
196+
/// In order to pass the location through the callchain, that contains XmlDocument function
197+
/// that then calls back to our XmlDocumentWithLocation (so we cannot use call stack via passing via parameters),
198+
/// we use async local field, that simulates variable on call stack.
199+
/// </remarks>
200+
internal XmlElement CreateElement(string localName, string namespaceURI, ElementLocation location)
201+
{
202+
if (location != null)
203+
{
204+
this._elementLocation.Value = location;
205+
}
206+
try
207+
{
208+
return CreateElement(localName, namespaceURI);
209+
}
210+
finally
211+
{
212+
this._elementLocation.Value = null;
213+
}
214+
}
215+
183216
/// <summary>
184217
/// Called during load, to add an element.
185218
/// </summary>
@@ -192,6 +225,10 @@ public override XmlElement CreateElement(string prefix, string localName, string
192225
{
193226
return new XmlElementWithLocation(prefix, localName, namespaceURI, this, _reader.LineNumber, _reader.LinePosition);
194227
}
228+
else if (_elementLocation?.Value != null)
229+
{
230+
return new XmlElementWithLocation(prefix, localName, namespaceURI, this, _elementLocation.Value.Line, _elementLocation.Value.Column);
231+
}
195232

196233
// Must be a subsequent edit; we can't provide location information
197234
return new XmlElementWithLocation(prefix, localName, namespaceURI, this);

src/Build/Evaluation/ProjectParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ private ProjectItemElement ParseProjectItemElement(XmlElementWithLocation elemen
324324
}
325325
else if (isValidMetadataNameInAttribute)
326326
{
327-
ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute.Name, attribute.Value);
327+
ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute);
328328
metadatum.ExpressedAsAttribute = true;
329329
metadatum.Parent = item;
330330

@@ -744,7 +744,7 @@ private ProjectItemDefinitionElement ParseProjectItemDefinitionXml(XmlElementWit
744744
}
745745
else if (isValidMetadataNameInAttribute)
746746
{
747-
ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute.Name, attribute.Value);
747+
ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute);
748748
metadatum.ExpressedAsAttribute = true;
749749
metadatum.Parent = itemDefinition;
750750

src/Shared/UnitTests/MockLogger.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,13 @@ internal void LoggerEventHandler(object sender, BuildEventArgs eventArgs)
282282
bool logMessage = !(eventArgs is BuildFinishedEventArgs) || LogBuildFinished;
283283
if (logMessage)
284284
{
285-
_fullLog.AppendLine(eventArgs.Message);
286-
_testOutputHelper?.WriteLine(eventArgs.Message);
285+
string msg = eventArgs.Message;
286+
if (eventArgs is BuildMessageEventArgs m && m.LineNumber != 0)
287+
{
288+
msg = $"{m.File}({m.LineNumber},{m.ColumnNumber}): {msg}";
289+
}
290+
_fullLog.AppendLine(msg);
291+
_testOutputHelper?.WriteLine(msg);
287292
}
288293
break;
289294
}
@@ -496,9 +501,9 @@ internal void AssertLogDoesntContain(string contains)
496501
/// </summary>
497502
internal void AssertNoWarnings() => Assert.Equal(0, WarningCount);
498503

499-
internal void AssertMessageCount(string message, int expectedCount)
504+
internal void AssertMessageCount(string message, int expectedCount, bool regexSearch = true)
500505
{
501-
var matches = Regex.Matches(FullLog, message);
506+
var matches = Regex.Matches(FullLog, regexSearch ? message : Regex.Escape(message));
502507
matches.Count.ShouldBe(expectedCount);
503508
}
504509
}

src/Shared/UnitTests/ObjectModelHelpers.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,11 @@ internal static string GetOSPlatformAsString()
11551155
/// </summary>
11561156
internal static int Count(IEnumerable enumerable)
11571157
{
1158+
if (enumerable is ICollection c)
1159+
{
1160+
return c.Count;
1161+
}
1162+
11581163
int i = 0;
11591164
foreach (object _ in enumerable)
11601165
{

src/Shared/XmlUtilities.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation o
3232
return oldElement;
3333
}
3434

35-
XmlElementWithLocation newElement = (xmlNamespace == null)
36-
? (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName)
37-
: (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName, xmlNamespace);
35+
XmlElementWithLocation newElement =
36+
(XmlElementWithLocation)((XmlDocumentWithLocation)oldElement.OwnerDocument).CreateElement(newElementName, xmlNamespace ?? string.Empty, oldElement.Location);
3837

3938
// Copy over all the attributes.
4039
foreach (XmlAttribute oldAttribute in oldElement.Attributes)

0 commit comments

Comments
 (0)