Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Remove item operation to match on specified metadata instead of just item identity #4997

Merged
merged 26 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
94e4bb8
Profile Update operations too
cdmihai Dec 13, 2019
cbcc7f7
Allow qualified metadata references in the Update item operation
cdmihai Dec 11, 2019
054dc81
Avoid double matching
cdmihai Dec 13, 2019
aa46e27
Avoid type checks by unifying type params P, I
cdmihai Dec 17, 2019
d420e43
Renames and formatting
cdmihai Dec 17, 2019
263af2b
Resharper fixes
cdmihai Dec 17, 2019
0a3fd78
Add escape hatch to return to old behavior
cdmihai Dec 17, 2019
292334c
remove unnecessary type check
cdmihai Dec 17, 2019
7aa753e
Add Remove with MatchOnMetadata
cdmihai Dec 19, 2019
9738d93
Adding match on metadata implementation for remove not under targets,…
sfoslund Jan 2, 2020
e8d60e3
Adding MatchOnMetadata support for references, responding to PR feedback
sfoslund Jan 3, 2020
604c723
Updating remove on metadata with style changes from PR feedback
sfoslund Jan 7, 2020
c027544
Updating remove on metadata implementation to disallow multiple metad…
sfoslund Jan 15, 2020
229ec05
PR feedback for remove on metadata: renaming tests, converting to linq
sfoslund Jan 16, 2020
56b27de
Adding MatchOnMetadataOptions, implemented CaseInsensitive option
sfoslund Jan 23, 2020
9fcbd61
Adding path like option for MatchOnMetadata implementation and tests,…
sfoslund Jan 23, 2020
1c3454f
Updating error message wording and matchOnMetadata path comparison
sfoslund Jan 30, 2020
f32a94f
Adding file system case sensitivity detection for path comparison
sfoslund Jan 31, 2020
cd322a9
Updating file path comparison tests for case sensitivity changes
sfoslund Feb 4, 2020
374b563
Updating path comparison to match BCL implementation
sfoslund Feb 4, 2020
d71fa0d
Converting all testing file case senstivity tests to FileUtilities im…
sfoslund Feb 5, 2020
16a9a27
Merge branch 'master' into removeOn
sfoslund Mar 10, 2020
e103559
PR feedback for remove on MatchOnMetadata
sfoslund Mar 16, 2020
e3dbc87
PR feedback for match on metadata
sfoslund Apr 14, 2020
f1c5d97
Merge branch 'master' into removeOn
sfoslund Apr 14, 2020
ea92b7f
MatchOnMetadata PR feedback
sfoslund Apr 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions src/Build.OM.UnitTests/Definition/ProjectItem_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2173,6 +2173,49 @@ public void RemoveWithItemReferenceOnMatchingMetadata()
items.ElementAt(2).GetMetadataValue("M2").ShouldBe("d");
}

[Fact]
public void RemoveWithItemReferenceOnMatchingMetadataReferences()
{
string content =
@"<Project>
<PropertyGroup>
<p0>v0</p0>
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>
<ItemGroup>
<Meta2 Include='M2'/>
<Meta2 Include='g'/>

<I1 Include='a1' v0='1' M2='a'/>
<I1 Include='b1' v0='2' M2='x'/>
<I1 Include='c1' v0='3' M2='y'/>
<I1 Include='d1' v0='4' M2='b'/>

<I2 Include='a2' v0='x' m2='c'/>
<I2 Include='b2' v0='2' m2='x'/>
<I2 Include='c2' v0='3' m2='Y'/>
<I2 Include='d2' v0='y' m2='d'/>

<I2 Remove='@(I1)' MatchOnMetadata='$(Meta1);@(Meta2)' />
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>";

using (var env = TestEnvironment.Create())
{
var project = ObjectModelHelpers.CreateInMemoryProject(env.CreateProjectCollection().Collection, content, null, null);

var items = project.ItemsIgnoringCondition.Where(i => i.ItemType.Equals("I2"));

items.Select(i => i.EvaluatedInclude).ShouldBe(new[] { "a2", "c2", "d2" });

items.ElementAt(0).GetMetadataValue("v0").ShouldBe("x");
items.ElementAt(0).GetMetadataValue("M2").ShouldBe("c");
items.ElementAt(1).GetMetadataValue("v0").ShouldBe("3");
items.ElementAt(1).GetMetadataValue("M2").ShouldBe("Y");
items.ElementAt(2).GetMetadataValue("v0").ShouldBe("y");
items.ElementAt(2).GetMetadataValue("M2").ShouldBe("d");
}
}

