File: Evaluation\Expander.ItemExpander.Transforms.cs
Web Access
Project: 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.Frozen;
using System.Collections.Generic;
using System.Globalization;
#if !FEATURE_MSIOREDIST
using System.IO;
#endif
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.NET.StringTools;

#if FEATURE_MSIOREDIST
// File is intentionally NOT aliased — all typeof() comparisons use fully-qualified
// System.IO.File to match the types registered in AvailableStaticMethods.
using Directory = Microsoft.IO.Directory;
using Path = Microsoft.IO.Path;
#endif

#nullable disable

namespace Microsoft.Build.Evaluation;

internal partial class Expander<P, I>
{
    private static partial class ItemExpander
    {
        /// <summary>
        /// The set of functions that called during an item transformation, e.g. @(CLCompile->ContainsMetadata('MetaName', 'metaValue')).
        /// </summary>
        private static partial class Transforms
        {
            /// <summary>
            /// The number of characters added by a quoted expression.
            /// 3 characters for
            ///  </summary>
            private const int QuotedExpressionSurroundCharCount = 3;

            /// <summary>
            /// A precomputed lookup of item spec modifiers wrapped in regex strings.
            /// This allows us to completely skip of Regex parsing when the inner string matches a known modifier.
            /// IsDerivableItemSpecModifier doesn't currently support Span lookups, so we have to manually map these.
            /// </summary>
            private static readonly FrozenDictionary<string, string> s_itemSpecModifiers = new Dictionary<string, string>()
            {
                [$"%({ItemSpecModifiers.FullPath})"] = ItemSpecModifiers.FullPath,
                [$"%({ItemSpecModifiers.RootDir})"] = ItemSpecModifiers.RootDir,
                [$"%({ItemSpecModifiers.Filename})"] = ItemSpecModifiers.Filename,
                [$"%({ItemSpecModifiers.Extension})"] = ItemSpecModifiers.Extension,
                [$"%({ItemSpecModifiers.RelativeDir})"] = ItemSpecModifiers.RelativeDir,
                [$"%({ItemSpecModifiers.Directory})"] = ItemSpecModifiers.Directory,
                [$"%({ItemSpecModifiers.RecursiveDir})"] = ItemSpecModifiers.RecursiveDir,
                [$"%({ItemSpecModifiers.Identity})"] = ItemSpecModifiers.Identity,
                [$"%({ItemSpecModifiers.ModifiedTime})"] = ItemSpecModifiers.ModifiedTime,
                [$"%({ItemSpecModifiers.CreatedTime})"] = ItemSpecModifiers.CreatedTime,
                [$"%({ItemSpecModifiers.AccessedTime})"] = ItemSpecModifiers.AccessedTime,
                [$"%({ItemSpecModifiers.DefiningProjectFullPath})"] = ItemSpecModifiers.DefiningProjectFullPath,
                [$"%({ItemSpecModifiers.DefiningProjectDirectory})"] = ItemSpecModifiers.DefiningProjectDirectory,
                [$"%({ItemSpecModifiers.DefiningProjectName})"] = ItemSpecModifiers.DefiningProjectName,
                [$"%({ItemSpecModifiers.DefiningProjectExtension})"] = ItemSpecModifiers.DefiningProjectExtension,
            }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

            /// <summary>
            /// A thread-static string builder for use in ExpandQuotedExpressionFunction.
            /// In theory we should be able to use shared instance, but in a profile it appears something higher in
            /// the call-stack is already borrowing the instance, so it ends up always allocating.
            /// This should not be used outside of ExpandQuotedExpressionFunction unless validated to not conflict.
            /// </summary>
            [ThreadStatic]
            private static SpanBasedStringBuilder s_includeBuilder;

            /// <summary>
            /// A reference to the last extracted expression function to save on Regex-related allocations.
            /// In many cases, the expression is exactly the same as the previous.
            /// </summary>
            private static string s_lastParsedQuotedExpression;

            /// <summary>
            /// Intrinsic function that adds the number of items in the list.
            /// </summary>
            internal static void Count(List<TransformEntry> input, List<TransformEntry> output)
                => output.Add(new TransformEntry(input.Count.ToString(CultureInfo.InvariantCulture), item: null));

