Skip to content

Commit cc55017

Browse files
authored
Log item self-expansion (#8581)
Fixes #8527 Context Self referencing item metadata in an item definition within the target leads to possible unintended expansion (and cross-applying of pre-existing item instances). This behavior is not a bug - it's the implication of the target batching feature - but might be confusing. So detection and warning is added here Changes Made A self reference (qualified or unqualified) of metadata within the item defeiniton (which is within a target) is detected and high importance message is issued. Testing Tests added for a warning case and for a non-warning case (self-referencing metadata outside of target context) Doc MicrosoftDocs/visualstudio-docs-pr#11034 Possible impact It's unfortunately hard to obtain data on prevalence of this pattern due to limitations of available code searches (github search doesn't support '@' sign and no escaping option; SourceGraph, grep.app doesn't support regex lookaheads). So I cannot quantify (I'll try again later on). But there are some sparse evidence of usage: https://github.com/JeremyAnsel/JeremyAnsel.HLSL.Targets/blob/master/JeremyAnsel.HLSL.Targets/JeremyAnsel.HLSL.Targets/JeremyAnsel.HLSL.Targets.csproj#L49 https://github.com/neuecc/MessagePack-CSharp/blob/master/src/MessagePack.MSBuild.Tasks/MessagePack.MSBuild.Tasks.csproj#L35 https://github.com/dotnet/aspnetcore/blob/main/eng/AfterSigning.targets#L22 all of those seem to be doing something else then intended (the variable part of path is empty, so it gets just the base directory), but they do not break. tl;dr;: unless we are able to better quantify, we might resort to demote the warning to a message Demoted to Message severity
1 parent cb5e760 commit cc55017

24 files changed

+285
-13
lines changed

documentation/wiki/ChangeWaves.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t
2929
- [Log an error when no provided search path for an import exists](https://github.com/dotnet/msbuild/pull/8095)
3030
- [Log assembly loads](https://github.com/dotnet/msbuild/pull/8316)
3131
- [AnyHaveMetadataValue returns false when passed an empty list](https://github.com/dotnet/msbuild/pull/8603)
32+
- [Log item self-expansion](https://github.com/dotnet/msbuild/pull/8581)
3233

3334
### 17.4
3435
- [Respect deps.json when loading assemblies](https://github.com/dotnet/msbuild/pull/7520)

src/Build.UnitTests/BackEnd/MSBuild_Tests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,116 @@ public void ItemsIncludeExcludePathsCombinations()
772772
}
773773
}
774774

775+
/// <summary>
776+
/// Referring to an item outside of target leads to 'naturally expected' reference to the item being processed.
777+
/// No expansion occurs.
778+
/// </summary>
779+
[Fact]
780+
public void ItemsRecursionOutsideTarget()
781+
{
782+
using TestEnvironment env = TestEnvironment.Create();
783+
string projectContent = """
784+
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
785+
<ItemGroup>
786+
<iout1 Include='a/b.foo' TargetPath='%(Filename)%(Extension)' />
787+
<iout1 Include='c/d.foo' TargetPath='%(Filename)%(Extension)' />
788+
<iout1 Include='g/h.foo' TargetPath='%(Filename)%(Extension)' />
789+
</ItemGroup>
790+
<Target Name='a'>
791+
<Message Text="iout1=[@(iout1)]" Importance='High' />
792+
<Message Text="iout1-target-paths=[@(iout1->'%(TargetPath)')]" Importance='High' />
793+
</Target>
794+
</Project>
795+
""";
796+
var projectFile = env.CreateFile("test.proj", ObjectModelHelpers.CleanupFileContents(projectContent));
797+
798+
MockLogger logger = new MockLogger(_testOutput);
799+
ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile.Path, logger);
800+
801+
_testOutput.WriteLine(logger.FullLog);
802+
803+
logger.AssertLogContains("iout1=[a/b.foo;c/d.foo;g/h.foo]");
804+
logger.AssertLogContains("iout1-target-paths=[b.foo;d.foo;h.foo]");
805+
}
806+
807+
/// <summary>
808+
/// Referring to an item within target leads to item expansion which might be unintended behavior - hence warning.
809+
/// </summary>
810+
[Fact]
811+
public void ItemsRecursionWithinTarget()
812+
{
813+
using TestEnvironment env = TestEnvironment.Create();
814+
string projectContent = """
815+
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
816+
<Target Name='a'>
817+
<ItemGroup>
818+
<iin1 Include='a/b.foo' TargetPath='%(Filename)%(Extension)' />
819+
<iin1 Include='c/d.foo' TargetPath='%(Filename)%(Extension)' />
820+
<iin1 Include='g/h.foo' TargetPath='%(Filename)%(Extension)' />
821+
</ItemGroup>
822+
<Message Text="iin1=[@(iin1)]" Importance='High' />
823+
<Message Text="iin1-target-paths=[@(iin1->'%(TargetPath)')]" Importance='High' />
824+
</Target>
825+
</Project>
826+
""";
827+
var projectFile = env.CreateFile("test.proj", ObjectModelHelpers.CleanupFileContents(projectContent));
828+
829+
MockLogger logger = new MockLogger(_testOutput);
830+
ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile.Path, logger);
831+
832+
_testOutput.WriteLine(logger.FullLog);
833+
834+
logger.AssertLogDoesntContain("iin1=[a/b.foo;c/d.foo;g/h.foo]");
835+
logger.AssertLogDoesntContain("iin1-target-paths=[b.foo;d.foo;h.foo]");
836+
logger.AssertLogContains("iin1=[a/b.foo;c/d.foo;g/h.foo;g/h.foo]");
837+
logger.AssertLogContains("iin1-target-paths=[;b.foo;b.foo;d.foo]");
838+
839+
logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Filename"));
840+
logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Extension"));
841+
logger.AssertMessageCount("MSB4120", 6);
842+
Assert.Equal(0, logger.WarningCount);
843+
Assert.Equal(0, logger.ErrorCount);
844+
}
845+
846+
/// <summary>
847+
/// Referring to an unrelated item within target leads to expected expansion.
848+
/// </summary>
849+
[Fact]
850+
public void UnrelatedItemsRecursionWithinTarget()
851+
{
852+
using TestEnvironment env = TestEnvironment.Create();
853+
string projectContent = """
854+
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
855+
<ItemGroup>
856+
<iout1 Include='a/b.foo'/>
857+
<iout1 Include='c/d.foo'/>
858+
<iout1 Include='g/h.foo'/>
859+
</ItemGroup>
860+
861+
<Target Name='a'>
862+
<ItemGroup>
863+
<iin1 Include='@(iout1)' TargetPath='%(Filename)%(Extension)' />
864+
</ItemGroup>
865+
<Message Text="iin1=[@(iin1)]" Importance='High' />
866+
<Message Text="iin1-target-paths=[@(iin1->'%(TargetPath)')]" Importance='High' />
867+
</Target>
868+
</Project>
869+
""";
870+
var projectFile = env.CreateFile("test.proj", ObjectModelHelpers.CleanupFileContents(projectContent));
871+
872+
MockLogger logger = new MockLogger(_testOutput);
873+
ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile.Path, logger);
874+
875+
_testOutput.WriteLine(logger.FullLog);
876+
877+
logger.AssertLogContains("iin1=[a/b.foo;c/d.foo;g/h.foo]");
878+
logger.AssertLogContains("iin1-target-paths=[b.foo;d.foo;h.foo]");
879+
880+
logger.AssertLogDoesntContain("MSB4120");
881+
Assert.Equal(0, logger.WarningCount);
882+
Assert.Equal(0, logger.ErrorCount);
883+
}
884+
775885
/// <summary>
776886
/// Check if passing different global properties via metadata works
777887
/// </summary>

src/Build/BackEnd/Components/Logging/LoggingContext.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ internal void LogComment(MessageImportance importance, string messageResourceNam
128128
_loggingService.LogComment(_eventContext, importance, messageResourceName, messageArgs);
129129
}
130130

131+
/// <summary>
132+
/// Helper method to create a message build event from a string resource and some parameters
133+
/// </summary>
134+
/// <param name="importance">Importance level of the message</param>
135+
/// <param name="file">The file in which the event occurred</param>
136+
/// <param name="messageResourceName">string within the resource which indicates the format string to use</param>
137+
/// <param name="messageArgs">string resource arguments</param>
138+
internal void LogComment(MessageImportance importance, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs)
139+
{
140+
ErrorUtilities.VerifyThrow(_isValid, "must be valid");
141+
142+
_loggingService.LogBuildEvent(new BuildMessageEventArgs(
143+
null,
144+
null,
145+
file.File,
146+
file.Line,
147+
file.Column,
148+
file.EndLine,
149+
file.EndColumn,
150+
ResourceUtilities.GetResourceString(messageResourceName),
151+
helpKeyword: null,
152+
senderName: "MSBuild",
153+
importance,
154+
DateTime.UtcNow,
155+
messageArgs)
156+
{
157+
BuildEventContext = _eventContext
158+
});
159+
}
160+
131161
/// <summary>
132162
/// Helper method to create a message build event from a string
133163
/// </summary>

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,20 @@ private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucke
184184

185185
if (condition)
186186
{
187-
string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, ExpanderOptions.ExpandAll, metadataInstance.Location, loggingContext);
187+
ExpanderOptions expanderOptions = ExpanderOptions.ExpandAll;
188+
ElementLocation location = metadataInstance.Location;
189+
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) &&
190+
// If multiple buckets were expanded - we do not want to repeat same error for same metadatum on a same line
191+
bucket.BucketSequenceNumber == 0 &&
192+
// Referring to unqualified metadata of other item (transform) is fine.
193+
child.Include.IndexOf("@(", StringComparison.Ordinal) == -1)
194+
{
195+
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;
198+
}
199+
200+
string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, expanderOptions, location, loggingContext);
188201

