File: Evaluation\Expander.MetadataExpander.cs
Web Access
Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Globalization;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Shared;
using Microsoft.NET.StringTools;

#nullable disable

namespace Microsoft.Build.Evaluation;

internal partial class Expander<P, I>
    where P : class, IProperty
    where I : class, IItem
{
    /// <summary>
    /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile).
    /// </summary>
    /// <remarks>
    /// This is a private nested class, exposed only through the Expander class.
    /// That allows it to hide its private methods even from Expander.
    /// </remarks>
    private static class MetadataExpander
    {
        /// <summary>
        /// Expands all embedded item metadata in the given string, using the bucketed items.
        /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile).
        /// </summary>
        /// <param name="expression">The expression containing item metadata references.</param>
        /// <param name="metadata">The metadata to be expanded.</param>
        /// <param name="options">Used to specify what to expand.</param>
        /// <param name="elementLocation">The location information for error reporting purposes.</param>
        /// <param name="loggingContext">The logging context for this operation.</param>
        /// <returns>The string with item metadata expanded in-place, escaped.</returns>
        internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options, IElementLocation elementLocation, LoggingContext loggingContext = null)
        {
            try
            {
                if ((options & ExpanderOptions.ExpandMetadata) == 0)
                {
                    return expression;
                }

                Assumed.NotNull(metadata, "Cannot expand metadata without providing metadata");

                // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail
                // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches!
                if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1)
                {
                    return expression;
                }

                string result = null;

                if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1)
                {
                    // if there are no item vectors in the string
                    // run a simpler Regex to find item metadata references
                    MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);

                    using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder();
                    RegularExpressions.ReplaceAndAppend(expression, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.ItemMetadataRegex);

                    // Don't create more strings
                    if (finalResultBuilder.Equals(expression.AsSpan()))
                    {
                        // If the final result is the same as the original expression, then just return the original expression
                        result = expression;
                    }
                    else
                    {
                        // Otherwise, convert the final result to a string
                        // and return that.
                        result = finalResultBuilder.ToString();
                    }
                }
                else
                {
                    ExpressionShredder.ReferencedItemExpressionsEnumerator itemVectorExpressionsEnumerator = ExpressionShredder.GetReferencedItemExpressions(expression);

                    // otherwise, run the more complex Regex to find item metadata references not contained in transforms
                    using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder();

                    int start = 0;

                    if (itemVectorExpressionsEnumerator.MoveNext())
                    {
                        MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);
                        ExpressionShredder.ItemExpressionCapture firstItemExpressionCapture = itemVectorExpressionsEnumerator.Current;

                        if (itemVectorExpressionsEnumerator.MoveNext())
                        {
                            // we're in the uncommon case with a partially enumerated enumerator. We need to process the first two items we enumerated and the remaining ones.
                            // Move over the expression, skipping those that have been recognized as an item vector expression
                            // Anything other than an item vector expression we want to expand bare metadata in.
                            start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture);
                            start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current);

                            while (itemVectorExpressionsEnumerator.MoveNext())
                            {
                                start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, itemVectorExpressionsEnumerator.Current);
                            }
                        }
                        else
                        {
                            // There is only one item. Check to see if we're in the common case.
                            if (firstItemExpressionCapture.Value == expression && firstItemExpressionCapture.Separator == null)
                            {
                                // The most common case is where the transform is the whole expression
                                // Also if there were no valid item vector expressions found, then go ahead and do the replacement on
                                // the whole expression (which is what Orcas did).
                                return expression;
                            }
                            else
                            {
                                start = ProcessItemExpressionCapture(expression, finalResultBuilder, matchEvaluator, start, firstItemExpressionCapture);
                            }
                        }
                    }

                    // If there's anything left after the last item vector expression
                    // then we need to metadata replace and then append that
                    if (start < expression.Length)
                    {
                        MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);
                        string subExpressionToReplaceIn = expression.Substring(start);

                        RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex);
                    }

                    if (finalResultBuilder.Equals(expression.AsSpan()))
                    {
                        // If the final result is the same as the original expression, then just return the original expression
                        result = expression;
                    }
                    else
                    {
                        // Otherwise, convert the final result to a string
                        // and return that.
                        result = finalResultBuilder.ToString();
                    }
                }

                return result;
            }
            catch (InvalidOperationException ex)
            {
                ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message);
            }

            return null;

            static int ProcessItemExpressionCapture(string expression, SpanBasedStringBuilder finalResultBuilder, MetadataMatchEvaluator matchEvaluator, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture)
            {
                // Extract the part of the expression that appears before the item vector expression
                // e.g. the ABC in ABC@(foo->'%(FullPath)')
                string subExpressionToReplaceIn = expression.Substring(start, itemExpressionCapture.Index - start);

                RegularExpressions.ReplaceAndAppend(subExpressionToReplaceIn, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex);

                // Expand any metadata that appears in the item vector expression's separator
                if (itemExpressionCapture.Separator != null)
                {
                    RegularExpressions.ReplaceAndAppend(itemExpressionCapture.Value, MetadataMatchEvaluator.ExpandSingleMetadata, matchEvaluator, -1, itemExpressionCapture.SeparatorStart, finalResultBuilder, RegularExpressions.NonTransformItemMetadataRegex);
                }
                else
                {
                    // Append the item vector expression as is
                    // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)')
                    finalResultBuilder.Append(itemExpressionCapture.Value);
                }

                // Move onto the next part of the expression that isn't an item vector expression
                start = (itemExpressionCapture.Index + itemExpressionCapture.Length);
                return start;
            }
        }
    }
}