            /// <summary>
            /// Intrinsic function that adds the specified built-in modifer value of the items in input
            /// Each entry pairs the current item include with the item under transformation.
            /// </summary>
            internal static void ItemSpecModifierFunction(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                foreach (TransformEntry item in input)
                {
                    // If the item include has become empty,
                    // this is the end of the pipeline for this item
                    if (String.IsNullOrEmpty(item.Value))
                    {
                        continue;
                    }

                    string result = null;

                    try
                    {
                        // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                        // In that case,
                        // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which
                        // only exist within a target where we can trust the current directory
                        // 2. in single process mode we get the project directory set for the thread
                        string directoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory();
                        string definingProjectEscaped = item.Item.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath);

                        result = ItemSpecModifiers.GetItemSpecModifier(item.Value, functionName, directoryToUse, definingProjectEscaped);
                    }
                    // InvalidOperationException is how GetItemSpecModifier communicates invalid conditions upwards, so
                    // we do not want to rethrow in that case.
                    catch (Exception e) when (!ExceptionHandling.NotExpectedException(e) || e is InvalidOperationException)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message);
                    }

                    if (!String.IsNullOrEmpty(result))
                    {
                        // GetItemSpecModifier will have returned us an escaped string
                        // there is nothing more to do than yield it into the pipeline
                        output.Add(new TransformEntry(result, item.Item));
                    }
                    else if (includeNullEntries)
                    {
                        output.Add(new TransformEntry(null, item.Item));
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds the subset of items that actually exist on disk.
            /// </summary>
            internal static void Exists(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                foreach (TransformEntry item in input)
                {
                    if (String.IsNullOrEmpty(item.Value))
                    {
                        continue;
                    }

                    // Unescape as we are passing to the file system
                    string unescapedPath = EscapingUtilities.UnescapeAll(item.Value);

                    string rootedPath = null;
                    try
                    {
                        // If we're a projectitem instance then we need to get
                        // the project directory and be relative to that
                        if (Path.IsPathRooted(unescapedPath))
                        {
                            rootedPath = unescapedPath;
                        }
                        else
                        {
                            // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                            // In that case,
                            // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which
                            // only exist within a target where we can trust the current directory
                            // 2. in single process mode we get the project directory set for the thread
                            string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty;
                            rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath);
                        }
                    }
                    catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message);
                    }

                    if (FileSystems.Default.FileOrDirectoryExists(rootedPath))
                    {
                        output.Add(item);
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that combines the existing paths of the input items with a given relative path.
            /// </summary>
            internal static void Combine(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string relativePath = arguments[0];

                foreach (TransformEntry item in input)
                {
                    if (String.IsNullOrEmpty(item.Value))
                    {
                        continue;
                    }

                    // Unescape as we are passing to the file system
                    string unescapedPath = EscapingUtilities.UnescapeAll(item.Value);
                    string combinedPath = Path.Combine(unescapedPath, relativePath);
                    string escapedPath = EscapingUtilities.Escape(combinedPath);
                    output.Add(new TransformEntry(escapedPath, null));
                }
            }

            /// <summary>
            /// Intrinsic function that adds all ancestor directories of the given items.
            /// </summary>
            internal static void GetPathsOfAllDirectoriesAbove(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                // Phase 1: find all the applicable directories.

                SortedSet<string> directories = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);

                foreach (TransformEntry item in input)
                {
                    if (String.IsNullOrEmpty(item.Value))
                    {
                        continue;
                    }

                    string directoryName = null;

                    // Unescape as we are passing to the file system
                    string unescapedPath = EscapingUtilities.UnescapeAll(item.Value);

                    try
                    {
                        string rootedPath;

                        // If we're a projectitem instance then we need to get
                        // the project directory and be relative to that
                        if (Path.IsPathRooted(unescapedPath))
                        {
                            rootedPath = unescapedPath;
                        }
                        else
                        {
                            // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                            // In that case,
                            // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which
                            // only exist within a target where we can trust the current directory
                            // 2. in single process mode we get the project directory set for the thread
                            string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty;
                            rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath);
                        }

                        // Normalize the path to remove elements like "..".
                        // Otherwise we run the risk of returning two or more different paths that represent the
                        // same directory.
                        rootedPath = FileUtilities.NormalizePath(rootedPath);
                        directoryName = Path.GetDirectoryName(rootedPath);
                    }
                    catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message);
                    }

