|
// 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 System.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames;
#nullable disable
namespace Microsoft.Build.BackEnd
{
using ItemsMetadataUpdateDictionary = System.Collections.Generic.Dictionary<Microsoft.Build.Execution.ProjectItemInstance, Microsoft.Build.BackEnd.Lookup.MetadataModifications>;
using ItemTypeToItemsMetadataUpdateDictionary = System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<Microsoft.Build.Execution.ProjectItemInstance, Microsoft.Build.BackEnd.Lookup.MetadataModifications>>;
/// <summary>
/// Contains a list of item and property collections, optimized to allow
/// - very fast "cloning"
/// - quick lookups
/// - scoping down of item subsets in nested scopes (useful for batches)
/// - isolation of adds, removes, modifies, and property sets inside nested scopes
///
/// When retrieving the item group for an item type, each table is consulted in turn,
/// starting with the primary table (the "top" or "innermost" table), until a table is found that has an entry for that type.
/// When an entry is found, it is returned without looking deeper.
/// This makes it possible to let callers see only a subset of items without affecting or cloning the original item groups,
/// by populating a scope with item groups that are empty or contain subsets of items in lower scopes.
///
/// Instances of this class can be cloned with Clone() to share between batches.
///
/// When EnterScope() is called, a fresh primary table is inserted, and all adds and removes will be invisible to
/// any clones made before the scope was entered and anyone who has access to item groups in lower tables.
///
/// When LeaveScope() is called, the primary tables are merged into the secondary tables, and the primary tables are discarded.
/// This makes the adds and removes in the primary tables visible to clones made during the previous scope.
///
/// Scopes can be populated (before Adds, Removes, and Lookups) using PopulateWithItem(). This reduces the set of items of a particular
/// type that are visible in a scope, because lookups of items of this type will stop at this level and see the subset, rather than the
/// larger set in a scope below.
///
/// Items can be added or removed by calling AddNewItem() and RemoveItem(). Only the primary level is modified.
/// When items are added or removed they enter into a primary table exclusively for adds or removes, instead of the main primary table.
/// This allows the adds and removes to be applied to the scope below on LeaveScope(). Even when LeaveScope() is called, the adds and removes
/// stay in their separate add and remove tables: if they were applied to a main table, they could truncate the downward traversal performed by lookups
/// and hide items in a lower main table. Only on the final call of LeaveScope() can all adds and removes be applied to the outermost table, i.e., the project.
///
/// Much the same applies to properties.
///
/// For sensible semantics, only the current primary scope can be modified at any point.
/// </summary>
internal class Lookup : IPropertyProvider<ProjectPropertyInstance>, IItemProvider<ProjectItemInstance>
{
#region Fields
/// <summary>
/// The first set of items used to create the lookup.
/// </summary>
/// <remarks>
/// This represents the primary table for the outer Scope. This is tracked separately as we don't have control
/// over the incoming implementation dictionary, while Scope uses a simplified item dictionary for perf.
/// </remarks>
private readonly IItemDictionary<ProjectItemInstance> _baseItems;
/// <summary>
/// Ordered stack of scopes used for lookup, where each scope references its parent.
/// Each scope contains multiple tables:
/// - the main item table (populated with subsets of lists, in order to create batches)
/// - the add table (items that have been added during execution)
/// - the remove table (items that have been removed during execution)
/// - the modify table (item metadata modifications)
/// - the main property table (populated with properties that are visible in this scope)
/// - the property set table (changes made to properties)
/// All have to be consulted to find the items and properties available in the current scope.
/// We have to keep them separate, because the adds and removes etc need to be applied to the table
/// below when we leave a scope.
/// </summary>
private Lookup.Scope _lookupScopes;
/// <summary>
/// When we are asked for all the items of a certain type using the GetItems() method, we may have to handle items
/// that have been modified earlier with ModifyItems(). These pending modifications can't be applied immediately to
/// the item because that would affect other batches. Instead we clone the item, apply the modification, and hand that over.
/// The problem is that later we might get asked to remove or modify that item. We want to make sure that we record that as
/// a remove or modify of the real item, not the clone we handed over. So we keep a lookup of (clone, original) to consult.
/// </summary>
private Dictionary<ProjectItemInstance, ProjectItemInstance> _cloneTable;
#endregion
#region Constructors
/// <summary>
/// Construct a lookup over specified items and properties.
/// </summary>
internal Lookup(IItemDictionary<ProjectItemInstance> projectItems, PropertyDictionary<ProjectPropertyInstance> properties)
{
ErrorUtilities.VerifyThrowInternalNull(projectItems);
ErrorUtilities.VerifyThrowInternalNull(properties);
_baseItems = projectItems;
_lookupScopes = new Lookup.Scope(this, "Lookup()", properties);
}
/// <summary>
/// Copy constructor (called via Clone() - clearer semantics)
/// </summary>
private Lookup(Lookup that)
{
// Add the same tables from the original.
_baseItems = that._baseItems;
_lookupScopes = that._lookupScopes;
// Clones need to share an (item)clone table; the batching engine asks for items from the lookup,
// then populates buckets with them, which have clone lookups.
_cloneTable = that._cloneTable;
}
#endregion
#region Properties
// Convenience private properties
// "Primary" is the "top" or "innermost" scope
// "Secondary" is the next from the top.
private ItemDictionarySlim PrimaryTable
{
get { return _lookupScopes.Items; }
set { _lookupScopes.Items = value; }
}
private ItemDictionarySlim PrimaryAddTable
{
get { return _lookupScopes.Adds; }
set { _lookupScopes.Adds = value; }
}
private ItemDictionarySlim PrimaryRemoveTable
{
get { return _lookupScopes.Removes; }
set { _lookupScopes.Removes = value; }
}
private ItemTypeToItemsMetadataUpdateDictionary PrimaryModifyTable
{
get { return _lookupScopes.Modifies; }
set { _lookupScopes.Modifies = value; }
}
private PropertyDictionary<ProjectPropertyInstance> PrimaryPropertySets
{
get { return _lookupScopes.PropertySets; }
set { _lookupScopes.PropertySets = value; }
}
private ItemDictionarySlim SecondaryAddTable
{
get { return _lookupScopes.Parent.Adds; }
set { _lookupScopes.Parent.Adds = value; }
}
private ItemDictionarySlim SecondaryRemoveTable
{
get { return _lookupScopes.Parent.Removes; }
set { _lookupScopes.Parent.Removes = value; }
}
private ItemTypeToItemsMetadataUpdateDictionary SecondaryModifyTable
{
get { return _lookupScopes.Parent.Modifies; }
set { _lookupScopes.Parent.Modifies = value; }
}
private PropertyDictionary<ProjectPropertyInstance> SecondaryProperties
{
get { return _lookupScopes.Parent.Properties; }
set { _lookupScopes.Parent.Properties = value; }
}
private PropertyDictionary<ProjectPropertyInstance> SecondaryPropertySets
{
get { return _lookupScopes.Parent.PropertySets; }
set { _lookupScopes.Parent.PropertySets = value; }
}
#endregion
#region Internal Methods
/// <summary>
/// Compares the primary property sets of the passed in lookups to determine if there are properties which are shared between
/// the lookups. We find these shared property names because this indicates that the current Lookup is overriding the property value of another Lookup
/// When an override is detected a messages is generated to inform the users that the property is being changed between batches
/// </summary>
/// <returns>array or error messages to log </returns>
internal List<string> GetPropertyOverrideMessages(Dictionary<string, string> lookupHash)
{
List<string> errorMessages = null;
// For each batch lookup list we need to compare the property items to see if they have already been set
if (PrimaryPropertySets != null)
{
foreach (ProjectPropertyInstance property in PrimaryPropertySets)
{
if (String.Equals(property.Name, ReservedPropertyNames.lastTaskResult, StringComparison.OrdinalIgnoreCase))
{
continue;
}
string propertyName = property.Name;
// If the hash contains the property name, output a messages that displays the previous property value and the new property value
if (lookupHash.TryGetValue(propertyName, out string propertyValue))
{
if (errorMessages == null)
{
errorMessages = new List<string>();
}
errorMessages.Add(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("PropertyOutputOverridden", propertyName, EscapingUtilities.UnescapeAll(propertyValue), property.EvaluatedValue));
}
// Set the value of the hash to the new property value
// PERF: we store the EvaluatedValueEscaped here to avoid unnecessary unescaping (the value is stored
// escaped in the property)
lookupHash[propertyName] = ((IProperty)property).EvaluatedValueEscaped;
}
}
return errorMessages;
}
/// <summary>
/// Clones this object, to create another one with its own list, but the same contents.
/// Then the clone can enter scope and have its own fresh primary list without affecting the other object.
/// </summary>
internal Lookup Clone()
{
return new Lookup(this);
}
/// <summary>
/// Enters the scope using the specified description.
/// Callers keep the scope in order to pass it to <see cref="LeaveScope">LeaveScope</see>.
/// </summary>
internal Lookup.Scope EnterScope(string description)
{
// We don't create the tables unless we need them
Scope scope = new Scope(this, description, null);
_lookupScopes = scope;
return scope;
}
/// <summary>
/// Leaves the specified scope, which must be the active one.
/// Moves all tables up one: the tertiary table becomes the secondary table, and so on. The primary
/// and secondary table are merged. This has the effect of "applying" the adds applied to the primary
/// table into the secondary table.
/// </summary>
private void LeaveScope(Lookup.Scope scopeToLeave)
{
ErrorUtilities.VerifyThrow(_lookupScopes.Count >= 2, "Too many calls to Leave().");
ErrorUtilities.VerifyThrow(Object.ReferenceEquals(scopeToLeave, _lookupScopes), "Attempting to leave with scope '{0}' but scope '{1}' is on top of the stack.", scopeToLeave.Description, _lookupScopes.Description);
// Our lookup works by stopping the first time it finds an item group of the appropriate type.
// So we can't apply an add directly into the table below because that could create a new group
// of that type, which would cause the next lookup to stop there and miss any existing items in a table below.
// Instead we keep adds stored separately until we're leaving the very last scope. Until then
// we only move adds down into the next add table below, and when we lookup we consider both tables.
// Same applies to removes.
if (_lookupScopes.Count == 2)
{
MergeScopeIntoLastScope();
}
else
{
MergeScopeIntoNotLastScope();
}
// Let go of our pointer into the clone table; we assume we won't need it after leaving scope and want to save memory.
// This is an assumption on IntrinsicTask, that it won't ask to remove or modify a clone in a higher scope than it was handed out in.
// We mustn't call cloneTable.Clear() because other clones of this lookup may still be using it. When the last lookup clone leaves scope,
// the table will be collected.
_cloneTable = null;
// Move all tables up one, discarding the primary tables
_lookupScopes = _lookupScopes.Parent;
}
/// <summary>
/// Leaving an arbitrary scope, just merging all the adds, removes, modifies, and sets into the scope below.
/// </summary>
private void MergeScopeIntoNotLastScope()
{
// Move all the adds down
if (PrimaryAddTable != null)
{
if (SecondaryAddTable == null)
{
SecondaryAddTable = PrimaryAddTable;
}
else
{
SecondaryAddTable.ImportItems(PrimaryAddTable);
}
}
// Move all the removes down
if (PrimaryRemoveTable != null)
{
if (SecondaryRemoveTable == null)
{
SecondaryRemoveTable = PrimaryRemoveTable;
}
else
{
// When merging remove lists from two or more batches both tables (primary and secondary) may contain
// identical items. The reason is when removing the items we get the original items rather than a clone,
// so the same item may have already been added to the secondary table.
// For perf, we concatenate these lists without checking for duplicates, deferring until used.
SecondaryRemoveTable.ImportItems(PrimaryRemoveTable);
}
}
// Move all the modifies down
if (PrimaryModifyTable != null)
{
if (SecondaryModifyTable == null)
{
SecondaryModifyTable = PrimaryModifyTable;
}
else
{
foreach (KeyValuePair<string, Dictionary<ProjectItemInstance, MetadataModifications>> entry in PrimaryModifyTable)
{
Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType;
if (SecondaryModifyTable.TryGetValue(entry.Key, out modifiesOfType))
{
// There are already modifies of this type: add to the existing table
foreach (KeyValuePair<ProjectItemInstance, MetadataModifications> modify in entry.Value)
{
MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins);
}
}
else
{
SecondaryModifyTable.Add(entry.Key, entry.Value);
}
}
}
}
// Move all the sets down
if (PrimaryPropertySets != null)
{
if (SecondaryPropertySets == null)
{
SecondaryPropertySets = PrimaryPropertySets;
}
else
{
SecondaryPropertySets.ImportProperties(PrimaryPropertySets);
}
}
}
/// <summary>
/// Merge the current scope down into the base scope. This means applying the adds, removes, modifies, and sets
/// directly into the item and property tables in this scope.
/// </summary>
private void MergeScopeIntoLastScope()
{
// End of the line for this object: we are done with add tables, and we want to expose our
// adds to the world
if (PrimaryAddTable != null)
{
foreach (KeyValuePair<string, List<ProjectItemInstance>> kvp in PrimaryAddTable)
{
_baseItems.ImportItemsOfType(kvp.Key, kvp.Value);
}
}
if (PrimaryRemoveTable != null)
{
foreach (KeyValuePair<string, List<ProjectItemInstance>> kvp in PrimaryRemoveTable)
{
_baseItems.RemoveItems(kvp.Value);
}
}
if (PrimaryModifyTable != null)
{
foreach (KeyValuePair<string, Dictionary<ProjectItemInstance, MetadataModifications>> entry in PrimaryModifyTable)
{
ApplyModificationsToTable(_baseItems, entry.Key, entry.Value);
}
}
if (PrimaryPropertySets != null)
{
SecondaryProperties ??= new PropertyDictionary<ProjectPropertyInstance>(PrimaryPropertySets.Count);
SecondaryProperties.ImportProperties(PrimaryPropertySets);
}
}
/// <summary>
/// Gets the effective property for the current scope.
/// taking the name from the provided string within the specified start and end indexes.
/// If no match is found, returns null.
/// Caller must not modify the property returned.
/// </summary>
public ProjectPropertyInstance GetProperty(string name, int startIndex, int endIndex)
{
// Walk down the tables and stop when the first
// property with this name is found
Scope scope = _lookupScopes;
while (scope != null)
{
if (scope.PropertySets != null)
{
ProjectPropertyInstance property = scope.PropertySets.GetProperty(name, startIndex, endIndex);
if (property != null)
{
return property;
}
}
if (scope.Properties != null)
{
ProjectPropertyInstance property = scope.Properties.GetProperty(name, startIndex, endIndex);
if (property != null)
{
return property;
}
}
scope = scope.Parent;
}
return null;
}
/// <summary>
/// Gets the effective property for the current scope.
/// If no match is found, returns null.
/// Caller must not modify the property returned.
/// </summary>
public ProjectPropertyInstance GetProperty(string name)
{
ErrorUtilities.VerifyThrowInternalLength(name, nameof(name));
return GetProperty(name, 0, name.Length - 1);
}
/// <summary>
/// Gets the items of the specified type that are visible in the current scope.
/// If no match is found, returns an empty list.
/// Caller must not modify the group returned.
/// </summary>
public ICollection<ProjectItemInstance> GetItems(string itemType)
{
// The visible items consist of the adds (accumulated as we go down)
// plus the first set of regular items we encounter
// minus any removes
List<List<ProjectItemInstance>> allAdds = null;
List<List<ProjectItemInstance>> allRemoves = null;
Dictionary<ProjectItemInstance, MetadataModifications> allModifies = null;
ICollection<ProjectItemInstance> groupFound = null;
// Iterate through all scopes *except* the outer scope.
// The outer scope will always be empty and is currently only used for tracking base properties.
// We use the base items only when no other item group is found.
Scope scope = _lookupScopes;
while (scope.Parent != null)
{
// Accumulate adds while we look downwards
if (scope.Adds != null)
{
List<ProjectItemInstance> adds = scope.Adds[itemType];
if (adds != null)
{
allAdds ??= [];
allAdds.Add(adds);
}
}
// Accumulate removes while we look downwards
if (scope.Removes != null)
{
List<ProjectItemInstance> removes = scope.Removes[itemType];
if (removes != null)
{
allRemoves ??= [];
allRemoves.Add(removes);
}
}
// Accumulate modifications as we look downwards
if (scope.Modifies != null)
{
Dictionary<ProjectItemInstance, MetadataModifications> modifies;
if (scope.Modifies.TryGetValue(itemType, out modifies))
{
if (modifies.Count != 0)
{
allModifies ??= new Dictionary<ProjectItemInstance, MetadataModifications>(modifies.Count);
// We already have some modifies for this type
foreach (KeyValuePair<ProjectItemInstance, MetadataModifications> modify in modifies)
{
// If earlier scopes modify the same metadata on the same item,
// they have priority
MergeModificationsIntoModificationTable(allModifies, modify, ModifyMergeType.FirstWins);
}
}
}
}
if (scope.Items != null)
{
groupFound = scope.Items[itemType];
if (groupFound?.Count > 0
|| (scope.ItemTypesToTruncateAtThisScope != null && scope.ItemTypesToTruncateAtThisScope.Contains(itemType)))
{
// Found a group: we go no further
break;
}
}
scope = scope.Parent;
}
// If we've made it to the root scope, use the original items.
if (groupFound == null && scope.Parent == null)
{
groupFound = _baseItems[itemType];
}
if ((allAdds == null) &&
(allRemoves == null) &&
(allModifies == null))
{
// We can just hand out this group verbatim -
// that avoids any importing
groupFound ??= Array.Empty<ProjectItemInstance>();
return groupFound;
}
// Set the initial sizes to avoid resizing during import
int itemsCount = groupFound?.Count ?? 0; // Start with initial set
itemsCount += allAdds?.Count ?? 0; // Add all the additions
itemsCount -= allRemoves?.Count ?? 0; // Remove the removals
if (itemsCount < 0)
{
itemsCount = 0;
}
// We have adds and/or removes and/or modifies to incorporate.
// We can't modify the group, because that might
// be visible to other batches; we have to create
// a new one.
List<ProjectItemInstance> result = new(itemsCount);
if (groupFound?.Count > 0)
{
if (allRemoves == null)
{
// No removes, so use fast path for ICollection<T>.
result.AddRange(groupFound);
}
else
{
// Otherwise, need to filter any items marked for removal.
foreach (ProjectItemInstance item in groupFound)
{
if (!ShouldRemoveItem(item, allRemoves))
{
result.Add(item);
}
}
}
}
// Removes are processed after adds; this means when we remove there's no need to concern ourselves
// with the case where the item being removed is in an add table somewhere. The converse case is not possible
// using a project file: a project file cannot create an item that was already removed, it can only create
// a unique new item.
if (allAdds != null)
{
foreach (List<ProjectItemInstance> adds in allAdds)
{
if (allRemoves == null)
{
// No removes, so use fast path for ICollection<T>.
result.AddRange(adds);
}
else
{
// Otherwise, need to filter any items marked for removal.
foreach (ProjectItemInstance item in adds)
{
if (!ShouldRemoveItem(item, allRemoves))
{
result.Add(item);
}
}
}
}
}
// Modifies can be processed last; if a modified item was removed, the modify will be ignored
if (allModifies != null)
{
ApplyModifies(result, allModifies);
}
return result;
// Helper to perform a linear search across the removes.
// PERF: This linear search is still cheaper than combining all removes into hashsets and performing a lookup
// due to allocation and hashcode costs, provided that we can quickly filter out false matches.
static bool ShouldRemoveItem(ProjectItemInstance item, List<List<ProjectItemInstance>> allRemoves)
{
ITaskItem2 itemAsTaskItem = item;
string evaluatedInclude = itemAsTaskItem.EvaluatedIncludeEscaped;
foreach (List<ProjectItemInstance> removes in allRemoves)
{
foreach (ProjectItemInstance remove in removes)
{
// Get access to allocation-free item spec property.
ITaskItem2 removeAsTaskItem = remove;
// Start with the item spec length as a fast filter for false matches.
if (evaluatedInclude.Length == removeAsTaskItem.EvaluatedIncludeEscaped.Length
&& StringComparer.Ordinal.Equals(evaluatedInclude, removeAsTaskItem.EvaluatedIncludeEscaped)
&& itemAsTaskItem == removeAsTaskItem)
{
return true;
}
}
}
return false;
}
}
/// <summary>
/// Populates with an item group. This is done before the item lookup is used in this scope.
/// Assumes all the items in the group have the same, provided, type.
/// Assumes there is no item group of this type in the primary table already.
/// Should be used only by batching buckets.
/// </summary>
internal void PopulateWithItems(string itemType, ICollection<ProjectItemInstance> group)
{
// The outer scope should never have primary table populated.
MustNotBeOuterScope();
if (group.Count == 0)
{
return;
}
PrimaryTable ??= new ItemDictionarySlim();
ICollection<ProjectItemInstance> existing = PrimaryTable[itemType];
ErrorUtilities.VerifyThrow(existing == null, "Cannot add an itemgroup of this type.");
PrimaryTable.ImportItemsOfType(itemType, group);
}
/// <summary>
/// Populates with an item. This is done before the item lookup is used in this scope.
/// There may or may not already be a group for it.
/// Should be used only by batching buckets.
/// </summary>
internal void PopulateWithItem(ProjectItemInstance item)
{
// The outer scope should never have primary table populated.
MustNotBeOuterScope();
PrimaryTable ??= new ItemDictionarySlim();
PrimaryTable.Add(item);
}
/// <summary>
/// Sets the item types to truncate at the current scope.
/// </summary>
/// <remarks>
/// This can only be setup once per-scope, as the truncate set will be frozen for perf.
/// </remarks>
internal void TruncateLookupsForItemTypes(ICollection<string> itemTypes)
{
ErrorUtilities.VerifyThrow(_lookupScopes.ItemTypesToTruncateAtThisScope == null, "Cannot add an itemgroup of this type.");
// Add the item types to truncate at this scope
_lookupScopes.ItemTypesToTruncateAtThisScope =
itemTypes?.ToFrozenSet(MSBuildNameIgnoreCaseComparer.Default)
?? FrozenSet<string>.Empty;
}
/// <summary>
/// Apply a property to this scope.
/// </summary>
internal void SetProperty(ProjectPropertyInstance property)
{
// Setting in outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
// Put in the set table
PrimaryPropertySets ??= new PropertyDictionary<ProjectPropertyInstance>();
PrimaryPropertySets.Set(property);
}
/// <summary>
/// Implements a true add, an item that has been created in a batch.
/// </summary>
internal void AddNewItemsOfItemType(string itemType, ICollection<ProjectItemInstance> group, bool doNotAddDuplicates = false, Action<IList> logFunction = null)
{
// Adding to outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
foreach (ProjectItemInstance item in group)
{
MustNotBeInAnyTables(item);
}
#endif
if (group.Count == 0)
{
return;
}
// Put them in the add table
PrimaryAddTable ??= new ItemDictionarySlim();
IEnumerable<ProjectItemInstance> itemsToAdd = group;
if (doNotAddDuplicates)
{
// Ensure we don't also add any that already exist.
var existingItems = GetItems(itemType);
var existingItemsHashSet = existingItems.ToHashSet(ProjectItemInstance.EqualityComparer);
var deduplicatedItemsToAdd = new List<ProjectItemInstance>();
foreach (var item in itemsToAdd)
{
if (existingItemsHashSet.Add(item))
{
deduplicatedItemsToAdd.Add(item);
}
}
itemsToAdd = deduplicatedItemsToAdd;
}
if (logFunction != null)
{
if (doNotAddDuplicates)
{
// itemsToAdd is guaranteed to be a List if we're doing the doNotAddDuplicates part.
logFunction.Invoke(itemsToAdd as List<ProjectItemInstance>);
}
else
{
var groupAsList = group as List<ProjectItemInstance>;
logFunction.Invoke(groupAsList ?? group.ToList());
}
}
PrimaryAddTable.ImportItemsOfType(itemType, itemsToAdd);
}
/// <summary>
/// Implements a true add, an item that has been created in a batch.
/// </summary>
internal void AddNewItem(ProjectItemInstance item)
{
// Adding to outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
// This item must not be in any table already; a project cannot create an item
// that already exists
MustNotBeInAnyTables(item);
#endif
// Put in the add table
PrimaryAddTable ??= new ItemDictionarySlim();
PrimaryAddTable.Add(item);
}
/// <summary>
/// Remove a bunch of items from this scope
/// </summary>
internal void RemoveItems(string itemType, ICollection<ProjectItemInstance> items)
{
// Removing from outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
if (items.Count == 0)
{
return;
}
PrimaryRemoveTable ??= new ItemDictionarySlim();
PrimaryRemoveTable.EnsureCapacityForItemType(itemType, items.Count);
IEnumerable<ProjectItemInstance> itemsToRemove = items.Select(RetrieveOriginalFromCloneTable);
PrimaryRemoveTable.ImportItemsOfType(itemType, itemsToRemove);
// No need to remove these items from the primary add table if it's
// already there -- we always apply removes after adds, so that add
// will be reversed anyway.
}
/// <summary>
/// Modifies items in this scope with the same set of metadata modifications.
/// Assumes all the items in the group have the same, provided, type.
/// </summary>
internal void ModifyItems(string itemType, ICollection<ProjectItemInstance> group, MetadataModifications metadataChanges)
{
// Modifying in outer scope could be easily implemented, but our code does not do it at present
MustNotBeOuterScope();
#if DEBUG
// This item should not already be in any remove table; there is no way a project can
// modify items that were already removed
// Obviously, do this only in debug, as it's a slow check for bugs.
Scope scope = _lookupScopes;
while (scope != null)
{
foreach (ProjectItemInstance item in group)
{
ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item);
MustNotBeInTable(scope.Removes, actualItem);
}
scope = scope.Parent;
}
#endif
if (!metadataChanges.HasChanges)
{
return;
}
// Put in the modify table
// We don't need to check whether the item is in the add table vs. the main table; either
// way the modification will be applied.
PrimaryModifyTable ??= new ItemTypeToItemsMetadataUpdateDictionary(MSBuildNameIgnoreCaseComparer.Default);
Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType;
if (!PrimaryModifyTable.TryGetValue(itemType, out modifiesOfType))
{
modifiesOfType = new Dictionary<ProjectItemInstance, MetadataModifications>();
PrimaryModifyTable[itemType] = modifiesOfType;
}
foreach (ProjectItemInstance item in group)
{
// Each item needs its own collection for metadata changes, even if this particular change is the same
// for more than one item, subsequent changes might not be.
var metadataChangeCopy = metadataChanges.Clone();
// If we're asked to modify a clone we handed out, record it as a modify of the original
// instead
ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item);
var modify = new KeyValuePair<ProjectItemInstance, MetadataModifications>(actualItem, metadataChangeCopy);
MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins);
}
}
#endregion
#region Private Methods
/// <summary>
/// Apply modifies to a temporary result group.
/// Items to be modified are virtual-cloned so the original isn't changed.
/// </summary>
private void ApplyModifies(List<ProjectItemInstance> result, Dictionary<ProjectItemInstance, MetadataModifications> allModifies)
{
// Clone, because we're modifying actual items, and this would otherwise be visible to other batches,
// and would be "published" even if a target fails.
// FUTURE - don't need to clone here for non intrinsic tasks, but at present, they don't do modifies
// Store the clone, in case we're asked to modify or remove it later (we will record it against the real item)
_cloneTable ??= new Dictionary<ProjectItemInstance, ProjectItemInstance>();
// Iterate through the result group while replacing any items that have pending modifications.
for (int i = 0; i < result.Count; i++)
{
ProjectItemInstance originalItem = result[i];
if (allModifies.TryGetValue(originalItem, out MetadataModifications modificationsToApply))
{
// Modify the cloned item and replace the original with it.
ProjectItemInstance cloneItem = originalItem.DeepClone();
ApplyMetadataModificationsToItem(modificationsToApply, cloneItem);
result[i] = cloneItem;
// This will be null if the item wasn't in the result group, ie, it had been removed after being modified
ErrorUtilities.VerifyThrow(!_cloneTable.ContainsKey(cloneItem), "Should be new, not already in table!");
_cloneTable[cloneItem] = originalItem;
}
}
}
/// <summary>
/// Applies the specified modifications to the supplied item.
/// </summary>
private static void ApplyMetadataModificationsToItem(MetadataModifications modificationsToApply, ProjectItemInstance itemToModify)
{
// PERF: Avoid additional allocations by going through the interfaces - ProjectItemInstance hides some symbols
// from its public API.
ITaskItem taskItem = itemToModify;
IMetadataContainer metadataContainer = itemToModify;
// Remove any metadata from the item which is slated for removal. The indexer in the modifications table will
// return a modification with Remove == true either if there is an explicit entry for that name in the modifications
// or if keepOnlySpecified == true and there is no entry for that name.
if (modificationsToApply.KeepOnlySpecified && metadataContainer.HasCustomMetadata)
{
foreach (KeyValuePair<string, string> m in metadataContainer.BackingMetadata.Dictionary)
{
if (modificationsToApply[m.Key].Remove)
{
taskItem.RemoveMetadata(m.Key);
}
}
}
// Now make any additions or modifications
foreach (var modificationPair in modificationsToApply.ExplicitModifications)
{
if (modificationPair.Value.Remove)
{
taskItem.RemoveMetadata(modificationPair.Key);
}
else if (modificationPair.Value.NewValue != null)
{
taskItem.SetMetadata(modificationPair.Key, modificationPair.Value.NewValue);
}
}
}
/// <summary>
/// Look up the "real" item by using its clone, and return the real item.
/// See <see cref="_cloneTable"/> for explanation of the clone table.
/// </summary>
private ProjectItemInstance RetrieveOriginalFromCloneTable(ProjectItemInstance item)
{
ProjectItemInstance original;
if (_cloneTable != null)
{
if (_cloneTable.TryGetValue(item, out original))
{
item = original;
}
}
return item;
}
/// <summary>
/// Applies a list of modifications to the appropriate <see cref="ItemDictionary{ProjectItemInstance}" /> in a main table.
/// If any modifications conflict, these modifications win.
/// </summary>
private void ApplyModificationsToTable(IItemDictionary<ProjectItemInstance> table, string itemType, ItemsMetadataUpdateDictionary modify)
{
ICollection<ProjectItemInstance> existing = table[itemType];
if (existing != null)
{
foreach (var kvPair in modify)
{
if (table.Contains(kvPair.Key))
{
var itemToModify = kvPair.Key;
var modificationsToApply = kvPair.Value;
ApplyMetadataModificationsToItem(modificationsToApply, itemToModify);
}
}
}
}
/// <summary>
/// Applies a modification to an item in a table of modifications.
/// If the item already exists in the table, merges in the modifications; if there is a conflict
/// the mergeType indicates which should win.
/// </summary>
private void MergeModificationsIntoModificationTable(Dictionary<ProjectItemInstance, MetadataModifications> modifiesOfType,
KeyValuePair<ProjectItemInstance, MetadataModifications> modify,
ModifyMergeType mergeType)
{
MetadataModifications existingMetadataChanges;
if (modifiesOfType.TryGetValue(modify.Key, out existingMetadataChanges))
{
// There's already modifications for this item; merge with those
if (mergeType == ModifyMergeType.SecondWins)
{
// Merge the new modifications on top of the existing modifications.
existingMetadataChanges.ApplyModifications(modify.Value);
}
else
{
// Only apply explicit modifications.
foreach (var metadataChange in modify.Value.ExplicitModifications)
{
// If the existing metadata change list has an entry for this metadata, ignore this change.
// We continue to allow changes made when KeepOnlySpecified is set because it is assumed that explicit metadata changes
// always trump implicit ones.
if (!existingMetadataChanges.ContainsExplicitModification(metadataChange.Key))
{
existingMetadataChanges[metadataChange.Key] = metadataChange.Value;
}
}
}
}
else
{
modifiesOfType.Add(modify.Key, modify.Value);
}
}
#if DEBUG
/// <summary>
/// Verify item is not in the table
/// </summary>
private void MustNotBeInTable(ItemDictionarySlim table, ProjectItemInstance item)
{
if (table?.ContainsKey(item.ItemType) == true)
{
List<ProjectItemInstance> tableOfItemsOfSameType = table[item.ItemType];
if (tableOfItemsOfSameType != null)
{
ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.Contains(item), "Item should not be in table");
}
}
}
/// <summary>
/// Verify item is not in the modify table
/// </summary>
private void MustNotBeInTable(ItemTypeToItemsMetadataUpdateDictionary table, ProjectItemInstance item)
{
ItemsMetadataUpdateDictionary tableOfItemsOfSameType = null;
if (table?.TryGetValue(item.ItemType, out tableOfItemsOfSameType) == true)
{
if (tableOfItemsOfSameType is not null)
{
ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.ContainsKey(item), "Item should not be in table");
}
}
}
/// <summary>
/// Verify item is not in any table in any scope
/// </summary>
private void MustNotBeInAnyTables(ProjectItemInstance item)
{
// This item should not already be in any table; there is no way a project can
// create items that already existed
// Obviously, do this only in debug, as it's a slow check for bugs.
Scope scope = _lookupScopes;
while (scope != null)
{
MustNotBeInTable(scope.Adds, item);
MustNotBeInTable(scope.Removes, item);
MustNotBeInTable(scope.Modifies, item);
scope = scope.Parent;
}
}
#endif
/// <summary>
/// Add/remove/modify/set directly on an outer scope would need to be handled separately - it would apply
/// directly to the main tables. Our code isn't expected to do this.
/// </summary>
private void MustNotBeOuterScope()
{
ErrorUtilities.VerifyThrow(_lookupScopes.Parent != null, "Operation in outer scope not supported");
}
#endregion
/// <summary>
/// When merging metadata, we can deal with a conflict two different ways:
/// FirstWins = any previous metadata with the name takes precedence
/// SecondWins = the new metadata with the name takes precedence
/// </summary>
private enum ModifyMergeType
{
FirstWins = 1,
SecondWins = 2
}
/// <summary>
/// A class representing a set of additions, modifications or removal of metadata from items.
/// </summary>
internal class MetadataModifications
{
/// <summary>
/// Flag indicating if the modifications should be interpreted such that the lack of an explicit entry for a metadata name
/// means that that metadata should be removed.
/// </summary>
private bool _keepOnlySpecified;
/// <summary>
/// A set of explicitly-specified modifications.
/// </summary>
private Dictionary<string, MetadataModification> _modifications;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="keepOnlySpecified">When true, metadata which is not explicitly-specified here but which is present on the target
/// item should be removed. When false, only explicitly-specified modifications apply.</param>
public MetadataModifications(bool keepOnlySpecified)
{
_keepOnlySpecified = keepOnlySpecified;
_modifications = new Dictionary<string, MetadataModification>(MSBuildNameIgnoreCaseComparer.Default);
}
/// <summary>
/// Cloning constructor.
/// </summary>
/// <param name="other">The metadata modifications to clone.</param>
private MetadataModifications(MetadataModifications other)
{
_keepOnlySpecified = other._keepOnlySpecified;
_modifications = new Dictionary<string, MetadataModification>(other._modifications, MSBuildNameIgnoreCaseComparer.Default);
}
/// <summary>
/// Clones this modification set.
/// </summary>
/// <returns>A copy of the modifications.</returns>
public MetadataModifications Clone()
{
return new MetadataModifications(this);
}
/// <summary>
/// A flag indicating whether or not there are any changes which might apply.
/// </summary>
public bool HasChanges
{
get { return _modifications.Count > 0 || _keepOnlySpecified; }
}
/// <summary>
/// A flag indicating whether only those metadata explicitly-specified should be retained on a target item.
/// </summary>
public bool KeepOnlySpecified
{
get { return _keepOnlySpecified; }
}
/// <summary>
/// Applies the modifications from the specified modifications to this one, performing a proper merge.
/// </summary>
/// <param name="other">The set of metadata modifications to merge into this one.</param>
public void ApplyModifications(MetadataModifications other)
{
// Apply implicit modifications
if (other._keepOnlySpecified)
{
// Any metadata not specified in other must be removed from this one.
var metadataToRemove = new List<string>(_modifications.Keys.Where(m => other[m].Remove));
foreach (var metadata in metadataToRemove)
{
_modifications.Remove(metadata);
}
}
_keepOnlySpecified |= other._keepOnlySpecified;
// Now apply the explicit modifications from the other table
foreach (var modificationPair in other.ExplicitModifications)
{
MetadataModification existingModification;
if (modificationPair.Value.KeepValue && _modifications.TryGetValue(modificationPair.Key, out existingModification))
{
// The incoming modification requests we maintain the "current value" of the metadata. If we have
// an existing change, maintain that as-is. Otherwise, fall through and apply our change directly.
if (existingModification.Remove || existingModification.NewValue != null)
{
continue;
}
}
// Just copy over the changes from the other table to this one.
_modifications[modificationPair.Key] = modificationPair.Value;
}
}
/// <summary>
/// Returns true if this block contains an explicitly-specified modification for the provided metadata name.
/// </summary>
/// <param name="metadataName">The name of the metadata.</param>
/// <returns>True if there is an explicit modification for this metadata, false otherwise.</returns>
/// <remarks>The return value of this method is unaffected by the <see cref="KeepOnlySpecified"/> property.</remarks>
public bool ContainsExplicitModification(string metadataName)
{
return _modifications.ContainsKey(metadataName);
}
/// <summary>
/// Adds metadata to the modification table.
/// </summary>
/// <param name="metadataName">The name of the metadata to add (or change) in the target item.</param>
/// <param name="metadataValue">The metadata value.</param>
public void Add(string metadataName, string metadataValue)
{
_modifications.Add(metadataName, MetadataModification.CreateFromNewValue(metadataValue));
}
/// <summary>
/// Provides an enumeration of the explicit metadata modifications.
/// </summary>
public Dictionary<string, MetadataModification> ExplicitModifications
{
get { return _modifications; }
}
/// <summary>
/// Sets or retrieves a modification from the modifications table.
/// </summary>
/// <param name="metadataName">The metadata name.</param>
/// <returns>If <see cref="KeepOnlySpecified"/> is true, this will return a modification with <see cref="MetadataModification.Remove"/>
/// set to true if the metadata has no other explicitly-specified modification. Otherwise it will return only the explicitly-specified
/// modification if one exists.</returns>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">When <see cref="KeepOnlySpecified"/> if false, this is thrown if the metadata
/// specified does not exist when attempting to retrieve a metadata modification.</exception>
public MetadataModification this[string metadataName]
{
get
{
MetadataModification modification;
if (!_modifications.TryGetValue(metadataName, out modification))
{
if (_keepOnlySpecified)
{
// This metadata was not specified and we are only keeping specified metadata, so remove it.
return MetadataModification.CreateFromRemove();
}
return MetadataModification.CreateFromNoChange();
}
return modification;
}
set
{
ErrorUtilities.VerifyThrowInternalNull(value, "value");
_modifications[metadataName] = value;
}
}
}
/// <summary>
/// A type of metadata modification.
/// </summary>
internal enum ModificationType
{
/// <summary>
/// Indicates the metadata value should be kept unchanged.
/// </summary>
Keep,
/// <summary>
/// Indicates the metadata value should be changed.
/// </summary>
Update,
/// <summary>
/// Indicates the metadata value should be removed.
/// </summary>
Remove
}
/// <summary>
/// Represents a modification for a single metadata.
/// </summary>
internal class MetadataModification
{
/// <summary>
/// When true, indicates the metadata should be removed from the target item.
/// </summary>
private readonly bool _remove;
/// <summary>
/// The value to which the metadata should be set. If null, the metadata value should be retained unmodified.
/// </summary>
private readonly string _newValue;
/// <summary>
/// A modification which indicates the metadata value should be retained without modification.
/// </summary>
private static readonly MetadataModification s_keepModification = new MetadataModification(ModificationType.Keep);
/// <summary>
/// A modification which indicates the metadata should be removed.
/// </summary>
private static readonly MetadataModification s_removeModification = new MetadataModification(ModificationType.Remove);
/// <summary>
/// Constructor for metadata modifications of type Keep or Remove.
/// </summary>
/// <param name="modificationType">The type of modification to make.</param>
private MetadataModification(ModificationType modificationType)
{
ErrorUtilities.VerifyThrow(modificationType != ModificationType.Update, "Modification type may only be update when a value is specified.");
_remove = modificationType == ModificationType.Remove;
_newValue = null;
}
/// <summary>
/// Constructor for metadata modifications of type Update.
/// </summary>
/// <param name="value">The new value for the metadata.</param>
private MetadataModification(string value)
{
_remove = false;
_newValue = value;
}
/// <summary>
/// Creates a metadata modification of type Keep.
/// </summary>
/// <returns>The metadata modification.</returns>
public static MetadataModification CreateFromNoChange()
{
return s_keepModification;
}
/// <summary>
/// Creates a metadata modification of type Update with the specified metadata value.
/// </summary>
/// <param name="newValue">The new metadata value.</param>
/// <returns>The metadata modification.</returns>
public static MetadataModification CreateFromNewValue(string newValue)
{
return new MetadataModification(newValue);
}
/// <summary>
/// Creates a metadata modification of type Remove.
/// </summary>
/// <returns>The metadata modification.</returns>
public static MetadataModification CreateFromRemove()
{
return s_removeModification;
}
/// <summary>
/// When true, this modification indicates the associated metadata should be removed.
/// </summary>
public bool Remove
{
get { return _remove; }
}
/// <summary>
/// When true, this modification indicates the associated metadata should retain its existing value.
/// </summary>
public bool KeepValue
{
get { return !_remove && _newValue == null; }
}
/// <summary>
/// The new value of the metadata. Only valid when <see cref="Remove"/> is false.
/// </summary>
public string NewValue
{
get { return _newValue; }
}
}
/// <summary>
/// Represents an entry in the lookup list.
/// Class rather than a struct so that it can be modified in the list.
/// </summary>
internal class Scope
{
/// <summary>
/// Contains all of the original items at this level in the Lookup
/// </summary>
private ItemDictionarySlim _items;
/// <summary>
/// Contains all of the items which have been added at this level in the Lookup
/// </summary>
private ItemDictionarySlim _adds;
/// <summary>
/// Contails all of the items which have been removed at this level in the Lookup
/// </summary>
private ItemDictionarySlim _removes;
/// <summary>
/// Contains all of the metadata which has been changed for items at this level in the Lookup.
/// Schema: { K=type, V= { K=item, V=table of { K=metadata name, V=metadata value }}}
/// </summary>
private ItemTypeToItemsMetadataUpdateDictionary _modifies;
/// <summary>
/// Contains all of the original properties at this level in the Lookup
/// </summary>
private PropertyDictionary<ProjectPropertyInstance> _properties;
/// <summary>
/// Contains all of the properties which have been set at this level or above in the Lookup
/// </summary>
private PropertyDictionary<ProjectPropertyInstance> _propertySets;
/// <summary>
/// A description of this scope, for error checking
/// </summary>
private string _description;
/// <summary>
/// The lookup which owns this scope, for error checking.
/// </summary>
private Lookup _owningLookup;
/// <summary>
/// Indicates whether or not further levels in the Lookup should be consulted beyond this one
/// to find the actual value for the desired item type.
/// </summary>
private FrozenSet<string> _itemTypesToTruncateAtThisScope;
internal Scope(Lookup lookup, string description, PropertyDictionary<ProjectPropertyInstance> properties)
{
_owningLookup = lookup;
_description = description;
_items = null;
_adds = null;
_removes = null;
_modifies = null;
_properties = properties;
_propertySets = null;
_itemTypesToTruncateAtThisScope = null;
Parent = lookup._lookupScopes;
}
/// <summary>
/// The parent scope in the stack, if any.
/// </summary>
internal Scope Parent { get; }
/// <summary>
/// The total number of scopes in the chain.
/// </summary>
internal int Count
{
get
{
int count = 1;
Scope scope = Parent;
while (scope != null)
{
count++;
scope = scope.Parent;
}
return count;
}
}
/// <summary>
/// The main table, populated with items that
/// are initially visible in this scope. Does not
/// include adds or removes unless it's the table in
/// the outermost scope.
/// </summary>
internal ItemDictionarySlim Items
{
get { return _items; }
set { _items = value; }
}
/// <summary>
/// Adds made in this scope or above.
/// </summary>
internal ItemDictionarySlim Adds
{
get { return _adds; }
set { _adds = value; }
}
/// <summary>
/// Removes made in this scope or above.
/// </summary>
internal ItemDictionarySlim Removes
{
get { return _removes; }
set { _removes = value; }
}
/// <summary>
/// Modifications made in this scope or above.
/// </summary>
internal ItemTypeToItemsMetadataUpdateDictionary Modifies
{
get { return _modifies; }
set { _modifies = value; }
}
/// <summary>
/// The main property table, populated with properties
/// that are initially visible in this scope. Does not
/// include sets unless it's the table in the outermost scope.
/// </summary>
internal PropertyDictionary<ProjectPropertyInstance> Properties
{
get { return _properties; }
set { _properties = value; }
}
/// <summary>
/// Properties set in this scope or above.
/// </summary>
internal PropertyDictionary<ProjectPropertyInstance> PropertySets
{
get { return _propertySets; }
set { _propertySets = value; }
}
/// <summary>
/// Whether to stop lookups going beyond this scope downwards for item types in the set.
/// </summary>
internal FrozenSet<string> ItemTypesToTruncateAtThisScope
{
get { return _itemTypesToTruncateAtThisScope; }
set { _itemTypesToTruncateAtThisScope = value; }
}
/// <summary>
/// The description assigned to this scope.
/// </summary>
internal string Description
{
get { return _description; }
}
/// <summary>
/// Leaves the current lookup scope.
/// </summary>
internal void LeaveScope()
{
_owningLookup.LeaveScope(this);
}
}
}
}
|