189202
// This both stores the metadata so we can add it to all the items we just created later, and
190203
// exposes this metadata to further metadata evaluations in subsequent loop iterations.
@@ -612,7 +625,7 @@ private List<ProjectItemInstance> FindItemsMatchingMetadataSpecification(
612625
/// 1. The metadata table created for the bucket, may be null.
613626
/// 2. The metadata table derived from the item definition group, may be null.
614627
/// </summary>
615-
private class NestedMetadataTable : IMetadataTable
628+
private class NestedMetadataTable : IMetadataTable, IItemTypeDefinition
616629
{
617630
/// <summary>
618631
/// The table for all metadata added during expansion
@@ -722,6 +735,8 @@ internal void SetValue(string name, string value)
722735
{
723736
_addTable[name] = value;
724737
}
738+
739+
string IItemTypeDefinition.ItemType => _itemType;
725740
}
726741
}
727742
}

src/Build/Definition/ProjectItemDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Microsoft.Build.Evaluation
2626
/// ProjectMetadataElement, and these can be added, removed, and modified.
2727
/// </remarks>
2828
[DebuggerDisplay("{_itemType} #Metadata={MetadataCount}")]
29-
public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadata>, IProjectMetadataParent
29+
public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadata>, IProjectMetadataParent, IItemTypeDefinition
3030
{
3131
/// <summary>
3232
/// Project that this item definition lives in.

src/Build/Evaluation/Expander.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ internal enum ExpanderOptions
8888
/// </summary>
8989
Truncate = 0x40,
9090

91+
/// <summary>
92+
/// Issues build message if item references unqualified or qualified metadata odf self - as this can lead to unintended expansion and
93+
/// cross-combination of other items.
94+
/// More info: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-batching#item-batching-on-self-referencing-metadata
95+
/// </summary>
96+
LogOnItemMetadataSelfReference = 0x80,
97+
9198
/// <summary>
9299
/// Expand only properties and then item lists
93100
/// </summary>
@@ -441,7 +448,7 @@ internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions
441448

442449
ErrorUtilities.VerifyThrowInternalNull(elementLocation, nameof(elementLocation));
443450

444-
string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation);
451+
string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation, loggingContext);
445452
result = PropertyExpander<P>.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _usedUninitializedProperties, _fileSystem, loggingContext);
446453
result = ItemExpander.ExpandItemVectorsIntoString<I>(this, result, _items, options, elementLocation);
447454
result = FileUtilities.MaybeAdjustFilePath(result);
@@ -871,8 +878,9 @@ private static class MetadataExpander
871878
/// <param name="metadata">The metadata to be expanded.</param>
872879
/// <param name="options">Used to specify what to expand.</param>
873880
/// <param name="elementLocation">The location information for error reporting purposes.</param>
881+
/// <param name="loggingContext">The logging context for this operation.</param>
874882
/// <returns>The string with item metadata expanded in-place, escaped.</returns>
875-
internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation)
883+
internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null)
876884
{
877885
try
878886
{
@@ -896,7 +904,7 @@ internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTa
896904
{
897905
// if there are no item vectors in the string
898906
// run a simpler Regex to find item metadata references
899-
MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options);
907+
MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);
900908
result = RegularExpressions.ItemMetadataPattern.Value.Replace(expression, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata));
901909
}
902910
else
@@ -915,7 +923,7 @@ internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTa
915923
using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder();
916924