                    while (!String.IsNullOrEmpty(directoryName))
                    {
                        if (directories.Contains(directoryName))
                        {
                            // We've already got this directory (and all its ancestors) in the set.
                            break;
                        }

                        directories.Add(directoryName);
                        directoryName = Path.GetDirectoryName(directoryName);
                    }
                }

                // Phase 2: Go through the directories and return them in order

                foreach (string directoryPath in directories)
                {
                    string escapedDirectoryPath = EscapingUtilities.Escape(directoryPath);
                    output.Add(new TransformEntry(escapedDirectoryPath, null));
                }
            }

            /// <summary>
            /// Intrinsic function that adds the DirectoryName of the items in input
            /// UNDONE: This can be removed in favor of a built-in %(DirectoryName) metadata in future.
            /// </summary>
            internal static void DirectoryName(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                Dictionary<string, string> directoryNameTable = new Dictionary<string, string>(input.Count, StringComparer.OrdinalIgnoreCase);

                foreach (TransformEntry item in input)
                {
                    // If the item include has become empty,
                    // this is the end of the pipeline for this item
                    if (String.IsNullOrEmpty(item.Value))
                    {
                        continue;
                    }

                    string directoryName;
                    if (!directoryNameTable.TryGetValue(item.Value, out directoryName))
                    {
                        // Unescape as we are passing to the file system
                        string unescapedPath = EscapingUtilities.UnescapeAll(item.Value);

                        try
                        {
                            string rootedPath;

                            // If we're a projectitem instance then we need to get
                            // the project directory and be relative to that
                            if (Path.IsPathRooted(unescapedPath))
                            {
                                rootedPath = unescapedPath;
                            }
                            else
                            {
                                // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                                // In that case,
                                // 1. in multiprocess mode we're safe to get the current directory as we'll be running on TaskItems which
                                // only exist within a target where we can trust the current directory
                                // 2. in single process mode we get the project directory set for the thread
                                string baseDirectoryToUse = item.Item.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? String.Empty;
                                rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath);
                            }

                            directoryName = Path.GetDirectoryName(rootedPath);
                        }
                        catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Value, e.Message);
                        }

                        // Escape as this is going back into the engine
                        directoryName = EscapingUtilities.Escape(directoryName);
                        directoryNameTable[unescapedPath] = directoryName;
                    }

                    if (!String.IsNullOrEmpty(directoryName))
                    {
                        // return a result through the enumerator
                        output.Add(new TransformEntry(directoryName, item.Item));
                    }
                    else if (includeNullEntries)
                    {
                        output.Add(new TransformEntry(null, item.Item));
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds the contents of the metadata in specified in argument[0].
            /// </summary>
            internal static void Metadata(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string metadataName = arguments[0];

                foreach (TransformEntry item in input)
                {
                    if (item.Item != null)
                    {
                        string metadataValue = null;

                        try
                        {
                            metadataValue = item.Item.GetMetadataValueEscaped(metadataName);
                        }
                        catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
                        {
                            // Blank metadata name
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message);
                        }

                        if (!String.IsNullOrEmpty(metadataValue))
                        {
                            // It may be that the itemspec has unescaped ';'s in it so we need to split here to handle
                            // that case.
                            if (metadataValue.Contains(';'))
                            {
                                var splits = ExpressionShredder.SplitSemiColonSeparatedList(metadataValue);

                                foreach (string itemSpec in splits)
                                {
                                    // return a result through the enumerator
                                    output.Add(new TransformEntry(itemSpec, item.Item));
                                }
                            }
                            else
                            {
                                // return a result through the enumerator
                                output.Add(new TransformEntry(metadataValue, item.Item));
                            }
                        }
                        else if (metadataValue != String.Empty && includeNullEntries)
                        {
                            output.Add(new TransformEntry(metadataValue, item.Item));
                        }
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds only the items from input that have a distinct Include
            /// Using a case sensitive comparison.
            /// </summary>
            internal static void DistinctWithCase(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
                => DistinctWithComparer(input, output, arguments, StringComparer.Ordinal, functionName, elementLocation);

            /// <summary>
            /// Intrinsic function that adds only the items from input that have a distinct Include
            /// Using a case insensitive comparison.
            /// </summary>
            internal static void Distinct(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
                => DistinctWithComparer(input, output, arguments, StringComparer.OrdinalIgnoreCase, functionName, elementLocation);

            /// <summary>
            /// Intrinsic function that adds only the items from input that have a distinct Include
            /// using the specified comparer.
            /// </summary>
            private static void DistinctWithComparer(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                StringComparer comparer,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                // This dictionary will ensure that we only return one result per unique itemspec
                HashSet<string> seenItems = new HashSet<string>(input.Count, comparer);

                foreach (TransformEntry item in input)
                {
                    if (item.Value != null && seenItems.Add(item.Value))
                    {
                        output.Add(item);
                    }
                }
            }

            /// <summary>
            /// Intrinsic function reverses the item list.
            /// </summary>
            internal static void Reverse(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                for (int i = input.Count - 1; i >= 0; i--)
                {
                    output.Add(input[i]);
                }
            }

            /// <summary>
            ///  Intrinsic function that transforms expressions like the %(foo) in @(Compile->'%(foo)').
            /// </summary>
            internal static void ExpandQuotedExpressionFunction(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                ExpandQuotedExpressionFunction(input, output, arguments[0], includeNullEntries, elementLocation);
            }

            internal static void ExpandQuotedExpressionFunction(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string text,
                bool includeNullEntries,
                IElementLocation elementLocation)
            {
                OneOrMultipleMetadataMatches matches = GetQuotedExpressionMatches(text, elementLocation);

                switch (matches.Type)
                {
                    case MetadataMatchType.None:
                        ExpandLiteralTransform(input, output, text, includeNullEntries);
                        return;

                    case MetadataMatchType.ExactSingle:
                        ExpandExactMetadataTransform(input, output, matches.Single, includeNullEntries, elementLocation);
                        return;

                    case MetadataMatchType.InexactSingle:
                        ExpandSingleMetadataTransform(input, output, text, matches.Single, includeNullEntries, elementLocation);
                        return;

                    case MetadataMatchType.Multiple:
                        ExpandMultipleMetadataTransform(input, output, text, matches.Multiple, includeNullEntries, elementLocation);
                        return;
                }
            }

            private static void ExpandLiteralTransform(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string literal,
                bool includeNullEntries)
            {
                foreach (TransformEntry item in input)
                {
                    AddTransformResult(output, item.Value is null ? null : literal, item.Item, includeNullEntries);
                }
            }

            private static void ExpandExactMetadataTransform(
                List<TransformEntry> input,
                List<TransformEntry> output,
                MetadataMatch match,
                bool includeNullEntries,
                IElementLocation elementLocation)
            {
                foreach (TransformEntry item in input)
                {
                    string include = null;
                    if (item.Value is not null)
                    {
                        include = GetMetadataValueFromMatch(match, item.Value, item.Item, elementLocation);
                    }

                    AddTransformResult(output, include, item.Item, includeNullEntries);
                }
            }

            private static void ExpandSingleMetadataTransform(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string text,
                MetadataMatch match,
                bool includeNullEntries,
                IElementLocation elementLocation)
            {
                SpanBasedStringBuilder includeBuilder = s_includeBuilder ?? new SpanBasedStringBuilder();
                s_includeBuilder = null;

                int prefixLength = match.Index;
                int suffixIndex = match.Index + match.Length;
                int suffixLength = text.Length - suffixIndex;

                foreach (TransformEntry item in input)
                {
                    string include = null;
                    if (item.Value is not null)
                    {
                        if (prefixLength > 0)
                        {
                            includeBuilder.Append(text, 0, prefixLength);
                        }

                        includeBuilder.Append(GetMetadataValueFromMatch(match, item.Value, item.Item, elementLocation));

                        if (suffixLength > 0)
                        {
                            includeBuilder.Append(text, suffixIndex, suffixLength);
                        }

                        include = includeBuilder.ToString();
                        includeBuilder.Clear();
                    }

                    AddTransformResult(output, include, item.Item, includeNullEntries);
                }

                s_includeBuilder = includeBuilder;
            }

            private static void ExpandMultipleMetadataTransform(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string text,
                List<MetadataMatch> matches,
                bool includeNullEntries,
                IElementLocation elementLocation)
            {
                SpanBasedStringBuilder includeBuilder = s_includeBuilder ?? new SpanBasedStringBuilder();
                s_includeBuilder = null;

                foreach (TransformEntry item in input)
                {
                    string include = null;
                    if (item.Value is not null)
                    {
                        int currentIndex = 0;
                        foreach (MetadataMatch match in matches)
                        {
                            includeBuilder.Append(text, currentIndex, match.Index - currentIndex);
                            includeBuilder.Append(GetMetadataValueFromMatch(match, item.Value, item.Item, elementLocation));
                            currentIndex = match.Index + match.Length;
                        }

                        includeBuilder.Append(text, currentIndex, text.Length - currentIndex);
                        include = includeBuilder.ToString();
                        includeBuilder.Clear();
                    }

                    AddTransformResult(output, include, item.Item, includeNullEntries);
                }

                s_includeBuilder = includeBuilder;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            private static void AddTransformResult(List<TransformEntry> output, string include, I item, bool includeNullEntries)
            {
                // Empty transforms are optionally retained as null entries so callers can correlate
                // transform results with their source items.
                if (!include.IsNullOrEmpty())
                {
                    output.Add(new TransformEntry(include, item));
                }
                else if (includeNullEntries)
                {
                    output.Add(new TransformEntry(value: null, item));
                }
            }

            /// <summary>
            /// Scans the input string for unqualified metadata references of the form <c>%(Name)</c>.
            /// In the vast majority of cases, we'll only have 1-2 matches.
            /// Qualified metadata (e.g. <c>%(ItemType.Name)</c>) is not allowed in transforms and will throw.
            /// </summary>
            private static OneOrMultipleMetadataMatches GetQuotedExpressionMatches(string text, IElementLocation elementLocation)
            {
                // Exact metadata references can use cached names or the built-in modifier lookup.
                if (text is ['%', '(', .., ')'] &&
                    (TryGetCachedMetadataMatch(text, out string cachedName)
                     || s_itemSpecModifiers.TryGetValue(text, out cachedName)))
                {
                    return new OneOrMultipleMetadataMatches(cachedName);
                }

                int metadataMarkerIndex = ExpressionShredder.IndexOfMetadataMarker(text);
                if (metadataMarkerIndex == -1)
                {
                    return OneOrMultipleMetadataMatches.None;
                }

                List<MetadataMatch> multipleMatches = null;
                MetadataMatch firstMatch = default;
                bool hasFirstMatch = false;

                do
                {
                    int refEnd = metadataMarkerIndex + 2;

                    if (!ExpressionShredder.TryParseMetadataExpression(text, ref refEnd, text.Length, out string itemType, out string name))
                    {
                        metadataMarkerIndex = ExpressionShredder.IndexOfMetadataMarker(text, metadataMarkerIndex + 2);
                        continue;
                    }

                    // Qualified metadata is not allowed in transforms.
                    if (itemType != null)
                    {
                        string matchValue = text.Substring(metadataMarkerIndex, refEnd - metadataMarkerIndex);
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "QualifiedMetadataInTransformNotAllowed", matchValue, name);
                    }

                    int matchLength = refEnd - metadataMarkerIndex;

                    if (!hasFirstMatch)
                    {
                        firstMatch = new MetadataMatch(metadataMarkerIndex, matchLength, name);
                        hasFirstMatch = true;
                    }
                    else
                    {
                        multipleMatches ??= [firstMatch];
                        multipleMatches.Add(new MetadataMatch(metadataMarkerIndex, matchLength, name));
                    }

                    metadataMarkerIndex = ExpressionShredder.IndexOfMetadataMarker(text, refEnd);
                }
                while (metadataMarkerIndex >= 0);

                if (multipleMatches != null)
                {
                    return new OneOrMultipleMetadataMatches(multipleMatches);
                }

                if (hasFirstMatch)
                {
                    OneOrMultipleMetadataMatches singleMatch = new(text, firstMatch.Index, firstMatch.Length, firstMatch.Name);

                    if (singleMatch.Type == MetadataMatchType.ExactSingle && !ItemSpecModifiers.IsItemSpecModifier(firstMatch.Name))
                    {
                        s_lastParsedQuotedExpression = firstMatch.Name;
                    }

                    return singleMatch;
                }

                return new OneOrMultipleMetadataMatches();
            }

            /// <summary>
            ///  Tries to reuse the metadata name cached from the most recently parsed exact quoted
            ///  metadata reference.
            /// </summary>
            /// <remarks>
            ///  The cached name is copied to a local and validated against <paramref name="stringToCheck"/>
            ///  before it is returned, so another thread may safely replace the cached value concurrently.
            /// </remarks>
            /// <param name="stringToCheck">The quoted metadata expression to inspect.</param>
            /// <param name="cachedMatch">The cached metadata name when it matches the expression.</param>
            /// <returns>
            ///  <see langword="true"/> when the cached metadata name matches the expression; otherwise,
            ///  <see langword="false"/>.
            /// </returns>
            private static bool TryGetCachedMetadataMatch(string stringToCheck, out string cachedMatch)
            {
                // Pull a local reference first in case the cached value is swapped.
                cachedMatch = s_lastParsedQuotedExpression;
                if (string.IsNullOrEmpty(cachedMatch))
                {
                    return false;
                }

                // Quickly cancel out of definite misses.
                int length = stringToCheck.Length;
                if (length == cachedMatch.Length + QuotedExpressionSurroundCharCount)
                {
                    // If the inner slice is a hit, don't allocate a string.
                    ReadOnlySpan<char> span = stringToCheck.AsSpan(2, length - QuotedExpressionSurroundCharCount);
                    if (span.SequenceEqual(cachedMatch.AsSpan()))
                    {
                        return true;
                    }
                }

                return false;
            }

            /// <summary>
            /// Intrinsic function that transforms expressions by invoking methods of System.String on the itemspec
            /// of the item in the pipeline.
            /// </summary>
            internal static void ExecuteStringFunction(
                Expander<P, I> expander,
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                // Transform: expression is like @(Compile->'%(foo)'), so create completely new items,
                // using the Include from the source items
                foreach (TransformEntry item in input)
                {
                    Function function = new Function(
                        typeof(string),
                        item.Value,
                        item.Value,
                        functionName,
                        arguments,
                        BindingFlags.Public | BindingFlags.InvokeMethod,
                        string.Empty,
                        expander.PropertiesUseTracker,
                        expander._fileSystem,
                        expander._loggingContext);

                    object result = function.Execute(item.Value, expander._properties, ExpanderOptions.ExpandAll, elementLocation);

                    string include = PropertyExpander.ConvertToString(result);

                    // We pass in the existing item so we can copy over its metadata
                    if (include.Length > 0)
                    {
                        output.Add(new TransformEntry(include, item.Item));
                    }
                    else if (includeNullEntries)
                    {
                        output.Add(new TransformEntry(null, item.Item));
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds the items from input with their metadata cleared, i.e. only the itemspec is retained.
            /// </summary>
            internal static void ClearMetadata(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                bool includeNullEntries,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                foreach (TransformEntry item in input)
                {
                    if (includeNullEntries || item.Value != null)
                    {
                        output.Add(new TransformEntry(item.Value, null));
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds only those items that have a not-blank value for the metadata specified
            /// Using a case insensitive comparison.
            /// </summary>
            internal static void HasMetadata(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string metadataName = arguments[0];

                foreach (TransformEntry item in input)
                {
                    string metadataValue = null;

                    try
                    {
                        metadataValue = item.Item.GetMetadataValueEscaped(metadataName);
                    }
                    catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
                    {
                        // Blank metadata name
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message);
                    }

                    // GetMetadataValueEscaped returns empty string for missing metadata,
                    // but IItem specifies it should return null
                    if (!string.IsNullOrEmpty(metadataValue))
                    {
                        // return a result through the enumerator
                        output.Add(item);
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds only those items have the given metadata value
            /// Using a case insensitive comparison.
            /// </summary>
            internal static void WithMetadataValue(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string metadataName = arguments[0];
                string metadataValueToFind = arguments[1];

                foreach (TransformEntry item in input)
                {
                    string metadataValue = null;

                    try
                    {
                        metadataValue = item.Item.GetMetadataValueEscaped(metadataName);
                    }
                    catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
                    {
                        // Blank metadata name
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message);
                    }

                    if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase))
                    {
                        // return a result through the enumerator
                        output.Add(item);
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds those items don't have the given metadata value
            /// Using a case insensitive comparison.
            /// </summary>
            internal static void WithoutMetadataValue(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string metadataName = arguments[0];
                string metadataValueToFind = arguments[1];

                foreach (TransformEntry item in input)
                {
                    string metadataValue = null;

                    try
                    {
                        metadataValue = item.Item.GetMetadataValueEscaped(metadataName);
                    }
                    catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
                    {
                        // Blank metadata name
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message);
                    }

                    if (!String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase))
                    {
                        // return a result through the enumerator
                        output.Add(item);
                    }
                }
            }

            /// <summary>
            /// Intrinsic function that adds a boolean to indicate if any of the items have the given metadata value
            /// Using a case insensitive comparison.
            /// </summary>
            internal static void AnyHaveMetadataValue(
                List<TransformEntry> input,
                List<TransformEntry> output,
                string[] arguments,
                string functionName,
                IElementLocation elementLocation)
            {
                ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);

                string metadataName = arguments[0];
                string metadataValueToFind = arguments[1];
                bool metadataFound = false;

                foreach (TransformEntry item in input)
                {
                    if (item.Item != null)
                    {
                        string metadataValue = null;

                        try
                        {
                            metadataValue = item.Item.GetMetadataValueEscaped(metadataName);
                        }
                        catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
                        {
                            // Blank metadata name
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message);
                        }

                        if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase))
                        {
                            metadataFound = true;

                            // return a result through the enumerator
                            output.Add(new TransformEntry("true", item.Item));

                            // break out as soon as we found a match
                            return;
                        }
                    }
                }

                if (!metadataFound)
                {
                    // We did not locate an item with the required metadata
                    output.Add(new TransformEntry("false", null));
                }
            }

            /// <summary>
            /// Expands the metadata in the match provided into a string result.
            /// The match is expected to be the content of a transform.
            /// For example, representing "%(Filename.obj)" in the original expression "@(Compile->'%(Filename.obj)')".
            /// </summary>
            private static string GetMetadataValueFromMatch(
                MetadataMatch match,
                string itemSpec,
                IItem sourceOfMetadata,
                IElementLocation elementLocation)
            {
                string value = null;
                try
                {
                    if (match.IsDerivableItemSpecModifier)
                    {
                        ItemSpecModifierKind modifierKind = match.ModifierKind;
                        string directoryToUse = null;
                        string definingProjectEscaped = null;

                        // Only path-based and defining-project modifiers need project context. ProjectItem and
                        // ProjectItemInstance provide ProjectDirectory; otherwise, single-process evaluation sets
                        // the thread working directory, and multiprocess TaskItems can trust the process directory.
                        switch (modifierKind)
                        {
                            case ItemSpecModifierKind.FullPath:
                            case ItemSpecModifierKind.RootDir:
                            case ItemSpecModifierKind.Directory:
                                directoryToUse = sourceOfMetadata.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory();
                                break;

                            case ItemSpecModifierKind.DefiningProjectFullPath:
                            case ItemSpecModifierKind.DefiningProjectDirectory:
                            case ItemSpecModifierKind.DefiningProjectName:
                            case ItemSpecModifierKind.DefiningProjectExtension:
                                directoryToUse = sourceOfMetadata.ProjectDirectory ?? FileUtilities.CurrentThreadWorkingDirectory ?? Directory.GetCurrentDirectory();
                                definingProjectEscaped = sourceOfMetadata.GetMetadataValueEscaped(ItemSpecModifiers.DefiningProjectFullPath);
                                break;
                        }

                        value = ItemSpecModifiers.GetItemSpecModifier(itemSpec, modifierKind, directoryToUse, definingProjectEscaped);
                    }
                    else
                    {
                        value = sourceOfMetadata.GetMetadataValueEscaped(match.Name);
                    }
                }
                catch (InvalidOperationException ex)
                {
                    ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", match.Name, ex.Message);
                }

                return value;
            }
        }
    }
}