[Fact]
public void KeepWithItemReferenceOnNonmatchingMetadata()
{
Expand Down Expand Up @@ -2225,7 +2268,7 @@ public void FailWithMultipleItemReferenceOnMatchingMetadata()

<I3 Remove='@(I1);@(I2)' MatchOnMetadata='M1;m2' />");

Assert.ThrowsAny<Exception>(() => ObjectModelHelpers.CreateInMemoryProject(content));
Assert.ThrowsAny<InvalidProjectFileException>(() => ObjectModelHelpers.CreateInMemoryProject(content));
}

[Fact]
Expand All @@ -2244,7 +2287,7 @@ public void FailWithMetadataItemReferenceOnMatchingMetadata()

<I2 Remove='%(I1.M1)' MatchOnMetadata='M1;m2' />");

Assert.ThrowsAny<Exception>(() => ObjectModelHelpers.CreateInMemoryProject(content));
Assert.ThrowsAny<InvalidProjectFileException>(() => ObjectModelHelpers.CreateInMemoryProject(content));
}

[Fact]
Expand Down
48 changes: 46 additions & 2 deletions src/Build.UnitTests/BackEnd/IntrinsicTask_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,50 @@ public void RemoveWithItemReferenceOnMatchingMetadata()
items.ElementAt(2).GetMetadataValue("M2").ShouldBe("d");
}

[Fact]
public void RemoveWithItemReferenceOnMatchingMetadataReferences()
{
// <PropertyGroup>
// <p0>v0</p0>
// </PropertyGroup>
string content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
<Target Name='t'>
<ItemGroup>
<Meta2 Include='M2'/>
<Meta2 Include='g'/>

<I1 Include='a1' v0='1' M2='a'/>
<I1 Include='b1' v0='2' M2='x'/>
<I1 Include='c1' v0='3' M2='y'/>
<I1 Include='d1' v0='4' M2='b'/>

<I2 Include='a2' v0='x' m2='c'/>
<I2 Include='b2' v0='2' m2='x'/>
<I2 Include='c2' v0='3' m2='Y'/>
<I2 Include='d2' v0='y' m2='d'/>

<I2 Remove='@(I1)' MatchOnMetadata='$(p0);@(Meta2)' />
</ItemGroup>
</Target></Project>");

IntrinsicTask task = CreateIntrinsicTask(content);
PropertyDictionary<ProjectPropertyInstance> properties = GeneratePropertyGroup();
Lookup lookup = LookupHelpers.CreateLookup(properties);
ExecuteTask(task, lookup);

var items = lookup.GetItems("I2");

items.Select(i => i.EvaluatedInclude).ShouldBe(new[] { "a2", "c2", "d2" });

items.ElementAt(0).GetMetadataValue("v0").ShouldBe("x");
items.ElementAt(0).GetMetadataValue("M2").ShouldBe("c");
items.ElementAt(1).GetMetadataValue("v0").ShouldBe("3");
items.ElementAt(1).GetMetadataValue("M2").ShouldBe("Y");
items.ElementAt(2).GetMetadataValue("v0").ShouldBe("y");
items.ElementAt(2).GetMetadataValue("M2").ShouldBe("d");
}

[Fact]
public void KeepWithItemReferenceOnNonmatchingMetadata()
{
Expand Down Expand Up @@ -1814,7 +1858,7 @@ public void FailWithMultipleItemReferenceOnMatchingMetadata()
</Target></Project>");
IntrinsicTask task = CreateIntrinsicTask(content);
Lookup lookup = LookupHelpers.CreateEmptyLookup();
Assert.ThrowsAny<InternalErrorException>(() => ExecuteTask(task, lookup));
Assert.ThrowsAny<InvalidProjectFileException>(() => ExecuteTask(task, lookup));
}