917925
int start = 0;
918-
MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options);
926+
MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);
919927

920928
if (itemVectorExpressions != null)
921929
{
@@ -993,13 +1001,23 @@ private class MetadataMatchEvaluator
9931001
/// </summary>
9941002
private ExpanderOptions _options;
9951003

1004+
private IElementLocation _elementLocation;
1005+
1006+
private LoggingContext _loggingContext;
1007+
9961008
/// <summary>
9971009
/// Constructor taking a source of metadata.
9981010
/// </summary>
999-
internal MetadataMatchEvaluator(IMetadataTable metadata, ExpanderOptions options)
1011+
internal MetadataMatchEvaluator(
1012+
IMetadataTable metadata,
1013+
ExpanderOptions options,
1014+
IElementLocation elementLocation,
1015+
LoggingContext loggingContext)
10001016
{
10011017
_metadata = metadata;
1002-
_options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate);
1018+
_options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference);
1019+
_elementLocation = elementLocation;
1020+
_loggingContext = loggingContext;
10031021

10041022
ErrorUtilities.VerifyThrow(options != ExpanderOptions.Invalid, "Must be expanding metadata of some kind");
10051023
}
@@ -1030,6 +1048,17 @@ internal string ExpandSingleMetadata(Match itemMetadataMatch)
10301048
(!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0)))
10311049
{
10321050
metadataValue = _metadata.GetEscapedValue(itemType, metadataName);
1051+
1052+
if ((_options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 &&
1053+
_loggingContext != null &&
1054+
!string.IsNullOrEmpty(metadataName) &&
1055+
_metadata is IItemTypeDefinition itemMetadata &&
1056+
(string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal)))
1057+
{
1058+
_loggingContext.LogComment(MessageImportance.High, new BuildEventFileInfo(_elementLocation),
1059+
"ItemReferencingSelfInTarget", itemMetadata.ItemType, metadataName);
1060+
}
1061+
10331062
if (IsTruncationEnabled(_options) && metadataValue.Length > CharacterLimitPerExpansion)
10341063
{
10351064
metadataValue = metadataValue.Substring(0, CharacterLimitPerExpansion - 3) + "...";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Build.Evaluation;
5+
6+
internal interface IItemTypeDefinition
7+
{
8+
/// <summary>
9+
/// The item type to which this metadata applies.
10+
/// </summary>
11+
string ItemType { get; }
12+
}

src/Build/Instance/ProjectItemDefinitionInstance.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace Microsoft.Build.Execution
2020
/// Immutable.
2121
/// </summary>
2222
[DebuggerDisplay("{_itemType} #Metadata={MetadataCount}")]
23-
public class ProjectItemDefinitionInstance : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadataInstance>, ITranslatable
23+
public class ProjectItemDefinitionInstance : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadataInstance>, ITranslatable, IItemTypeDefinition
2424
{
2525
/// <summary>
2626
/// Item type, for example "Compile", that this item definition applies to
@@ -235,5 +235,7 @@ internal static ProjectItemDefinitionInstance FactoryForDeserialization(ITransla
235235

236236
return instance;
237237
}
238+
239+
string IItemTypeDefinition.ItemType => _itemType;
238240
}
239241
}

src/Build/Instance/ProjectItemInstance.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public class ProjectItemInstance :
3333
ITaskItem2,
3434
IMetadataTable,
3535
ITranslatable,
36-
IMetadataContainer
36+
IMetadataContainer,
37+
IItemTypeDefinition
3738
{
3839
/// <summary>
3940
/// The project instance to which this item belongs.
@@ -2137,7 +2138,7 @@ public void SetMetadata(IEnumerable<Pair<ProjectMetadataElement, string>> metada
21372138
/// Also, more importantly, because typically the same regular metadata values can be shared by many items,
21382139
/// and keeping item-specific metadata out of it could allow it to be implemented as a copy-on-write table.
21392140
/// </summary>
2140-
private class BuiltInMetadataTable : IMetadataTable
2141+
private class BuiltInMetadataTable : IMetadataTable, IItemTypeDefinition
21412142
{
21422143
/// <summary>
21432144
/// Item type
@@ -2195,6 +2196,8 @@ public string GetEscapedValueIfPresent(string requiredItemType, string name)
21952196

21962197
return value;
21972198
}
2199+
2200+
string IItemTypeDefinition.ItemType => _itemType;
21982201
}
21992202
}
22002203

src/Build/Microsoft.Build.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
<Compile Include="BackEnd\Components\SdkResolution\SdkResolverException.cs" />
161161
<Compile Include="BackEnd\Components\SdkResolution\TranslationHelpers.cs" />
162162
<Compile Include="FileSystem\*.cs" />
163+
<Compile Include="Evaluation\IItemTypeDefinition.cs" />
163164
<Compile Include="Utilities\ReaderWriterLockSlimExtensions.cs" />
164165
<Compile Include="BackEnd\Node\ConsoleOutput.cs" />
165166
<Compile Include="BackEnd\Node\PartialBuildTelemetry.cs" />

0 commit comments

Comments
 (0)