File: Evaluation\LazyItemEvaluator.UpdateOperation.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    internal partial class LazyItemEvaluator<P, I, M, D>
    {
        private class UpdateOperation : LazyItemOperation
        {
            private readonly ImmutableArray<ProjectMetadataElement> _metadata;
            private ImmutableList<ItemBatchingContext>.Builder _itemsToUpdate = null;
            private ItemSpecMatchesItem _matchItemSpec = null;
            private bool? _needToExpandMetadataForEachItem = null;
 
            public UpdateOperation(OperationBuilderWithMetadata builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator)
                : base(builder, lazyEvaluator)
            {
                _metadata = builder.Metadata.ToImmutable();
            }
 
            private readonly struct MatchResult
            {
                public bool IsMatch { get; }
                public Dictionary<string, I> CapturedItemsFromReferencedItemTypes { get; }
 
                public MatchResult(bool isMatch, Dictionary<string, I> capturedItemsFromReferencedItemTypes)
                {
                    IsMatch = isMatch;
                    CapturedItemsFromReferencedItemTypes = capturedItemsFromReferencedItemTypes;
                }
            }
 
            private delegate MatchResult ItemSpecMatchesItem(ItemSpec<P, I> itemSpec, I itemToMatch);
 
            protected override void ApplyImpl(OrderedItemDataCollection.Builder listBuilder, ImmutableHashSet<string> globsToIgnore)
            {
                if (!_conditionResult)
                {
                    return;
                }
 
                SetMatchItemSpec();
                _itemsToUpdate ??= ImmutableList.CreateBuilder<ItemBatchingContext>();
                _itemsToUpdate.Clear();
 
                for (int i = 0; i < listBuilder.Count; i++)
                {
                    var itemData = listBuilder[i];
 
                    var matchResult = _matchItemSpec(_itemSpec, itemData.Item);
 
                    if (matchResult.IsMatch)
                    {
                        listBuilder[i] = UpdateItem(listBuilder[i], matchResult.CapturedItemsFromReferencedItemTypes);
                    }
                }
 
                DecorateItemsWithMetadata(_itemsToUpdate.ToImmutableList(), _metadata, _needToExpandMetadataForEachItem);
            }
 
            /// <summary>
            /// Apply the Update operation to the item if it matches.
            /// </summary>
            /// <param name="item">The item to check for a match.</param>
            /// <returns>The updated item.</returns>
            internal ItemData UpdateItem(ItemData item)
            {
                if (_conditionResult)
                {
                    SetMatchItemSpec();
                    _itemsToUpdate ??= ImmutableList.CreateBuilder<ItemBatchingContext>();
                    _itemsToUpdate.Clear();
                    MatchResult matchResult = _matchItemSpec(_itemSpec, item.Item);
                    if (matchResult.IsMatch)
                    {
                        ItemData clonedData = UpdateItem(item, matchResult.CapturedItemsFromReferencedItemTypes);
                        DecorateItemsWithMetadata(_itemsToUpdate.ToImmutableList(), _metadata, _needToExpandMetadataForEachItem);
                        return clonedData;
                    }
                }
                return item;
            }
 
            private ItemData UpdateItem(ItemData item, Dictionary<string, I> capturedItemsFromReferencedItemTypes)
            {
                // items should be deep immutable, so clone and replace items before mutating them
                // otherwise, with GetItems caching enabled, the mutations would leak into the cache causing
                // future operations to mutate the state of past operations
                ItemData clonedData = item.Clone(_itemFactory, _itemElement);
                _itemsToUpdate.Add(new ItemBatchingContext(clonedData.Item, capturedItemsFromReferencedItemTypes));
                return clonedData;
            }
 
            /// <summary>
            /// This sets the function used to determine whether an item matches an item spec.
            /// </summary>
            private void SetMatchItemSpec()
            {
                if (ItemspecContainsASingleBareItemReference(_itemSpec, _itemElement.ItemType))
                {
                    // Perf optimization: If the Update operation references itself (e.g. <I Update="@(I)"/>)
                    // then all items are updated and matching is not necessary
                    _matchItemSpec = (itemSpec, item) => new MatchResult(true, null);
                }
                else if (ItemSpecContainsItemReferences(_itemSpec)
                         && QualifiedMetadataReferencesExist(_metadata, out _needToExpandMetadataForEachItem)
                         && !Traits.Instance.EscapeHatches.DoNotExpandQualifiedMetadataInUpdateOperation)
                {
                    var itemReferenceFragments = _itemSpec.Fragments.OfType<ItemSpec<P, I>.ItemExpressionFragment>().ToArray();
                    var nonItemReferenceFragments = _itemSpec.Fragments.Where(f => !(f is ItemSpec<P, I>.ItemExpressionFragment)).ToArray();
 
                    _matchItemSpec = (itemSpec, item) =>
                    {
                        var isMatch = nonItemReferenceFragments.Any(f => f.IsMatch(item.EvaluatedInclude));
                        Dictionary<string, I> capturedItemsFromReferencedItemTypes = null;
 
                        foreach (var itemReferenceFragment in itemReferenceFragments)
                        {
                            foreach (var referencedItem in itemReferenceFragment.ReferencedItems)
                            {
                                if (referencedItem.ItemAsValueFragment.IsMatch(item.EvaluatedInclude))
                                {
                                    isMatch = true;
 
                                    capturedItemsFromReferencedItemTypes ??= new Dictionary<string, I>(StringComparer.OrdinalIgnoreCase);
 
                                    capturedItemsFromReferencedItemTypes[referencedItem.Item.Key] = referencedItem.Item;
                                }
                            }
                        }
 
                        return new MatchResult(isMatch, capturedItemsFromReferencedItemTypes);
                    };
                }
                else
                {
                    _matchItemSpec = (itemSpec, item) => new MatchResult(itemSpec.MatchesItem(item), null);
                }
            }
 
            private bool QualifiedMetadataReferencesExist(ImmutableArray<ProjectMetadataElement> metadata, out bool? needToExpandMetadataForEachItem)
            {
                needToExpandMetadataForEachItem = NeedToExpandMetadataForEachItem(metadata, out var itemsAndMetadataFound);
 
                if (itemsAndMetadataFound.Metadata == null)
                {
                    return false;
                }
 
                foreach (var metadataReference in itemsAndMetadataFound.Metadata)
                {
                    if (!string.IsNullOrWhiteSpace(metadataReference.Value.ItemName))
                    {
                        return true;
                    }
                }
 
                return false;
            }
 
            private static bool ItemSpecContainsItemReferences(ItemSpec<P, I> itemSpec)
            {
                return itemSpec.Fragments.Any(f => f is ItemSpec<P, I>.ItemExpressionFragment);
            }
        }
    }
}