[Fact]
Expand All @@ -1839,7 +1883,7 @@ public void FailWithMetadataItemReferenceOnMatchingMetadata()
</Target></Project>");
IntrinsicTask task = CreateIntrinsicTask(content);
Lookup lookup = LookupHelpers.CreateEmptyLookup();
Assert.ThrowsAny<InternalErrorException>(() => ExecuteTask(task, lookup));
Assert.ThrowsAny<InvalidProjectFileException>(() => ExecuteTask(task, lookup));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,28 +272,21 @@ private List<ProjectItemInstance> FindItemsUsingMatchOnMetadata(

var itemSpec = new ItemSpec<ProjectPropertyInstance, ProjectItemInstance>(child.Remove, bucket.Expander, child.RemoveLocation, Project.Directory, true);

// todo create OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem resource
ErrorUtilities.VerifyThrowInvalidOperation(
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
itemSpec.Fragments.Count == 1
&& itemSpec.Fragments.First() is ItemSpec<ProjectPropertyInstance, ProjectItemInstance>.ItemExpressionFragment,
new BuildEventFileInfo(string.Empty),
"OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem",
child.RemoveLocation,
child.Remove);

var itemFragment = itemSpec.Fragments.First() as ItemSpec<ProjectPropertyInstance, ProjectItemInstance>.ItemExpressionFragment;

var itemsToRemove = new List<ProjectItemInstance>();
sfoslund marked this conversation as resolved.
Show resolved Hide resolved

foreach (var item in items)
{
foreach (var referencedItem in itemFragment.ReferencedItems)
if (itemSpec.MatchesItemOnMetadata(item, matchOnMetadata))
{
if (matchOnMetadata.All(
m =>
item.GetMetadataValue(m).Equals(referencedItem.Item.GetMetadataValue(m)) && !item.GetMetadataValue(m).Equals(string.Empty)))
{
itemsToRemove.Add(item);
}
itemsToRemove.Add(item);
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/Build/Evaluation/ItemSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ public override bool IsMatch(string itemToMatch)
return ReferencedItems.Any(v => v.ItemAsValueFragment.IsMatch(itemToMatch));
}

public override bool IsMatchOnMetadata(IItem itemToMatch, ImmutableList<string> metadata)
public override bool IsMatchOnMetadata(IItem item, IEnumerable<string> metadata)
{
foreach (var referencedItem in ReferencedItems)
{
if (metadata.All(m =>
itemToMatch.GetMetadataValue(m).Equals(referencedItem.Item.GetMetadataValue(m)) && !referencedItem.Item.GetMetadataValue(m).Equals(string.Empty)))
var nonEmptyMetadata = metadata.Where(m => !item.GetMetadataValue(m).Equals(string.Empty) || !referencedItem.Item.GetMetadataValue(m).Equals(string.Empty));
if (nonEmptyMetadata.Count() != 0 && nonEmptyMetadata.All(
m => item.GetMetadataValue(m).Equals(referencedItem.Item.GetMetadataValue(m))))
{
return true;
}
Expand Down Expand Up @@ -313,7 +314,7 @@ public bool MatchesItem(I item)
/// <param name="item">The item to attempt to find a match for based on matching metadata</param>
/// <param name="metadata">Names of metadata to look for matches for</param>
/// <returns></returns>
public bool MatchesItemOnMetadata(IItem item, ImmutableList<string> metadata)
public bool MatchesItemOnMetadata(IItem item, IEnumerable<string> metadata)
{
foreach (var fragment in Fragments)
cdmihai marked this conversation as resolved.
Show resolved Hide resolved
{
Expand Down Expand Up @@ -454,7 +455,7 @@ public virtual bool IsMatch(string itemToMatch)
return FileMatcher.IsMatch(itemToMatch);
}

public virtual bool IsMatchOnMetadata(IItem itemToMatch, ImmutableList<string> metadata)
public virtual bool IsMatchOnMetadata(IItem itemToMatch, IEnumerable<string> metadata)
{
return false;
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down
26 changes: 19 additions & 7 deletions src/Build/Evaluation/LazyItemEvaluator.RemoveOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using System.Collections.Immutable;
using System.Linq;
Expand All @@ -14,24 +15,27 @@ class RemoveOperation : LazyItemOperation
{
readonly ImmutableList<string> _matchOnMetadata;

public RemoveOperation(OperationBuilder builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator)
public RemoveOperation(RemoveOperationBuilder builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator)
: base(builder, lazyEvaluator)
{
_matchOnMetadata = builder.ItemElement.MatchOnMetadata.Equals(string.Empty) ? null : builder.ItemElement.MatchOnMetadata.Split(';').ToImmutableList();
_matchOnMetadata = builder.MatchOnMetadata.ToImmutable();
}

// todo port the self referencing matching optimization (e.g. <I Remove="@(I)">) from Update to Remove as well. Ideally make one mechanism for both. https://github.com/Microsoft/msbuild/issues/2314
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
// todo Perf: do not match against the globs: https://github.com/Microsoft/msbuild/issues/2329
protected override ImmutableList<I> SelectItems(ImmutableList<ItemData>.Builder listBuilder, ImmutableHashSet<string> globsToIgnore)
{
if (_matchOnMetadata != null && (_itemSpec.Fragments.Count != 1 || _itemSpec.ItemSpecString.Contains('%')))
{
throw new InternalErrorException("Only a single item reference allowed when removing with metadata match");
}
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
_matchOnMetadata.IsEmpty || (!_matchOnMetadata.IsEmpty
&& _itemSpec.Fragments.Count == 1
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
&& _itemSpec.Fragments.First() is ItemSpec<ProjectProperty, ProjectItem>.ItemExpressionFragment),
new BuildEventFileInfo(string.Empty),
"OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem");

var items = ImmutableHashSet.CreateBuilder<I>();
foreach (ItemData item in listBuilder)
{
if ((_matchOnMetadata == null && _itemSpec.MatchesItem(item.Item)) || (_matchOnMetadata != null && _itemSpec.MatchesItemOnMetadata(item.Item, _matchOnMetadata)))
if ((_matchOnMetadata.IsEmpty && _itemSpec.MatchesItem(item.Item)) || (!_matchOnMetadata.IsEmpty && _itemSpec.MatchesItemOnMetadata(item.Item, _matchOnMetadata)))
items.Add(item.Item);
}

Expand Down Expand Up @@ -64,5 +68,13 @@ public ImmutableHashSet<string>.Builder GetRemovedGlobs()
return builder;
}
}
class RemoveOperationBuilder : OperationBuilder
{
public ImmutableList<string>.Builder MatchOnMetadata { get; } = ImmutableList.CreateBuilder<string>();

public RemoveOperationBuilder(ProjectItemElement itemElement, bool conditionResult) : base(itemElement, conditionResult)
{
}
}
}
}
32 changes: 30 additions & 2 deletions src/Build/Evaluation/LazyItemEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,38 @@ private IncludeOperation BuildIncludeOperation(string rootDirectory, ProjectItem

private RemoveOperation BuildRemoveOperation(string rootDirectory, ProjectItemElement itemElement, bool conditionResult)
{
OperationBuilder operationBuilder = new OperationBuilder(itemElement, conditionResult);
RemoveOperationBuilder operationBuilder = new RemoveOperationBuilder(itemElement, conditionResult);

ProcessItemSpec(rootDirectory, itemElement.Remove, itemElement.RemoveLocation, operationBuilder);

// Process MatchOnMetadata
if (itemElement.MatchOnMetadata.Length > 0)
{
string evaluatedmatchOnMetadata = _expander.ExpandIntoStringLeaveEscaped(itemElement.MatchOnMetadata, ExpanderOptions.ExpandProperties, itemElement.MatchOnMetadataLocation);

if (evaluatedmatchOnMetadata.Length > 0)
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
{
var matchOnMetadataSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedmatchOnMetadata);

ImmutableList<string>.Builder metadataPatterns = ImmutableList.CreateBuilder<string>();
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
foreach (var matchOnMetadataSplit in matchOnMetadataSplits)
{
metadataPatterns.Add(matchOnMetadataSplit);
AddItemReferences(matchOnMetadataSplit, operationBuilder, itemElement.MatchOnMetadataLocation);
}

if (metadataPatterns != null)
sfoslund marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (string metadata in metadataPatterns)
{
string metadataExpanded = _expander.ExpandIntoStringLeaveEscaped(metadata, ExpanderOptions.ExpandPropertiesAndItems, itemElement.MatchOnMetadataLocation);
var metadataSplits = ExpressionShredder.SplitSemiColonSeparatedList(metadataExpanded);
operationBuilder.MatchOnMetadata.AddRange(metadataSplits);
}
}
}
}

return new RemoveOperation(operationBuilder, this);
}

Expand Down Expand Up @@ -578,7 +606,7 @@ private void ProcessMetadataElements(ProjectItemElement itemElement, OperationBu
}
}

private void AddItemReferences(string expression, IncludeOperationBuilder operationBuilder, IElementLocation elementLocation)
private void AddItemReferences(string expression, OperationBuilder operationBuilder, IElementLocation elementLocation)
{
if (expression.Length == 0)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Build/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,9 @@ Utilization: {0} Average Utilization: {1:###.0}</value>
<data name="OM_MatchingProjectAlreadyInCollection">
<value>An equivalent project (a project with the same global properties and tools version) is already present in the project collection, with the path "{0}". To load an equivalent into this project collection, unload this project first.</value>
</data>
<data name="OM_MatchOnMetadataIsRestrictedToOnlyOneReferencedItem">
<value>Removing with MatchOnMetadata only supports one referenced item.</value>
</data>
<data name="OM_ProjectWasNotLoaded">
<value>The project provided was not loaded in the collection.</value>
</data>
Expand Down
5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.en.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Build/Resources/xlf/Strings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading