File: Evaluation\GlobResultBuilder.cs
Web Access
Project: src\msbuild\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;
#if NET
using System.Buffers;
#endif
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Globbing;

namespace Microsoft.Build.Evaluation;

/// <summary>
/// Derives <see cref="GlobResult"/> information (the include globs of an item element together with the
/// excludes present on it and all the removes that apply to it) from a set of
/// <see cref="ProjectItemElement"/>s.
/// </summary>
/// <remarks>
/// This logic is shared between <see cref="Project.GetAllGlobs()"/> (which runs after evaluation over the
/// public object model) and the evaluator's synthesis of <c>MSBuildItemGlob</c> items (which runs during
/// evaluation). Sharing a single implementation guarantees the two produce identical results, so the globs
/// observed by a target or task during a command-line build match those returned by <c>GetAllGlobs</c>.
/// </remarks>
internal static class GlobResultBuilder
{
    /// <summary>
    /// * and ? are invalid file name characters, but they occur in globs as wild cards.
    /// </summary>
#if NET
    private static readonly SearchValues<char> s_invalidGlobChars = SearchValues.Create(
#else
    private static readonly char[] s_invalidGlobChars = (
#endif
        FileUtilities.InvalidFileNameCharsArray.Where(c => c is not ('*' or '?' or '/' or '\\' or ':')).ToArray());

    /// <summary>
    /// Builds a <see cref="GlobResult"/> for each include element in <paramref name="projectItemElements"/>
    /// that contributes globs.
    /// </summary>
    /// <remarks>
    /// The elements are scanned in reverse document order so that each include element is attributed exactly
    /// the removes that appear after it (the removes that can affect the items it produced). Consequently the
    /// returned list is in reverse document order (the last include element first). Because every
    /// <see cref="GlobResult"/> already folds in its own excludes and applicable removes, membership can be
    /// decided by testing a file against the union of the results regardless of their order.
    /// </remarks>
    public static List<GlobResult> BuildGlobResults<P, I>(IReadOnlyList<ProjectItemElement> projectItemElements, Expander<P, I> expander)
        where P : class, IProperty
        where I : class, IItem, IMetadataTable
    {
        if (projectItemElements.Count == 0)
        {
            return new List<GlobResult>();
        }

        // Scan the project elements in reverse order and build globbing information for each include element.
        // Based on the fact that relevant removes for a particular include element (xml element A) consist of:
        // - all the removes seen by the next include statement of A's type (xml element B which appears after A in file order)
        // - new removes between A and B (removes that apply to A but not to B. Spatially, these are placed between A's element and B's element)

        // Example:
        // 1. <I Include="A"/>
        // 2. <I Remove="..."/> // this remove applies to the include at 1
        // 3. <I Include="B"/>
        // 4. <I Remove="..."/> // this remove applies to the includes at 1, 3
        // 5. <I Include="C"/>
        // 6. <I Remove="..."/> // this remove applies to the includes at 1, 3, 5
        // So A's applicable removes are composed of:
        //
        // The applicable removes for the element at position 1 (xml element A) are composed of:
        // - all the removes seen by the next include statement of I's type (xml element B, position 3, which appears after A in file order). In this example that's Removes at positions 4 and 6.
        // - new removes between A and B. In this example that's Remove 2.

        // use immutable builders because there will be a lot of structural sharing between includes which share increasing subsets of corresponding remove elements
        // item type -> aggregated information about all removes seen so far for that item type
        var removeElementCache = new Dictionary<string, CumulativeRemoveElementData>(projectItemElements.Count);
        var globResults = new List<GlobResult>(projectItemElements.Count);

        for (var i = projectItemElements.Count - 1; i >= 0; i--)
        {
            var itemElement = projectItemElements[i];

            if (!string.IsNullOrEmpty(itemElement.Include))
            {
                var globResult = BuildGlobResultFromIncludeItem(itemElement, removeElementCache, expander);

                if (globResult != null)
                {
                    globResults.Add(globResult);
                }
            }
            else if (!string.IsNullOrEmpty(itemElement.Remove))
            {
                CacheInformationFromRemoveItem(itemElement, removeElementCache, expander);
            }
        }

        globResults.TrimExcess();

        return globResults;
    }

    private static GlobResult? BuildGlobResultFromIncludeItem<P, I>(ProjectItemElement itemElement, IReadOnlyDictionary<string, CumulativeRemoveElementData> removeElementCache, Expander<P, I> expander)
        where P : class, IProperty
        where I : class, IItem, IMetadataTable
    {
        var includeItemspec = new ItemSpec<P, I>(itemElement.Include, expander, itemElement.IncludeLocation, itemElement.ContainingProject.DirectoryPath);

        List<ItemSpecFragment>? includeGlobFragmentsList = null;
        foreach (ItemSpecFragment fragment in includeItemspec.Fragments)
        {
            if (fragment is GlobFragment && fragment.TextFragment.AsSpan().IndexOfAny(s_invalidGlobChars) < 0)
            {
                includeGlobFragmentsList ??= new List<ItemSpecFragment>(includeItemspec.Fragments.Count);
                includeGlobFragmentsList.Add(fragment);
            }
        }

        if (includeGlobFragmentsList == null || includeGlobFragmentsList.Count == 0)
        {
            return null;
        }

        string[] includeGlobStrings = new string[includeGlobFragmentsList.Count];
        for (int i = 0; i < includeGlobStrings.Length; ++i)
        {
            includeGlobStrings[i] = includeGlobFragmentsList[i].TextFragment;
        }

        var includeGlob = CompositeGlob.Create(includeGlobFragmentsList.Select(f => f.ToMSBuildGlob()));

        IEnumerable<string> excludeFragmentStrings = [];
        IMSBuildGlob? excludeGlob = null;

        if (!string.IsNullOrEmpty(itemElement.Exclude))
        {
            var excludeItemspec = new ItemSpec<P, I>(itemElement.Exclude, expander, itemElement.ExcludeLocation, itemElement.ContainingProject.DirectoryPath);

            excludeFragmentStrings = excludeItemspec.FlattenFragmentsAsStrings().ToImmutableHashSet();
            excludeGlob = excludeItemspec.ToMSBuildGlob();
        }

        IEnumerable<string> removeFragmentStrings = [];
        IMSBuildGlob? removeGlob = null;

        if (removeElementCache.TryGetValue(itemElement.ItemType, out CumulativeRemoveElementData removeItemElement))
        {
            removeFragmentStrings = removeItemElement.FragmentStrings;
            removeGlob = CompositeGlob.Create(removeItemElement.Globs);
        }

        var includeGlobWithGaps = CreateIncludeGlobWithGaps(includeGlob, excludeGlob, removeGlob);

        return new GlobResult(itemElement, includeGlobStrings.ToImmutableArray(), includeGlobWithGaps, excludeFragmentStrings, removeFragmentStrings);
    }

    private static IMSBuildGlob CreateIncludeGlobWithGaps(IMSBuildGlob includeGlob, IMSBuildGlob? excludeGlob, IMSBuildGlob? removeGlob)
    {
        return (excludeGlob, removeGlob) switch
        {
            (null, null) => includeGlob,
            (not null, null) => new MSBuildGlobWithGaps(includeGlob, excludeGlob),
            (null, not null) => new MSBuildGlobWithGaps(includeGlob, removeGlob),
            (not null, not null) => new MSBuildGlobWithGaps(includeGlob, new CompositeGlob(excludeGlob, removeGlob))
        };
    }

    private static void CacheInformationFromRemoveItem<P, I>(ProjectItemElement itemElement, Dictionary<string, CumulativeRemoveElementData> removeElementCache, Expander<P, I> expander)
        where P : class, IProperty
        where I : class, IItem, IMetadataTable
    {
        if (!removeElementCache.TryGetValue(itemElement.ItemType, out CumulativeRemoveElementData cumulativeRemoveElementData))
        {
            cumulativeRemoveElementData = CumulativeRemoveElementData.Create();

            removeElementCache[itemElement.ItemType] = cumulativeRemoveElementData;
        }

        var removeSpec = new ItemSpec<P, I>(itemElement.Remove, expander, itemElement.RemoveLocation, itemElement.ContainingProject.DirectoryPath);

        cumulativeRemoveElementData.AccumulateInformationFromRemoveItemSpec(removeSpec.FlattenFragmentsAsStrings(), removeSpec.ToMSBuildGlob());
    }

    // represents cumulated remove information for a particular item type
    private struct CumulativeRemoveElementData
    {
        private ImmutableList<IMSBuildGlob>.Builder _globs;
        private ImmutableHashSet<string>.Builder _fragmentStrings;

        public IEnumerable<IMSBuildGlob> Globs => _globs.ToImmutable();
        public IEnumerable<string> FragmentStrings => _fragmentStrings.ToImmutable();

        public static CumulativeRemoveElementData Create()
        {
            return new CumulativeRemoveElementData
            {
                _globs = ImmutableList.CreateBuilder<IMSBuildGlob>(),
                _fragmentStrings = ImmutableHashSet.CreateBuilder<string>()
            };
        }

        public readonly void AccumulateInformationFromRemoveItemSpec(IEnumerable<string> removeSpecFragmentStrings, IMSBuildGlob removeGlob)
        {
            _globs.Add(removeGlob);

            foreach (var removeFragment in removeSpecFragmentStrings)
            {
                _fragmentStrings.Add(removeFragment);
            }
        }
    }
}