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 Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.NET.StringTools;

namespace Microsoft.Build.Evaluation;

internal partial class Expander<P, I>
{
    /// <summary>
    ///  Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile).
    /// </summary>
    /// <remarks>
    ///  This is a private nested ref struct, exposed only through the static
    ///  <see cref="ExpandMetadataLeaveEscaped"/> entry point.
    /// </remarks>
    private readonly ref struct MetadataExpander
    {
        private readonly IMetadataTable _metadata;
        private readonly ExpanderOptions _options;
        private readonly IElementLocation _elementLocation;
        private readonly LoggingContext? _loggingContext;
        private readonly SpanBasedStringBuilder _builder;

        private MetadataExpander(
            IMetadataTable metadata,
            ExpanderOptions options,
            IElementLocation elementLocation,
            LoggingContext? loggingContext,
            SpanBasedStringBuilder builder)
        {
            _metadata = metadata;
            _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference);
            _elementLocation = elementLocation;
            _loggingContext = loggingContext;
            _builder = builder;
        }

        /// <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)
        {
            if ((options & ExpanderOptions.ExpandMetadata) == 0)
            {
                return expression;
            }

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

            // PERF NOTE: pre-scanning the string for "%(" is cheaper than a full scan.
            if (expression.IndexOf("%(", StringComparison.Ordinal) < 0)
            {
                return expression;
            }

            try
            {
                using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder();
                MetadataExpander expander = new(metadata, options, elementLocation, loggingContext, builder);

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

            return Assumed.Unreachable<string>();
        }

        private string Expand(string expression)
        {
            if (expression.IndexOf("@(", StringComparison.Ordinal) < 0)
            {
                // No item vectors in the string — scan for metadata references directly.
                ScanAndExpandMetadata(expression);
            }
            else
            {
                ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator = ExpressionShredder.GetReferencedItemExpressions(expression);

                if (!enumerator.MoveNext())
                {
                    // The string contains "@(" but no well-formed item vector expressions —
                    // scan the entire string for metadata references.
                    ScanAndExpandMetadata(expression);
                }
                else if (enumerator.Current.Value == expression
                    && enumerator.Current.Separator == null
                    && !enumerator.MoveNext())
                {
                    // The entire expression is a single item vector with no separator, so there are
                    // no gaps to expand metadata in — return the expression unchanged.
                    return expression;
                }
                else
                {
                    // Reuse the already-advanced enumerator (positioned at the first capture) to
                    // expand metadata in the gaps between item vector expressions. This avoids
                    // shredding the expression a second time.
                    ScanAndExpandMetadataInGaps(expression, ref enumerator);
                }
            }

            return _builder.Equals(expression.AsSpan())
                ? expression
                : _builder.ToString();
        }

        /// <summary>
        ///  Expands metadata in the gaps between item vector expressions and within their separators.
        /// </summary>
        /// <remarks>
        ///  The supplied enumerator must already be positioned at the first capture (i.e. a successful
        ///  <see cref="ExpressionShredder.ReferencedItemExpressionsEnumerator.MoveNext"/> has been called).
        ///  Passing the already-advanced enumerator lets the caller's shred be reused instead of
        ///  re-scanning the expression from scratch.
        /// </remarks>
        private void ScanAndExpandMetadataInGaps(string expression, ref ExpressionShredder.ReferencedItemExpressionsEnumerator enumerator)
        {
            int start = 0;

            do
            {
                start = ProcessItemExpressionCapture(expression, start, enumerator.Current);
            }
            while (enumerator.MoveNext());

            // Expand metadata in any trailing text after the last item vector expression.
            if (start < expression.Length)
            {
                ScanAndExpandMetadata(expression, start, expression.Length);
            }
        }

        private int ProcessItemExpressionCapture(string expression, int start, ExpressionShredder.ItemExpressionCapture itemExpressionCapture)
        {
            // Expand metadata in the gap before this item vector expression.
            if (itemExpressionCapture.Index > start)
            {
                ScanAndExpandMetadata(expression, start, itemExpressionCapture.Index);
            }

            // Expand metadata that appears in the item vector expression's separator.
            if (itemExpressionCapture.Separator != null)
            {
                // Append the portion before the separator verbatim, then expand within the separator portion.
                string value = itemExpressionCapture.Value;
                int separatorStart = itemExpressionCapture.SeparatorStart;

                _builder.Append(value, 0, separatorStart);
                ScanAndExpandMetadata(value, separatorStart, value.Length);
            }
            else
            {
                // Append the item vector expression as-is.
                _builder.Append(itemExpressionCapture.Value);
            }

            // Advance past this item vector expression.
            return itemExpressionCapture.Index + itemExpressionCapture.Length;
        }

        /// <inheritdoc cref="ScanAndExpandMetadata(string, int, int)" />
        private void ScanAndExpandMetadata(string input)
            => ScanAndExpandMetadata(input, 0, input.Length);

        /// <summary>
        ///  Scans the specified range of <paramref name="input"/> for item metadata references
        ///  of the form <c>%(Name)</c> or <c>%(ItemType.Name)</c>, expands them using the
        ///  provided metadata table, and appends the results to the builder.
        /// </summary>
        /// <remarks>
        ///  A valid metadata name starts with a letter or underscore, followed by zero or more
        ///  letters, digits, underscores, or hyphens: <c>[A-Za-z_][A-Za-z_0-9\-]*</c>. This grammar is
        ///  enforced by <see cref="ExpressionShredder.TryParseMetadataExpression"/> and must stay in sync
        ///  with <see cref="ProjectWriter.itemTypeOrMetadataNameSpecification"/>.
        ///  Whitespace is allowed around the parentheses and the dot separator.
        ///  If a <c>%(</c> sequence does not form a valid metadata reference, it is appended
        ///  to the output verbatim.
        /// </remarks>
        private void ScanAndExpandMetadata(string input, int startIndex, int endIndex)
        {
            int lastCopied = startIndex;

            int i = input.IndexOf("%(", startIndex, StringComparison.Ordinal);

            while (i >= 0 && i < endIndex - 1)
            {
                int pos = i + 2;

                if (!ExpressionShredder.TryParseMetadataExpression(input, ref pos, endIndex, out string itemType, out string metadataName))
                {
                    // Not a valid metadata reference — skip past '%(' and keep scanning.
                    i = input.IndexOf("%(", i + 2, StringComparison.Ordinal);
                    continue;
                }

                // Append everything before this reference.
                if (i > lastCopied)
                {
                    _builder.Append(input, lastCopied, i - lastCopied);
                }

                // Determine whether to expand this metadata reference.
                bool isBuiltInMetadata = ItemSpecModifiers.IsItemSpecModifier(metadataName);

                if ((isBuiltInMetadata && ((_options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) ||
                   (!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0)))
                {
                    string expanded = _metadata.GetEscapedValue(itemType, metadataName);

                    if ((_options & ExpanderOptions.LogOnItemMetadataSelfReference) != 0 &&
                        _loggingContext != null &&
                        !string.IsNullOrEmpty(metadataName) &&
                        _metadata is IItemTypeDefinition itemMetadata &&
                        (string.IsNullOrEmpty(itemType) || string.Equals(itemType, itemMetadata.ItemType, StringComparison.Ordinal)))
                    {
                        _loggingContext.LogComment(
                            MessageImportance.Low,
                            new BuildEventFileInfo(_elementLocation),
                            "ItemReferencingSelfInTarget",
                            itemMetadata.ItemType,
                            metadataName);
                    }

                    if (IsTruncationEnabled(_options) && expanded.Length > CharacterLimitPerExpansion)
                    {
                        expanded = TruncateString(expanded);
                    }

                    _builder.Append(expanded);
                }
                else
                {
                    _builder.Append(input, i, pos - i);
                }

                lastCopied = pos;

                // Continue scanning after this reference.
                i = input.IndexOf("%(", pos, StringComparison.Ordinal);
            }

            // Append any remaining text after the last reference.
            if (lastCopied < endIndex)
            {
                _builder.Append(input, lastCopied, endIndex - lastCopied);
            }
        }
    }
}