File: Evaluation\LazyItemEvaluator.LazyItemOperation.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.Diagnostics;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Eventing;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    internal partial class LazyItemEvaluator<P, I, M, D>
    {
        private abstract class LazyItemOperation : IItemOperation
        {
            private readonly string _itemType;
            private readonly ImmutableDictionary<string, LazyItemList> _referencedItemLists;
 
            protected readonly LazyItemEvaluator<P, I, M, D> _lazyEvaluator;
            protected readonly ProjectItemElement _itemElement;
            protected readonly ItemSpec<P, I> _itemSpec;
            protected readonly EvaluatorData _evaluatorData;
            protected readonly Expander<P, I> _expander;
            protected readonly bool _conditionResult;
 
            // This is used only when evaluating an expression, which instantiates
            //  the items and then removes them
            protected readonly IItemFactory<I, I> _itemFactory;
            internal ItemSpec<P, I> Spec => _itemSpec;
 
            protected LazyItemOperation(OperationBuilder builder, LazyItemEvaluator<P, I, M, D> lazyEvaluator)
            {
                _itemElement = builder.ItemElement;
                _itemType = builder.ItemType;
                _itemSpec = builder.ItemSpec;
                _referencedItemLists = builder.ReferencedItemLists.ToImmutable();
                _conditionResult = builder.ConditionResult;
 
                _lazyEvaluator = lazyEvaluator;
 
                _evaluatorData = new EvaluatorData(_lazyEvaluator._outerEvaluatorData, _referencedItemLists);
                _itemFactory = new ItemFactoryWrapper(_itemElement, _lazyEvaluator._itemFactory);
                _expander = new Expander<P, I>(_evaluatorData, _evaluatorData, _lazyEvaluator.EvaluationContext, _lazyEvaluator._loggingContext);
 
                _itemSpec.Expander = _expander;
            }
 
            protected FileMatcher FileMatcher => _lazyEvaluator.FileMatcher;
 
            public void Apply(OrderedItemDataCollection.Builder listBuilder, ImmutableHashSet<string> globsToIgnore)
            {
                MSBuildEventSource.Log.ApplyLazyItemOperationsStart(_itemElement.ItemType);
                using (_lazyEvaluator._evaluationProfiler.TrackElement(_itemElement))
                {
                    ApplyImpl(listBuilder, globsToIgnore);
                }
                MSBuildEventSource.Log.ApplyLazyItemOperationsStop(_itemElement.ItemType);
            }
 
            protected virtual void ApplyImpl(OrderedItemDataCollection.Builder listBuilder, ImmutableHashSet<string> globsToIgnore)
            {
                var items = SelectItems(listBuilder, globsToIgnore);
                MutateItems(items);
                SaveItems(items, listBuilder);
            }
 
            /// <summary>
            /// Produce the items to operate on. For example, create new ones or select existing ones
            /// </summary>
            protected virtual ImmutableArray<I> SelectItems(OrderedItemDataCollection.Builder listBuilder, ImmutableHashSet<string> globsToIgnore)
            {
                return listBuilder.Select(itemData => itemData.Item)
                                  .ToImmutableArray();
            }
 
            // todo Refactoring: MutateItems should clone each item before mutation. See https://github.com/dotnet/msbuild/issues/2328
            protected virtual void MutateItems(ImmutableArray<I> items) { }
 
            protected virtual void SaveItems(ImmutableArray<I> items, OrderedItemDataCollection.Builder listBuilder) { }
 
            [DebuggerDisplay(@"{DebugString()}")]
            protected readonly struct ItemBatchingContext
            {
                public I OperationItem { get; }
                private Dictionary<string, I> CapturedItems { get; }
 
                public ItemBatchingContext(I operationItem, Dictionary<string, I> capturedItems = null)
                {
                    OperationItem = operationItem;
 
                    CapturedItems = capturedItems == null || capturedItems.Count == 0
                        ? null
                        : capturedItems;
                }
 
                public IMetadataTable GetMetadataTable()
                {
                    return CapturedItems == null
                        ? (IMetadataTable)OperationItem
                        : new ItemOperationMetadataTable(OperationItem, CapturedItems);
                }
 
                private string DebugString()
                {
                    var referencedItemsString = CapturedItems == null
                        ? "none"
                        : string.Join(";", CapturedItems.Select(kvp => $"{kvp.Key} : {kvp.Value.EvaluatedInclude}"));
 
                    return $"{OperationItem.Key} : {OperationItem.EvaluatedInclude}; CapturedItems: {referencedItemsString}";
                }
            }
 
            private class ItemOperationMetadataTable : IMetadataTable
            {
                private readonly I _operationItem;
                private readonly Dictionary<string, I> _capturedItems;
 
                public ItemOperationMetadataTable(I operationItem, Dictionary<string, I> capturedItems)
                {
                    ErrorUtilities.VerifyThrow(
                        capturedItems.Comparer == StringComparer.OrdinalIgnoreCase,
                        "MSBuild assumes case insensitive item name comparison");
 
                    _operationItem = operationItem;
                    _capturedItems = capturedItems;
                }
 
                public string GetEscapedValue(string name)
                {
                    return _operationItem.GetEscapedValue(name);
                }
 
                public string GetEscapedValue(string itemType, string name)
                {
                    return RouteCall(itemType, name, (t, it, n) => t.GetEscapedValue(it, n));
                }
 
                public string GetEscapedValueIfPresent(string itemType, string name)
                {
                    return RouteCall(itemType, name, (t, it, n) => t.GetEscapedValueIfPresent(it, n));
                }
 
                private string RouteCall(string itemType, string name, Func<IMetadataTable, string, string, string> getEscapedValueFunc)
                {
                    if (itemType?.Equals(_operationItem.Key, StringComparison.OrdinalIgnoreCase) != false)
                    {
                        return getEscapedValueFunc(_operationItem, itemType, name);
                    }
                    else if (_capturedItems.TryGetValue(itemType, out var item))
                    {
                        return getEscapedValueFunc(item, itemType, name);
                    }
                    else
                    {
                        return string.Empty;
                    }
                }
            }
 
            protected void DecorateItemsWithMetadata(IEnumerable<ItemBatchingContext> itemBatchingContexts, ImmutableArray<ProjectMetadataElement> metadata, bool? needToExpandMetadata = null)
            {
                if (metadata.Length > 0)
                {
                    ////////////////////////////////////////////////////
                    // UNDONE: Implement batching here.
                    //
                    // We want to allow built-in metadata in metadata values here.
                    // For example, so that an Idl file can specify that its Tlb output should be named %(Filename).tlb.
                    //
                    // In other words, we want batching. However, we won't need to go to the trouble of using the regular batching code!
                    // That's because that code is all about grouping into buckets of similar items. In this context, we're not
                    // invoking a task, and it's fine to process each item individually, which will always give the correct results.
                    //
                    // For the CTP, to make the minimal change, we will not do this quite correctly.
                    //
                    // We will do this:
                    // -- check whether any metadata values or their conditions contain any bare built-in metadata expressions,
                    //    or whether they contain any custom metadata && the Include involved an @(itemlist) expression.
                    // -- if either case is found, we go ahead and evaluate all the metadata separately for each item.
                    // -- otherwise we can do the old thing (evaluating all metadata once then applying to all items)
                    //
                    // This algorithm gives the correct results except when:
                    // -- batchable expressions exist on the include, exclude, or condition on the item element itself
                    //
                    // It means that 99% of cases still go through the old code, which is best for the CTP.
                    // When we ultimately implement this correctly, we should make sure we optimize for the case of very many items
                    // and little metadata, none of which varies between items.
 
                    // Do not expand properties as they have been already expanded by the lazy evaluator upon item operation construction.
                    // Prior to lazy evaluation ExpanderOptions.ExpandAll was used.
                    const ExpanderOptions metadataExpansionOptions = ExpanderOptions.ExpandAll;
 
                    needToExpandMetadata ??= NeedToExpandMetadataForEachItem(metadata, out _);
 
                    if (needToExpandMetadata.Value)
                    {
                        foreach (var itemContext in itemBatchingContexts)
                        {
                            _expander.Metadata = itemContext.GetMetadataTable();
 
                            foreach (var metadataElement in metadata)
                            {
                                if (!EvaluateCondition(metadataElement.Condition, metadataElement, metadataExpansionOptions, ParserOptions.AllowAll, _expander, _lazyEvaluator))
                                {
                                    continue;
                                }
 
                                string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadataElement.Value, metadataExpansionOptions, metadataElement.Location);
 
                                itemContext.OperationItem.SetMetadata(metadataElement, FileUtilities.MaybeAdjustFilePath(evaluatedValue, metadataElement.ContainingProject.DirectoryPath));
                            }
                        }
 
                        // End of legal area for metadata expressions.
                        _expander.Metadata = null;
                    }
                    // End of pseudo batching
                    ////////////////////////////////////////////////////
                    // Start of old code
                    else
                    {
                        // Metadata expressions are allowed here.
                        // Temporarily gather and expand these in a table so they can reference other metadata elements above.
                        EvaluatorMetadataTable metadataTable = new EvaluatorMetadataTable(_itemType, capacity: metadata.Length);
                        _expander.Metadata = metadataTable;
 
                        // Also keep a list of everything so we can get the predecessor objects correct.
                        List<KeyValuePair<ProjectMetadataElement, string>> metadataList = new(metadata.Length);
 
                        foreach (var metadataElement in metadata)
                        {
                            // Because of the checking above, it should be safe to expand metadata in conditions; the condition
                            // will be true for either all the items or none
                            if (
                                !EvaluateCondition(
                                    metadataElement.Condition,
                                    metadataElement,
                                    metadataExpansionOptions,
                                    ParserOptions.AllowAll,
                                    _expander,
                                    _lazyEvaluator))
                            {
                                continue;
                            }
 
                            string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadataElement.Value, metadataExpansionOptions, metadataElement.Location, _lazyEvaluator._loggingContext);
                            evaluatedValue = FileUtilities.MaybeAdjustFilePath(evaluatedValue, metadataElement.ContainingProject.DirectoryPath);
 
                            metadataTable.SetValue(metadataElement, evaluatedValue);
                            metadataList.Add(new KeyValuePair<ProjectMetadataElement, string>(metadataElement, evaluatedValue));
                        }
 
                        // Apply those metadata to each item
                        // Note that several items could share the same metadata objects
 
                        // Set all the items at once to make a potential copy-on-write optimization possible.
                        // This is valuable in the case where one item element evaluates to
                        // many items (either by semicolon or wildcards)
                        // and that item also has the same piece/s of metadata for each item.
                        _itemFactory.SetMetadata(metadataList, itemBatchingContexts.Select(i => i.OperationItem));
 
                        // End of legal area for metadata expressions.
                        _expander.Metadata = null;
                    }
                }
            }
 
            private static IEnumerable<string> GetMetadataValuesAndConditions(ImmutableArray<ProjectMetadataElement> metadata)
            {
                foreach (var metadataElement in metadata)
                {
                    yield return metadataElement.Value;
                    yield return metadataElement.Condition;
                }
            }
 
            protected bool NeedToExpandMetadataForEachItem(ImmutableArray<ProjectMetadataElement> metadata, out ItemsAndMetadataPair itemsAndMetadataFound)
            {
                itemsAndMetadataFound = ExpressionShredder.GetReferencedItemNamesAndMetadata(GetMetadataValuesAndConditions(metadata));
 
                bool needToExpandMetadataForEachItem = false;
 
                if (itemsAndMetadataFound.Metadata?.Values.Count > 0)
                {
                    // If there is bare metadata of any kind, and the Include involved an item list, we should
                    // run items individually, as even non-built-in metadata might differ between items
 
                    if (_referencedItemLists.Count >= 0)
                    {
                        needToExpandMetadataForEachItem = true;
                    }
                    else
                    {
                        // If there is bare built-in metadata, we must always run items individually, as that almost
                        // always differs between items.
 
                        // UNDONE: When batching is implemented for real, we need to make sure that
                        // item definition metadata is included in all metadata operations during evaluation
                        if (itemsAndMetadataFound.Metadata.Values.Count > 0)
                        {
                            needToExpandMetadataForEachItem = true;
                        }
                    }
                }
 
                return needToExpandMetadataForEachItem;
            }
 
            /// <summary>
            /// Is this spec a single reference to a specific item?
            /// </summary>
            /// <returns>True if the item is a simple reference to the referenced item type.</returns>
            protected static bool ItemspecContainsASingleBareItemReference(ItemSpec<P, I> itemSpec, string referencedItemType)
            {
                if (itemSpec.Fragments.Count != 1)
                {
                    return false;
                }
 
                var itemExpressionFragment = itemSpec.Fragments[0] as ItemSpec<P, I>.ItemExpressionFragment;
                if (itemExpressionFragment == null)
                {
                    return false;
                }
 
                if (!itemExpressionFragment.Capture.ItemType.Equals(referencedItemType, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
 
                // If the itemSpec is a single call to an item function, like @(X->Something(...)), it may get this
                // far, but shouldn't be treated as a single reference: the item function may return entirely
                // different results from a bare reference like @(X).
                if (itemExpressionFragment.Capture.Captures is object)
                {
                    return false;
                }
 
                return true;
            }
        }
    }
}