File: Evaluation\ExpressionShredder.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.Generic;
using Microsoft.Build.Collections;
using Microsoft.Build.Shared;
using Microsoft.NET.StringTools;

#nullable disable

namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// What the shredder should be looking for.
    /// </summary>
    [Flags]
    internal enum ShredderOptions
    {
        /// <summary>
        /// Don't use
        /// </summary>
        Invalid = 0x0,

        /// <summary>
        /// Shred item types
        /// </summary>
        ItemTypes = 0x1,

        /// <summary>
        /// Shred metadata not contained inside of a transform.
        /// </summary>
        MetadataOutsideTransforms = 0x2,

        /// <summary>
        /// Shred both items and metadata not contained in a transform.
        /// </summary>
        All = ItemTypes | MetadataOutsideTransforms
    }

    /// <summary>
    /// A class which interprets and splits MSBuild expressions
    /// </summary>
    internal static class ExpressionShredder
    {
        /// <summary>
        ///  The marker that can begin a property expression.
        /// </summary>
        public const string PropertyMarker = "$(";

        /// <summary>
        ///  The marker that can begin an item-vector expression.
        /// </summary>
        public const string ItemVectorMarker = "@(";

        /// <summary>
        ///  The marker that can begin a metadata expression.
        /// </summary>
        public const string MetadataMarker = "%(";

        private const char PropertyMarkerPrefix = '$';
        private const char ItemVectorMarkerPrefix = '@';
        private const char MetadataMarkerPrefix = '%';

        private static readonly char[] s_itemVectorOrMetadataMarkerPrefixes = [ItemVectorMarkerPrefix, MetadataMarkerPrefix];

        /// <summary>
        ///  Determines whether <paramref name="expression"/> contains a property marker.
        /// </summary>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  <see langword="true"/> if a property marker is found; otherwise, <see langword="false"/>.
        /// </returns>
        public static bool ContainsPropertyMarker(string expression)
            => IndexOfPropertyMarker(expression) >= 0;

        /// <summary>
        ///  Determines whether <paramref name="expression"/> contains an item-vector marker.
        /// </summary>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  <see langword="true"/> if an item-vector marker is found; otherwise, <see langword="false"/>.
        /// </returns>
        public static bool ContainsItemVectorMarker(string expression)
            => IndexOfItemVectorMarker(expression) >= 0;

        /// <summary>
        ///  Determines whether <paramref name="expression"/> contains a metadata marker.
        /// </summary>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  <see langword="true"/> if a metadata marker is found; otherwise, <see langword="false"/>.
        /// </returns>
        public static bool ContainsMetadataMarker(string expression)
            => IndexOfMetadataMarker(expression) >= 0;

        /// <summary>
        ///  Finds the first property marker.
        /// </summary>
        /// <remarks>
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfPropertyMarker(string expression)
            => IndexOfMarker(expression, PropertyMarkerPrefix);

        /// <summary>
        ///  Finds the first property marker at or after <paramref name="startIndex"/>.
        /// </summary>
        /// <inheritdoc cref="IndexOfPropertyMarker(string)"/>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfPropertyMarker(string expression, int startIndex)
            => IndexOfMarker(expression, PropertyMarkerPrefix, startIndex, expression.Length - startIndex);

        /// <summary>
        ///  Finds the first property marker in the range beginning at <paramref name="startIndex"/>
        ///  and spanning <paramref name="count"/> characters.
        /// </summary>
        /// <remarks>
        ///  Both characters of the marker must be contained within the specified range.
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <param name="count">The number of characters to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfPropertyMarker(string expression, int startIndex, int count)
            => IndexOfMarker(expression, PropertyMarkerPrefix, startIndex, count);

        /// <summary>
        ///  Finds the first item-vector marker.
        /// </summary>
        /// <remarks>
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfItemVectorMarker(string expression)
            => IndexOfMarker(expression, ItemVectorMarkerPrefix);

        /// <summary>
        ///  Finds the first item-vector marker at or after <paramref name="startIndex"/>.
        /// </summary>
        /// <remarks>
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfItemVectorMarker(string expression, int startIndex)
            => IndexOfMarker(expression, ItemVectorMarkerPrefix, startIndex, expression.Length - startIndex);

        /// <summary>
        ///  Finds the first item-vector marker in the range beginning at <paramref name="startIndex"/>
        ///  and spanning <paramref name="count"/> characters.
        /// </summary>
        /// <remarks>
        ///  Both characters of the marker must be contained within the specified range.
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <param name="count">The number of characters to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfItemVectorMarker(string expression, int startIndex, int count)
            => IndexOfMarker(expression, ItemVectorMarkerPrefix, startIndex, count);

        /// <summary>
        ///  Finds the first metadata marker.
        /// </summary>
        /// <remarks>
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfMetadataMarker(string expression)
            => IndexOfMarker(expression, MetadataMarkerPrefix);

        /// <summary>
        ///  Finds the first metadata marker at or after <paramref name="startIndex"/>.
        /// </summary>
        /// <remarks>
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfMetadataMarker(string expression, int startIndex)
            => IndexOfMarker(expression, MetadataMarkerPrefix, startIndex, expression.Length - startIndex);

        /// <summary>
        ///  Finds the first metadata marker in the range beginning at <paramref name="startIndex"/>
        ///  and spanning <paramref name="count"/> characters.
        /// </summary>
        /// <remarks>
        ///  Both characters of the marker must be contained within the specified range.
        ///  This method does not validate the expression or locate its closing parenthesis.
        /// </remarks>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <param name="count">The number of characters to scan.</param>
        /// <returns>
        ///  The zero-based index of the marker, or <c>-1</c> if it is not found.
        /// </returns>
        public static int IndexOfMetadataMarker(string expression, int startIndex, int count)
            => IndexOfMarker(expression, MetadataMarkerPrefix, startIndex, count);

        private static int IndexOfMarker(string expression, char marker)
        {
            // IndexOf(char) is significantly faster than an ordinal two-character search,
            // especially when the marker is absent, so check the opening parenthesis separately.
            int markerIndex = expression.IndexOf(marker);
            if (markerIndex < 0 || markerIndex == expression.Length - 1)
            {
                return -1;
            }

            do
            {
                int nextIndex = markerIndex + 1;
                if (expression[nextIndex] == '(')
                {
                    return markerIndex;
                }

                markerIndex = expression.IndexOf(marker, nextIndex);
            }
            while (markerIndex >= 0 && markerIndex < expression.Length - 1);

            return -1;
        }

        private static int IndexOfMarker(string expression, char marker, int startIndex, int count)
        {
            // IndexOf(char, int, int) is significantly faster than an ordinal two-character search,
            // especially when the marker is absent, so check the opening parenthesis separately.
            int markerIndex = expression.IndexOf(marker, startIndex, count);
            if (markerIndex < 0)
            {
                return -1;
            }

            int endIndex = startIndex + count;
            if (markerIndex == endIndex - 1)
            {
                return -1;
            }

            do
            {
                int nextIndex = markerIndex + 1;
                if (expression[nextIndex] == '(')
                {
                    return markerIndex;
                }

                markerIndex = expression.IndexOf(marker, nextIndex, endIndex - nextIndex);
            }
            while (markerIndex >= 0 && markerIndex < endIndex - 1);

            return -1;
        }

        /// <summary>
        /// Splits an expression into fragments at semi-colons, except where the
        /// semi-colons are in a macro or separator expression.
        /// Fragments are trimmed and empty fragments discarded.
        /// </summary>
        /// <remarks>
        /// See <see cref="SemiColonTokenizer"/> for rules.
        /// </remarks>
        /// <param name="expression">List expression to split</param>
        /// <returns>Array of non-empty strings from split list.</returns>
        internal static SemiColonTokenizer SplitSemiColonSeparatedList(string expression)
        {
            return new SemiColonTokenizer(expression);
        }

        /// <summary>
        /// Given a list of expressions that may contain item list expressions,
        /// returns a pair of tables of all item names found, as K=Name, V=String.Empty;
        /// and all metadata not in transforms, as K=Metadata key, V=MetadataReference,
        /// where metadata key is like "itemname.metadataname" or "metadataname".
        /// PERF: Tables are null if there are no entries, because this is quite a common case.
        /// </summary>
        internal static ItemsAndMetadataPair GetReferencedItemNamesAndMetadata(IReadOnlyList<string> expressions)
        {
            ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null);

            // PERF: Use for to avoid boxing expressions enumerator
            for (int i = 0; i < expressions.Count; i++)
            {
                string expression = expressions[i];
                GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.All);
            }

            return pair;
        }

        /// <summary>
        /// Returns true if there is a metadata expression (outside of a transform) in the expression.
        /// </summary>
        internal static bool ContainsMetadataExpressionOutsideTransform(string expression)
        {
            ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null);

            GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.MetadataOutsideTransforms);

            bool result = (pair.Metadata?.Count > 0);

            return result;
        }

        /// <inheritdoc cref="TryGetNextItemVectorExpression(string, int, out ItemExpressionCapture)"/>
        public static bool TryGetNextItemVectorExpression(string expression, out ItemExpressionCapture itemVector)
            => TryGetNextItemVectorExpression(expression, startIndex: 0, out itemVector);

        /// <summary>
        ///  Finds and parses the next valid item-vector expression at or after <paramref name="startIndex"/>.
        /// </summary>
        /// <param name="expression">The expression to scan.</param>
        /// <param name="startIndex">The index at which to begin scanning.</param>
        /// <param name="itemVector">The parsed item-vector expression.</param>
        /// <returns>
        ///  <see langword="true"/> if a valid item-vector expression is found; otherwise,
        ///  <see langword="false"/>.
        /// </returns>
        public static bool TryGetNextItemVectorExpression(string expression, int startIndex, out ItemExpressionCapture itemVector)
        {
            while ((startIndex = IndexOfItemVectorMarker(expression, startIndex)) >= 0)
            {
                if (TryParseItemVectorExpression(expression, startIndex, out itemVector))
                {
                    return true;
                }

                startIndex += 2;
            }

            itemVector = default;
            return false;
        }

        private static bool TryParseItemVectorExpression(string expression, int startIndex, out ItemExpressionCapture itemVector)
        {
            int end = expression.Length;
            int index = startIndex + 2;

            SinkWhitespace(expression, ref index);

            int startOfName = index;

            if (!SinkValidName(expression, ref index, end))
            {
                itemVector = default;
                return false;
            }

            // '-' is a legitimate char in an item name, but we should match '->' as an arrow
            // in '@(foo->'x')' rather than as the last char of the item name.
            // The old regex accomplished this by being "greedy"
            if (end > index && expression[index - 1] == '-' && expression[index] == '>')
            {
                index--;
            }

            // Grab the name, but continue to verify it's a well-formed expression
            // before we store it.
            string itemName = Strings.WeakIntern(expression.AsSpan(startOfName, index - startOfName));

            SinkWhitespace(expression, ref index);
            List<ItemExpressionCapture> transformExpressions = null;

            // If there's an '->' eat it and the subsequent quoted expression or transform function
            while (Sink(expression, ref index, end, '-', '>'))
            {
                SinkWhitespace(expression, ref index);
                int startTransform = index;

                if (SinkSingleQuotedExpression(expression, ref index, end))
                {
                    int startQuoted = startTransform + 1;
                    int endQuoted = index - 1;
                    if (transformExpressions == null)
                    {
                        // PERF: Almost all expressions have only one capture, so optimize for that case
                        transformExpressions = new List<ItemExpressionCapture>(1);
                    }

                    transformExpressions.Add(new ItemExpressionCapture(startQuoted, endQuoted - startQuoted, expression.Substring(startQuoted, endQuoted - startQuoted)));
                    SinkWhitespace(expression, ref index);
                    continue;
                }

                startTransform = index;
                if (TryParseFunctionTransform(expression, startTransform, ref index, end, out ItemExpressionCapture transform))
                {
                    // PERF: Almost all expressions have only one capture, so optimize for that case
                    transformExpressions ??= new List<ItemExpressionCapture>(1);
                    transformExpressions.Add(transform);

                    SinkWhitespace(expression, ref index);
                    continue;
                }

                itemVector = default;
                return false;
            }

            SinkWhitespace(expression, ref index);

            string separator = null;
            int separatorStart = -1;

            // If there's a ',', eat it and the subsequent quoted expression
            if (Sink(expression, ref index, ','))
            {
                SinkWhitespace(expression, ref index);

                if (!Sink(expression, ref index, '\''))
                {
                    itemVector = default;
                    return false;
                }

                int closingQuote = expression.IndexOf('\'', index);
                if (closingQuote == -1)
                {
                    itemVector = default;
                    return false;
                }

                separatorStart = index - startIndex;
                separator = expression.Substring(index, closingQuote - index);

                index = closingQuote + 1;
            }

            SinkWhitespace(expression, ref index);

            if (!Sink(expression, ref index, ')'))
            {
                itemVector = default;
                return false;
            }

            int length = index - startIndex;

            // Create an expression capture that encompasses the entire expression between the @( and the )
            // with the item name and any separator contained within it
            // and each transform expression contained within it (i.e. each ->XYZ)
            itemVector = new ItemExpressionCapture(
                index: startIndex,
                length,
                subExpression: Strings.WeakIntern(expression.AsSpan(startIndex, length)),
                itemType: itemName,
                separator,
                separatorStart,
                captures: transformExpressions);

            return true;
        }

        /// <summary>
        /// Given a subexpression, finds referenced item names and inserts them into the table
        /// as K=Name, V=String.Empty.
        /// </summary>
        /// <remarks>
        /// We can ignore any semicolons in the expression, since we're not itemizing it.
        /// </remarks>
        internal static void GetReferencedItemNamesAndMetadata(string expression, int start, int end, ref ItemsAndMetadataPair pair, ShredderOptions whatToShredFor)
        {
            int i = start;

            while (i < end)
            {
                int markerIndex = expression.IndexOfAny(s_itemVectorOrMetadataMarkerPrefixes, i, end - i);
                if (markerIndex < 0)
                {
                    break;
                }

                char markerPrefix = expression[markerIndex];
                i = markerIndex + 1;

                if (i >= end)
                {
                    break;
                }

                if (expression[i] != '(')
                {
                    continue;
                }

                i++;
                int restartPoint = i;

                if (markerPrefix == '@')
                {
                    // Start of a possible item list expression

                    SinkWhitespace(expression, ref i);

                    int startOfName = i;

                    if (!SinkValidName(expression, ref i, end))
                    {
                        i = restartPoint;
                        continue;
                    }

                    // '-' is a legitimate char in an item name, but we should match '->' as an arrow
                    // in '@(foo->'x')' rather than as the last char of the item name.
                    // The old regex accomplished this by being "greedy"
                    if (end > i && expression[i - 1] == '-' && expression[i] == '>')
                    {
                        i--;
                    }

                    // Grab the name boundaries, but continue to verify it's a well-formed expression
                    // before we store it.
                    int nameLength = i - startOfName;

                    SinkWhitespace(expression, ref i);

                    bool transformOrFunctionFound = true;

                    // If there's an '->' eat it and the subsequent quoted expression or transform function
                    while (Sink(expression, ref i, end, '-', '>') && transformOrFunctionFound)
                    {
                        SinkWhitespace(expression, ref i);
                        int startTransform = i;

                        bool isQuotedTransform = SinkSingleQuotedExpression(expression, ref i, end);
                        if (isQuotedTransform)
                        {
                            SinkWhitespace(expression, ref i);
                            continue;
                        }

                        if (TryParseFunctionTransform(expression, startTransform, ref i, end, out _))
                        {
                            SinkWhitespace(expression, ref i);
                            continue;
                        }

                        i = restartPoint;
                        transformOrFunctionFound = false;
                    }

                    if (!transformOrFunctionFound)
                    {
                        continue;
                    }

                    SinkWhitespace(expression, ref i);

                    // If there's a ',', eat it and the subsequent quoted expression
                    if (Sink(expression, ref i, ','))
                    {
                        SinkWhitespace(expression, ref i);

                        if (!Sink(expression, ref i, '\''))
                        {
                            i = restartPoint;
                            continue;
                        }

                        int closingQuote = expression.IndexOf('\'', i);
                        if (closingQuote == -1)
                        {
                            i = restartPoint;
                            continue;
                        }

                        // Look for metadata in the separator expression
                        // e.g., @(foo, '%(bar)') contains batchable metadata 'bar'
                        GetReferencedItemNamesAndMetadata(expression, i, closingQuote, ref pair, ShredderOptions.MetadataOutsideTransforms);

                        i = closingQuote + 1;
                    }

                    SinkWhitespace(expression, ref i);

                    if (!Sink(expression, ref i, ')'))
                    {
                        i = restartPoint;
                        continue;
                    }

                    // If we've got this far, we know the item expression was
                    // well formed, so make sure the name's in the table
                    if ((whatToShredFor & ShredderOptions.ItemTypes) != 0)
                    {
                        pair.Items ??= new HashSet<string>(MSBuildNameIgnoreCaseComparer.Default);
                        pair.Items.Add(expression.Substring(startOfName, nameLength));
                    }

                    continue;
                }

                // Start of a possible metadata expression

                if (!TryParseMetadataExpression(expression, ref i, end, out string itemName, out string metadataName))
                {
                    i = restartPoint;
                    continue;
                }

                if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0)
                {
                    string qualifiedMetadataName = itemName != null ? $"{itemName}.{metadataName}" : metadataName;
                    pair.Metadata ??= new Dictionary<string, MetadataReference>(MSBuildNameIgnoreCaseComparer.Default);
                    pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName);
                }
            }
        }

        /// <summary>
        ///  Attempts to parse a metadata expression of the form <c>%(Name)</c> or <c>%(ItemType.Name)</c>,
        ///  starting just after the <c>%(</c> has been consumed (i.e., <paramref name="i"/> points at
        ///  the first character after the opening parenthesis).
        /// </summary>
        /// <remarks>
        ///  On success, <paramref name="i"/> is left one past the closing <c>)</c>.
        ///  On failure, <paramref name="i"/> is at an indeterminate position and the caller
        ///  should restore it from a saved restart point.
        /// </remarks>
        /// <param name="expression">The expression being scanned.</param>
        /// <param name="i">Current scan position (just after <c>%(</c>). Advanced on success.</param>
        /// <param name="end">Exclusive end index of the scan range; no character at or beyond this index is read.</param>
        /// <param name="itemType">The item type if qualified; otherwise <see langword="null"/>.</param>
        /// <param name="metadataName">The metadata name.</param>
        /// <returns>
        ///  <see langword="true"/> if a valid metadata expression was parsed.
        /// </returns>
        internal static bool TryParseMetadataExpression(string expression, ref int i, int end, out string itemType, out string metadataName)
        {
            itemType = null;
            metadataName = null;

            SinkWhitespace(expression, ref i, end);

            int startOfText = i;

            if (!SinkValidName(expression, ref i, end))
            {
                return false;
            }

            string firstName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText));

            SinkWhitespace(expression, ref i, end);

            if (Sink(expression, ref i, end, '.'))
            {
                // Qualified: %(ItemType.Name)
                itemType = firstName;

                SinkWhitespace(expression, ref i, end);

                startOfText = i;

                if (!SinkValidName(expression, ref i, end))
                {
                    return false;
                }

                metadataName = Strings.WeakIntern(expression.AsSpan(startOfText, i - startOfText));

                SinkWhitespace(expression, ref i, end);
            }
            else
            {
                // Unqualified: %(Name)
                metadataName = firstName;
            }

            return Sink(expression, ref i, end, ')');
        }

        /// <summary>
        /// Returns true if a single quoted subexpression begins at the specified index
        /// and ends before the specified end index.
        /// Leaves index one past the end of the second quote.
        /// </summary>
        private static bool SinkSingleQuotedExpression(string expression, ref int i, int end)
        {
            if (!Sink(expression, ref i, '\''))
            {
                return false;
            }

            while (i < end && expression[i] != '\'')
            {
                i++;
            }

            i++;

            if (end <= i)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Scan for the closing bracket that matches the one we've already skipped;
        /// essentially, pushes and pops on a stack of parentheses to do this.
        /// Takes the expression and the index to start at.
        /// Returns the index of the matching parenthesis, or -1 if it was not found.
        /// </summary>
        private static bool SinkArgumentsInParentheses(string expression, ref int i, int end)
        {
            int nestLevel = 0;
            int length = expression.Length;
            int restartPoint;

            unsafe
            {
                fixed (char* pchar = expression)
                {
                    if (pchar[i] == '(')
                    {
                        nestLevel++;
                        i++;
                    }
                    else
                    {
                        return false;
                    }

                    // Scan for our closing ')'
                    while (i < length && i < end && nestLevel > 0)
                    {
                        char character = pchar[i];

                        if (character == '\'' || character == '`' || character == '"')
                        {
                            restartPoint = i;
                            if (!SinkUntilClosingQuote(character, expression, ref i, end))
                            {
                                i = restartPoint;
                                return false;
                            }
                        }
                        else if (character == '(')
                        {
                            nestLevel++;
                        }
                        else if (character == ')')
                        {
                            nestLevel--;
                        }

                        i++;
                    }
                }
            }

            if (nestLevel == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Skip all characters until we find the matching quote character
        /// </summary>
        private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref int i, int end)
        {
            unsafe
            {
                fixed (char* pchar = expression)
                {
                    // We have already checked the first quote
                    i++;

                    // Scan for our closing quoteChar
                    while (i < expression.Length && i < end)
                    {
                        if (pchar[i] == quoteChar)
                        {
                            return true;
                        }

                        i++;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Returns true if a item function subexpression begins at the specified index
        /// and ends before the specified end index.
        /// Leaves index one past the end of the closing paren.
        /// </summary>
        private static bool TryParseFunctionTransform(string expression, int startTransform, ref int i, int end, out ItemExpressionCapture transform)
        {
            if (SinkValidName(expression, ref i, end))
            {
                int endFunctionName = i;

                // Eat any whitespace between the function name and its arguments
                SinkWhitespace(expression, ref i);
                int startFunctionArguments = i + 1;

                if (SinkArgumentsInParentheses(expression, ref i, end))
                {
                    int endFunctionArguments = i - 1;

                    string functionName = expression.Substring(startTransform, endFunctionName - startTransform);
                    string functionArguments = null;
                    if (endFunctionArguments > startFunctionArguments)
                    {
                        functionArguments = Strings.WeakIntern(expression.AsSpan(startFunctionArguments, endFunctionArguments - startFunctionArguments));
                    }

                    transform = new ItemExpressionCapture(startTransform, i - startTransform, expression.Substring(startTransform, i - startTransform), null, null, -1, null, functionName, functionArguments);
                    return true;
                }
            }

            transform = default;
            return false;
        }

        /// <summary>
        /// Returns true if a valid name begins at the specified index.
        /// Leaves index one past the end of the name.
        /// </summary>
        /// <remarks>
        /// The accepted grammar is <c>[A-Za-z_][A-Za-z_0-9\-]*</c> (via
        /// <see cref="XmlUtilities.IsValidInitialElementNameCharacter"/> and
        /// <see cref="XmlUtilities.IsValidSubsequentElementNameCharacter"/>), which defines a valid item
        /// type or metadata name. This MUST be kept in sync with
        /// <see cref="ProjectWriter.itemTypeOrMetadataNameSpecification"/>: if the grammar used to parse
        /// item/metadata expressions diverges from the one used to write them back out, expressions could
        /// round-trip incorrectly.
        /// </remarks>
        private static bool SinkValidName(string expression, ref int i, int end)
        {
            if (end <= i || !XmlUtilities.IsValidInitialElementNameCharacter(expression[i]))
            {
                return false;
            }

            i++;

            while (end > i && XmlUtilities.IsValidSubsequentElementNameCharacter(expression[i]))
            {
                i++;
            }

            return true;
        }

        /// <summary>
        ///  Returns <see langword="true"/> if the character at the specified index is the specified char.
        ///  Leaves index one past the character.
        /// </summary>
        private static bool Sink(string expression, ref int i, char c)
            => Sink(expression, ref i, expression.Length, c);

        /// <summary>
        ///  Returns <see langword="true"/> if the character at the specified index (which must be before
        ///  <paramref name="end"/>) is the specified char. Leaves index one past the character.
        /// </summary>
        private static bool Sink(string expression, ref int i, int end, char c)
        {
            if (i < end && expression[i] == c)
            {
                i++;
                return true;
            }

            return false;
        }

        /// <summary>
        ///  Returns <see langword="true"/> if the next two characters at the specified index are the specified sequence.
        ///  Leaves index one past the second character.
        /// </summary>
        private static bool Sink(string expression, ref int i, int end, char c1, char c2)
        {
            if (i < end - 1 && expression[i] == c1 && expression[i + 1] == c2)
            {
                i += 2;
                return true;
            }

            return false;
        }

        /// <summary>
        ///  Moves past all whitespace starting at the specified index.
        ///  Returns the next index, possibly the string length.
        /// </summary>
        /// <param name="expression">The expression to process.</param>
        /// <param name="i">The start location for skipping whitespace, contains the next non-whitespace character on exit.</param>
        /// <remarks>
        ///  <see cref="char.IsWhiteSpace(char)"/> is not identical in behavior to regex's <c>\s</c> character class,
        ///  but it's extremely close, and it's what we use in conditional expressions.
        /// </remarks>
        private static void SinkWhitespace(string expression, ref int i)
            => SinkWhitespace(expression, ref i, expression.Length);

        /// <summary>
        ///  Moves past all whitespace starting at the specified index, without scanning at or beyond
        ///  <paramref name="end"/>. Returns the next index, possibly <paramref name="end"/>.
        /// </summary>
        /// <param name="expression">The expression to process.</param>
        /// <param name="i">
        ///  The start location for skipping whitespace, contains the next non-whitespace character (or <paramref name="end"/>) on exit.
        /// </param>
        /// <param name="end">Exclusive end index of the scan range.</param>
        /// <remarks>
        ///  <see cref="char.IsWhiteSpace(char)"/> is not identical in behavior to regex's <c>\s</c> character class,
        ///  but it's extremely close, and it's what we use in conditional expressions.
        /// </remarks>
        private static void SinkWhitespace(string expression, ref int i, int end)
        {
            while (i < end && char.IsWhiteSpace(expression[i]))
            {
                i++;
            }
        }

        /// <summary>
        /// Represents one substring for a single successful capture.
        /// </summary>
        internal struct ItemExpressionCapture
        {
            /// <summary>
            /// Create an Expression Capture instance
            /// Represents a sub expression, shredded from a larger expression
            /// </summary>
            public ItemExpressionCapture(int index, int length, string subExpression)
                : this(index, length, subExpression, null, null, -1, null, null, null)
            {
            }

            public ItemExpressionCapture(int index, int length, string subExpression, string itemType, string separator, int separatorStart, List<ItemExpressionCapture> captures)
                : this(index, length, subExpression, itemType, separator, separatorStart, captures, null, null)
            {
            }

            /// <summary>
            /// Create an Expression Capture instance
            /// Represents a sub expression, shredded from a larger expression
            /// </summary>
            public ItemExpressionCapture(int index, int length, string subExpression, string itemType, string separator, int separatorStart, List<ItemExpressionCapture> captures, string functionName, string functionArguments)
            {
                Index = index;
                Length = length;
                Value = subExpression;
                ItemType = itemType;
                Separator = separator;
                SeparatorStart = separatorStart;
                Captures = captures;
                FunctionName = functionName;
                FunctionArguments = functionArguments;
            }

            /// <summary>
            /// Captures within this capture
            /// </summary>
            public List<ItemExpressionCapture> Captures { get; }

            /// <summary>
            /// The position in the original string where the first character of the captured
            /// substring was found.
            /// </summary>
            public int Index { get; }

            /// <summary>
            /// The length of the captured substring.
            /// </summary>
            public int Length { get; }

            /// <summary>
            /// Gets the captured substring from the input string.
            /// </summary>
            public string Value { get; }

            /// <summary>
            /// Gets the captured itemtype.
            /// </summary>
            public string ItemType { get; }

            /// <summary>
            /// Gets the captured itemtype.
            /// </summary>
            public string Separator { get; }

            /// <summary>
            /// The starting character of the separator.
            /// </summary>
            public int SeparatorStart { get; }

            /// <summary>
            /// The function name, if any, within this expression
            /// </summary>
            public string FunctionName { get; }

            /// <summary>
            /// The function arguments, if any, within this expression
            /// </summary>
            public string FunctionArguments { get; }

            /// <summary>
            /// Gets the captured substring from the input string.
            /// </summary>
            public override string ToString()
            {
                return Value;
            }
        }
    }
}