File: Evaluation\Expander.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.NET.StringTools;
using Microsoft.Win32;
using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods;
using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames;
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
using TaskItemFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.TaskItemFactory;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Indicates to the expander what exactly it should expand.
    /// </summary>
    [Flags]
    internal enum ExpanderOptions
    {
        /// <summary>
        /// Invalid
        /// </summary>
        Invalid = 0x0,
 
        /// <summary>
        /// Expand bare custom metadata, like %(foo), but not built-in
        /// metadata, such as %(filename) or %(identity)
        /// </summary>
        ExpandCustomMetadata = 0x1,
 
        /// <summary>
        /// Expand bare built-in metadata, such as %(filename) or %(identity)
        /// </summary>
        ExpandBuiltInMetadata = 0x2,
 
        /// <summary>
        /// Expand all bare metadata
        /// </summary>
        ExpandMetadata = ExpandCustomMetadata | ExpandBuiltInMetadata,
 
        /// <summary>
        /// Expand only properties
        /// </summary>
        ExpandProperties = 0x4,
 
        /// <summary>
        /// Expand only item list expressions
        /// </summary>
        ExpandItems = 0x8,
 
        /// <summary>
        /// If the expression is going to not be an empty string, break
        /// out early
        /// </summary>
        BreakOnNotEmpty = 0x10,
 
        /// <summary>
        /// When an error occurs expanding a property, just leave it unexpanded.
        /// </summary>
        /// <remarks>
        /// This should only be used in cases where property evaluation isn't critical, such as when attempting to log a
        /// message with a best effort expansion of a string, or when discovering partial information during lazy evaluation.
        /// </remarks>
        LeavePropertiesUnexpandedOnError = 0x20,
 
        /// <summary>
        /// When an expansion occurs, truncate it to Expander.DefaultTruncationCharacterLimit or Expander.DefaultTruncationItemLimit.
        /// </summary>
        Truncate = 0x40,
 
        /// <summary>
        /// Issues build message if item references unqualified or qualified metadata odf self - as this can lead to unintended expansion and
        ///  cross-combination of other items.
        /// More info: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-batching#item-batching-on-self-referencing-metadata
        /// </summary>
        LogOnItemMetadataSelfReference = 0x80,
 
        /// <summary>
        /// Expand only properties and then item lists
        /// </summary>
        ExpandPropertiesAndItems = ExpandProperties | ExpandItems,
 
        /// <summary>
        /// Expand only bare metadata and then properties
        /// </summary>
        ExpandPropertiesAndMetadata = ExpandMetadata | ExpandProperties,
 
        /// <summary>
        /// Expand only bare custom metadata and then properties
        /// </summary>
        ExpandPropertiesAndCustomMetadata = ExpandCustomMetadata | ExpandProperties,
 
        /// <summary>
        /// Expand bare metadata, then properties, then item expressions
        /// </summary>
        ExpandAll = ExpandMetadata | ExpandProperties | ExpandItems
    }
 
    /// <summary>
    /// Expands item/property/metadata in expressions.
    /// Encapsulates the data necessary for expansion.
    /// </summary>
    /// <remarks>
    /// Requires the caller to explicitly state what they wish to expand at the point of expansion (explicitly does not have a field for ExpanderOptions).
    /// Callers typically use a single expander in many locations, and this forces the caller to make explicit what they wish to expand at the point of expansion.
    ///
    /// Requires the caller to have previously provided the necessary material for the expansion requested.
    /// For example, if the caller requests ExpanderOptions.ExpandItems, the Expander will throw if it was not given items.
    /// </remarks>
    /// <typeparam name="P">Type of the properties used.</typeparam>
    /// <typeparam name="I">Type of the items used.</typeparam>
    internal class Expander<P, I>
        where P : class, IProperty
        where I : class, IItem
    {
        /// <summary>
        /// A helper struct wrapping a <see cref="SpanBasedStringBuilder"/> and providing file path conversion
        /// as used in e.g. property expansion.
        /// </summary>
        /// <remarks>
        /// If exactly one value is added and no concatenation takes places, this value is returned without
        /// conversion. In other cases values are stringified and attempted to be interpreted as file paths
        /// before concatenation.
        /// </remarks>
        private struct SpanBasedConcatenator : IDisposable
        {
            /// <summary>
            /// The backing <see cref="SpanBasedStringBuilder"/>, null until the second value is added.
            /// </summary>
            private SpanBasedStringBuilder _builder;
 
            /// <summary>
            /// The first value added to the concatenator. Tracked in its own field so it can be returned
            /// without conversion if no concatenation takes place.
            /// </summary>
            private object _firstObject;
 
            /// <summary>
            /// The first value added to the concatenator if it is a span. Tracked in its own field so the
            /// <see cref="SpanBasedStringBuilder"/> functionality doesn't have to be invoked if no concatenation
            /// takes place.
            /// </summary>
            private ReadOnlyMemory<char> _firstSpan;
 
            /// <summary>
            /// True if this instance is already disposed.
            /// </summary>
            private bool _disposed;
 
            /// <summary>
            /// Adds an object to be concatenated.
            /// </summary>
            public void Add(object obj)
            {
                CheckDisposed();
                FlushFirstValueIfNeeded();
                if (_builder != null)
                {
                    _builder.Append(FileUtilities.MaybeAdjustFilePath(obj.ToString()));
                }
                else
                {
                    _firstObject = obj;
                }
            }
 
            /// <summary>
            /// Adds a span to be concatenated.
            /// </summary>
            public void Add(ReadOnlyMemory<char> span)
            {
                CheckDisposed();
                FlushFirstValueIfNeeded();
                if (_builder != null)
                {
                    _builder.Append(FileUtilities.MaybeAdjustFilePath(span));
                }
                else
                {
                    _firstSpan = span;
                }
            }
 
            /// <summary>
            /// Returns the result of the concatenation.
            /// </summary>
            /// <returns>
            /// If only one value has been added and it is not a string, it is returned unchanged.
            /// In all other cases (no value, one string value, multiple values) the result is a
            /// concatenation of the string representation of the values, each additionally subjected
            /// to file path adjustment.
            /// </returns>
            public readonly object GetResult()
            {
                CheckDisposed();
                if (_firstObject != null)
                {
                    return (_firstObject is string stringValue) ? FileUtilities.MaybeAdjustFilePath(stringValue) : _firstObject;
                }
                return _firstSpan.IsEmpty
                    ? _builder?.ToString() ?? string.Empty
                    : FileUtilities.MaybeAdjustFilePath(_firstSpan).ToString();
            }
 
            /// <summary>
            /// Disposes of the struct by delegating the call to the underlying <see cref="SpanBasedStringBuilder"/>.
            /// </summary>
            public void Dispose()
            {
                CheckDisposed();
                _builder?.Dispose();
                _disposed = true;
            }
 
            /// <summary>
            /// Throws <see cref="ObjectDisposedException"/> if this concatenator is already disposed.
            /// </summary>
            private readonly void CheckDisposed() =>
                ErrorUtilities.VerifyThrowObjectDisposed(!_disposed, nameof(SpanBasedConcatenator));
 
            /// <summary>
            /// Lazily initializes <see cref="_builder"/> and populates it with the first value
            /// when the second value is being added.
            /// </summary>
            private void FlushFirstValueIfNeeded()
            {
                if (_firstObject != null)
                {
                    _builder = Strings.GetSpanBasedStringBuilder();
                    _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstObject.ToString()));
                    _firstObject = null;
                }
                else if (!_firstSpan.IsEmpty)
                {
                    _builder = Strings.GetSpanBasedStringBuilder();
#if FEATURE_SPAN
                    _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan));
#else
                    _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan.ToString()));
#endif
                    _firstSpan = new ReadOnlyMemory<char>();
                }
            }
        }
 
        /// <summary>
        /// A limit for truncating string expansions within an evaluated Condition. Properties, item metadata, or item groups will be truncated to N characters such as 'N...'.
        /// Enabled by ExpanderOptions.Truncate.
        /// </summary>
        private const int CharacterLimitPerExpansion = 1024;
        /// <summary>
        /// A limit for truncating string expansions for item groups within an evaluated Condition. N items will be evaluated such as 'A;B;C;...'.
        /// Enabled by ExpanderOptions.Truncate.
        /// </summary>
        private const int ItemLimitPerExpansion = 3;
        private static readonly char[] s_singleQuoteChar = { '\'' };
        private static readonly char[] s_backtickChar = { '`' };
        private static readonly char[] s_doubleQuoteChar = { '"' };
 
        /// <summary>
        /// Those characters which indicate that an expression may contain expandable
        /// expressions.
        /// </summary>
        private static char[] s_expandableChars = { '$', '%', '@' };
 
        /// <summary>
        /// The CultureInfo from the invariant culture. Used to avoid allocations for
        /// performing IndexOf etc.
        /// </summary>
        private static CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo;
 
        /// <summary>
        /// Properties to draw on for expansion.
        /// </summary>
        private IPropertyProvider<P> _properties;
 
        /// <summary>
        /// Items to draw on for expansion.
        /// </summary>
        private IItemProvider<I> _items;
 
        /// <summary>
        /// Metadata to draw on for expansion.
        /// </summary>
        private IMetadataTable _metadata;
 
        /// <summary>
        /// Set of properties which are null during expansion.
        /// </summary>
        private PropertiesUseTracker _propertiesUseTracker;
 
        private readonly IFileSystem _fileSystem;
 
        private readonly LoggingContext _loggingContext;
 
        /// <summary>
        /// Non-null if the expander was constructed for evaluation.
        /// </summary>
        internal EvaluationContext EvaluationContext { get; }
 
        private Expander(IPropertyProvider<P> properties, LoggingContext loggingContext)
        {
            _properties = properties;
            _propertiesUseTracker = new PropertiesUseTracker(loggingContext);
            _loggingContext = loggingContext;
        }
 
        /// <summary>
        /// Creates an expander passing it some properties to use.
        /// Properties may be null.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IFileSystem fileSystem, LoggingContext loggingContext)
            : this(properties, loggingContext)
        {
            _fileSystem = fileSystem;
        }
 
        /// <summary>
        /// Creates an expander passing it some properties to use.
        /// Properties may be null.
        ///
        /// Used for tests and for ToolsetReader - that operates agnostic on the project
        ///   - so no logging context is passed, and no BuildCheck check will be executed.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IFileSystem fileSystem)
        : this(properties, fileSystem, null)
        { }
 
        /// <summary>
        /// Creates an expander passing it some properties to use and the evaluation context.
        /// Properties may be null.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, EvaluationContext evaluationContext,
            LoggingContext loggingContext)
            : this(properties, loggingContext)
        {
            _fileSystem = evaluationContext.FileSystem;
            EvaluationContext = evaluationContext;
        }
 
        /// <summary>
        /// Creates an expander passing it some properties and items to use.
        /// Either or both may be null.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IItemProvider<I> items, IFileSystem fileSystem, LoggingContext loggingContext)
            : this(properties, fileSystem, loggingContext)
        {
            _items = items;
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="Expander{P, I}"/> class.
        /// Creates an expander passing it some properties and items to use, and the evaluation context.
        /// Either or both may be null.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IItemProvider<I> items, EvaluationContext evaluationContext, LoggingContext loggingContext)
            : this(properties, evaluationContext, loggingContext)
        {
            _items = items;
        }
 
        /// <summary>
        /// Creates an expander passing it some properties, items, and/or metadata to use.
        /// Any or all may be null.
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IItemProvider<I> items, IMetadataTable metadata, IFileSystem fileSystem, LoggingContext loggingContext)
            : this(properties, items, fileSystem, loggingContext)
        {
            _metadata = metadata;
        }
 
        /// <summary>
        /// Creates an expander passing it some properties, items, and/or metadata to use.
        /// Any or all may be null.
        ///
        /// This is for the purpose of evaluations through API calls, that might not be able to pass the logging context
        ///  - BuildCheck checking won't be executed for those.
        /// (for one of the calls we can actually pass IDataConsumingContext - as we have logging service and project)
        ///
        /// </summary>
        internal Expander(IPropertyProvider<P> properties, IItemProvider<I> items, IMetadataTable metadata, IFileSystem fileSystem)
            : this(properties, items, fileSystem, null)
        {
            _metadata = metadata;
        }
 
        private Expander(
            IPropertyProvider<P> properties,
            IItemProvider<I> items,
            IMetadataTable metadata,
            IFileSystem fileSystem,
            EvaluationContext evaluationContext,
            LoggingContext loggingContext)
            : this(properties, items, metadata, fileSystem, loggingContext)
        {
            EvaluationContext = evaluationContext;
        }
 
        /// <summary>
        /// Recreates the expander with passed in logging context
        /// </summary>
        /// <param name="loggingContext"></param>
        /// <returns></returns>
        internal Expander<P, I> WithLoggingContext(LoggingContext loggingContext)
        {
            return new Expander<P, I>(_properties, _items, _metadata, _fileSystem, EvaluationContext, loggingContext);
        }
 
        /// <summary>
        /// Accessor for the metadata.
        /// Set temporarily during item metadata evaluation.
        /// </summary>
        internal IMetadataTable Metadata
        {
            get { return _metadata; }
            set { _metadata = value; }
        }
 
        /// <summary>
        /// If a property is expanded but evaluates to null then it is considered to be un-initialized.
        /// We want to keep track of these properties so that we can warn if the property gets set later on.
        /// </summary>
        internal PropertiesUseTracker PropertiesUseTracker
        {
            get { return _propertiesUseTracker; }
            set { _propertiesUseTracker = value; }
        }
 
        /// <summary>
        /// Tests to see if the expression may contain expandable expressions, i.e.
        /// contains $, % or @.
        /// </summary>
        internal static bool ExpressionMayContainExpandableExpressions(string expression)
        {
            return expression.IndexOfAny(s_expandableChars) > -1;
        }
 
        /// <summary>
        /// Returns true if the expression contains an item vector pattern, else returns false.
        /// Used to flag use of item expressions where they are illegal.
        /// </summary>
        internal static bool ExpressionContainsItemVector(string expression)
        {
            List<ExpressionShredder.ItemExpressionCapture> transforms = ExpressionShredder.GetReferencedItemExpressions(expression);
 
            return transforms != null;
        }
 
        /// <summary>
        /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options.
        /// This is the standard form. Before using the expanded value, it must be unescaped, and this does that for you.
        ///
        /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
        /// </summary>
        internal string ExpandIntoStringAndUnescape(string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            string result = ExpandIntoStringLeaveEscaped(expression, options, elementLocation);
 
            return (result == null) ? null : EscapingUtilities.UnescapeAll(result);
        }
 
        /// <summary>
        /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options.
        /// Use this form when the result is going to be processed further, for example by matching against the file system,
        /// so literals must be distinguished, and you promise to unescape after that.
        ///
        /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
        /// </summary>
        internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            if (expression.Length == 0)
            {
                return String.Empty;
            }
 
            ErrorUtilities.VerifyThrowInternalNull(elementLocation);
 
            string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation, _loggingContext);
            result = PropertyExpander<P>.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem);
            result = ItemExpander.ExpandItemVectorsIntoString<I>(this, result, _items, options, elementLocation);
            result = FileUtilities.MaybeAdjustFilePath(result);
 
            return result;
        }
 
        /// <summary>
        /// Used only for unit tests. Expands the property expression (including any metadata expressions) and returns
        /// the result typed (i.e. not converted into a string if the result is a function return).
        /// </summary>
        internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            if (expression.Length == 0)
            {
                return String.Empty;
            }
 
            ErrorUtilities.VerifyThrowInternalNull(elementLocation);
 
            string metaExpanded = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation);
            return PropertyExpander<P>.ExpandPropertiesLeaveTypedAndEscaped(metaExpanded, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem);
        }
 
        /// <summary>
        /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options,
        /// then splits on semi-colons into a list of strings.
        /// Use this form when the result is going to be processed further, for example by matching against the file system,
        /// so literals must be distinguished, and you promise to unescape after that.
        /// </summary>
        internal SemiColonTokenizer ExpandIntoStringListLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            ErrorUtilities.VerifyThrow((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported");
 
            return ExpressionShredder.SplitSemiColonSeparatedList(ExpandIntoStringLeaveEscaped(expression, options, elementLocation));
        }
 
        /// <summary>
        /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options
        /// and produces a list of TaskItems.
        /// If the expression is empty, returns an empty list.
        /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
        /// </summary>
        internal IList<TaskItem> ExpandIntoTaskItemsLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            return ExpandIntoItemsLeaveEscaped(expression, (IItemFactory<I, TaskItem>)TaskItemFactory.Instance, options, elementLocation);
        }
 
        /// <summary>
        /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options
        /// and produces a list of items of the type for which it was specialized.
        /// If the expression is empty, returns an empty list.
        /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
        ///
        /// Use this form when the result is going to be processed further, for example by matching against the file system,
        /// so literals must be distinguished, and you promise to unescape after that.
        /// </summary>
        /// <typeparam name="T">Type of items to return.</typeparam>
        internal IList<T> ExpandIntoItemsLeaveEscaped<T>(string expression, IItemFactory<I, T> itemFactory, ExpanderOptions options, IElementLocation elementLocation)
            where T : class, IItem
        {
            if (expression.Length == 0)
            {
                return Array.Empty<T>();
            }
 
            ErrorUtilities.VerifyThrowInternalNull(elementLocation);
 
            expression = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options, elementLocation);
            expression = PropertyExpander<P>.ExpandPropertiesLeaveEscaped(expression, _properties, options, elementLocation, _propertiesUseTracker, _fileSystem);
            expression = FileUtilities.MaybeAdjustFilePath(expression);
 
            List<T> result = new List<T>();
 
            if (expression.Length == 0)
            {
                return result;
            }
 
            var splits = ExpressionShredder.SplitSemiColonSeparatedList(expression);
            foreach (string split in splits)
            {
                bool isTransformExpression;
                IList<T> itemsToAdd = ItemExpander.ExpandSingleItemVectorExpressionIntoItems<I, T>(this, split, _items, itemFactory, options, false /* do not include null items */, out isTransformExpression, elementLocation);
 
                if ((itemsToAdd == null /* broke out early non empty */ || (itemsToAdd.Count > 0)) && (options & ExpanderOptions.BreakOnNotEmpty) != 0)
                {
                    return null;
                }
 
                if (itemsToAdd != null)
                {
                    result.AddRange(itemsToAdd);
                }
                else
                {
                    // The expression is not of the form @(itemName).  Therefore, just
                    // treat it as a string, and create a new item from that string.
                    T itemToAdd = itemFactory.CreateItem(split, elementLocation.File);
 
                    result.Add(itemToAdd);
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// This is a specialized method for the use of TargetUpToDateChecker and Evaluator.EvaluateItemXml only.
        ///
        /// Extracts the items in the given SINGLE item vector.
        /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list.
        ///
        /// If there is in fact more than one vector in the expression, throws InvalidProjectFileException.
        ///
        /// If there are no item expressions in the expression (for example a literal "foo.cpp"), returns null.
        /// If expression expands to no items, returns an empty list.
        /// If item expansion is not allowed by the provided options, returns null.
        /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
        ///
        /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because
        /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means
        /// that the length of the returned list is always the same as the length of the referenced item list in the input string.
        /// That's important for any correlation the caller wants to do.
        ///
        /// If expression was a transform, 'isTransformExpression' is true, otherwise false.
        ///
        /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not
        /// have an item type set on it, it will be given the item type of the item vector to use.
        /// </summary>
        /// <typeparam name="T">Type of the items that should be returned.</typeparam>
        internal IList<T> ExpandSingleItemVectorExpressionIntoItems<T>(string expression, IItemFactory<I, T> itemFactory, ExpanderOptions options, bool includeNullItems, out bool isTransformExpression, IElementLocation elementLocation)
            where T : class, IItem
        {
            if (expression.Length == 0)
            {
                isTransformExpression = false;
                return Array.Empty<T>();
            }
 
            ErrorUtilities.VerifyThrowInternalNull(elementLocation);
 
            return ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, expression, _items, itemFactory, options, includeNullItems, out isTransformExpression, elementLocation);
        }
 
        internal static ExpressionShredder.ItemExpressionCapture ExpandSingleItemVectorExpressionIntoExpressionCapture(
                string expression, ExpanderOptions options, IElementLocation elementLocation)
        {
            return ItemExpander.ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation);
        }
 
        internal IList<T> ExpandExpressionCaptureIntoItems<S, T>(
                ExpressionShredder.ItemExpressionCapture expressionCapture, IItemProvider<S> items, IItemFactory<S, T> itemFactory,
                ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation)
            where S : class, IItem
            where T : class, IItem
        {
            return ItemExpander.ExpandExpressionCaptureIntoItems<S, T>(expressionCapture, this, items, itemFactory, options,
                includeNullEntries, out isTransformExpression, elementLocation);
        }
 
        internal bool ExpandExpressionCapture(
            ExpressionShredder.ItemExpressionCapture expressionCapture,
            IElementLocation elementLocation,
            ExpanderOptions options,
            bool includeNullEntries,
            out bool isTransformExpression,
            out List<KeyValuePair<string, I>> itemsFromCapture)
        {
            return ItemExpander.ExpandExpressionCapture(this, expressionCapture, _items, elementLocation, options, includeNullEntries, out isTransformExpression, out itemsFromCapture);
        }
 
        /// <summary>
        /// Returns true if the supplied string contains a valid property name.
        /// </summary>
        private static bool IsValidPropertyName(string propertyName)
        {
            if (propertyName.Length == 0 || !XmlUtilities.IsValidInitialElementNameCharacter(propertyName[0]))
            {
                return false;
            }
 
            for (int n = 1; n < propertyName.Length; n++)
            {
                if (!XmlUtilities.IsValidSubsequentElementNameCharacter(propertyName[n]))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Returns true if ExpanderOptions.Truncate is set and EscapeHatches.DoNotTruncateConditions is not set.
        /// </summary>
        private static bool IsTruncationEnabled(ExpanderOptions options)
        {
            return (options & ExpanderOptions.Truncate) != 0 && !Traits.Instance.EscapeHatches.DoNotTruncateConditions;
        }
 
        /// <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.
        /// Also returns flags to indicate if a propertyfunction or registry property is likely
        /// to be found in the expression.
        /// </summary>
        private static int ScanForClosingParenthesis(string expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction)
        {
            int nestLevel = 1;
            int length = expression.Length;
 
            potentialPropertyFunction = false;
            potentialRegistryFunction = false;
 
            // Scan for our closing ')'
            while (index < length && nestLevel > 0)
            {
                char character = expression[index];
                switch (character)
                {
                    case '\'':
                    case '`':
                    case '"':
                        {
                            index++;
                            index = ScanForClosingQuote(character, expression, index);
 
                            if (index < 0)
                            {
                                return -1;
                            }
                            break;
                        }
                    case '(':
                        {
                            nestLevel++;
                            break;
                        }
                    case ')':
                        {
                            nestLevel--;
                            break;
                        }
                    case '.':
                    case '[':
                    case '$':
                        {
                            potentialPropertyFunction = true;
                            break;
                        }
                    case ':':
                        {
                            potentialRegistryFunction = true;
                            break;
                        }
                }
 
                index++;
            }
 
            // We will have parsed past the ')', so step back one character
            index--;
 
            return (nestLevel == 0) ? index : -1;
        }
 
        /// <summary>
        /// Skip all characters until we find the matching quote character.
        /// </summary>
        private static int ScanForClosingQuote(char quoteChar, string expression, int index)
        {
            // Scan for our closing quoteChar
            return expression.IndexOf(quoteChar, index);
        }
 
        /// <summary>
        /// Add the argument in the StringBuilder to the arguments list, handling nulls
        /// appropriately.
        /// </summary>
        private static void AddArgument(List<string> arguments, SpanBasedStringBuilder argumentBuilder)
        {
            // we reached the end of an argument, add the builder's final result
            // to our arguments.
            argumentBuilder.Trim();
            string argValue = argumentBuilder.ToString();
 
            // We support passing of null through the argument constant value null
            if (String.Equals("null", argValue, StringComparison.OrdinalIgnoreCase))
            {
                arguments.Add(null);
            }
            else
            {
                if (argValue.Length > 0)
                {
                    if (argValue[0] == '\'' && argValue[argValue.Length - 1] == '\'')
                    {
                        arguments.Add(argValue.Trim(s_singleQuoteChar));
                    }
                    else if (argValue[0] == '`' && argValue[argValue.Length - 1] == '`')
                    {
                        arguments.Add(argValue.Trim(s_backtickChar));
                    }
                    else if (argValue[0] == '"' && argValue[argValue.Length - 1] == '"')
                    {
                        arguments.Add(argValue.Trim(s_doubleQuoteChar));
                    }
                    else
                    {
                        arguments.Add(argValue);
                    }
                }
                else
                {
                    arguments.Add(argValue);
                }
            }
        }
 
        /// <summary>
        /// Extract the first level of arguments from the content.
        /// Splits the content passed in at commas.
        /// Returns an array of unexpanded arguments.
        /// If there are no arguments, returns an empty array.
        /// </summary>
        private static string[] ExtractFunctionArguments(IElementLocation elementLocation, string expressionFunction, string argumentsString)
        {
            int argumentsContentLength = argumentsString.Length;
 
            List<string> arguments = new List<string>();
 
            using SpanBasedStringBuilder argumentBuilder = Strings.GetSpanBasedStringBuilder();
            int? argumentStartIndex = null;
 
            // We iterate over the string in the for loop below. When we find an argument, instead of adding it to the argument
            // builder one-character-at-a-time, we remember the start index and then call this function when we find the end of
            // the argument. This appends the entire {start, end} span to the builder in one call.
            void FlushCurrentArgumentToArgumentBuilder(int argumentEndIndex)
            {
                if (argumentStartIndex.HasValue)
                {
                    argumentBuilder.Append(argumentsString, argumentStartIndex.Value, argumentEndIndex - argumentStartIndex.Value);
                    argumentStartIndex = null;
                }
            }
 
            // Iterate over the contents of the arguments extracting the
            // the individual arguments as we go
            for (int n = 0; n < argumentsContentLength; n++)
            {
                // We found a property expression.. skip over all of it.
                if ((n < argumentsContentLength - 1) && (argumentsString[n] == '$' && argumentsString[n + 1] == '('))
                {
                    int nestedPropertyStart = n;
                    n += 2; // skip over the opening '$('
 
                    // Scan for the matching closing bracket, skipping any nested ones
                    n = ScanForClosingParenthesis(argumentsString, n, out _, out _);
 
                    if (n == -1)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis"));
                    }
 
                    FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: nestedPropertyStart);
                    argumentBuilder.Append(argumentsString, nestedPropertyStart, (n - nestedPropertyStart) + 1);
                }
                else if (argumentsString[n] == '`' || argumentsString[n] == '"' || argumentsString[n] == '\'')
                {
                    int quoteStart = n;
                    n++; // skip over the opening quote
 
                    n = ScanForClosingQuote(argumentsString[quoteStart], argumentsString, n);
 
                    if (n == -1)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedQuote"));
                    }
 
                    FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: quoteStart);
                    argumentBuilder.Append(argumentsString, quoteStart, (n - quoteStart) + 1);
                }
                else if (argumentsString[n] == ',')
                {
                    FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: n);
 
                    // We have reached the end of the current argument, go ahead and add it
                    // to our list
                    AddArgument(arguments, argumentBuilder);
 
                    // Clear out the argument builder ready for the next argument
                    argumentBuilder.Clear();
                }
                else
                {
                    argumentStartIndex ??= n;
                }
            }
 
            // We reached the end of the string but we may have seen the start but not the end of the last (or only) argument so flush it now.
            FlushCurrentArgumentToArgumentBuilder(argumentEndIndex: argumentsContentLength);
 
            // This will either be the one and only argument, or the last one
            // so add it to our list
            AddArgument(arguments, argumentBuilder);
 
            return arguments.ToArray();
        }
 
        /// <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;
                    }
 
                    ErrorUtilities.VerifyThrow(metadata != null, "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);
                        result = RegularExpressions.ItemMetadataPattern.Value.Replace(expression, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata));
                    }
                    else
                    {
                        List<ExpressionShredder.ItemExpressionCapture> itemVectorExpressions = ExpressionShredder.GetReferencedItemExpressions(expression);
 
                        // 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).
                        if (itemVectorExpressions?.Count == 1 && itemVectorExpressions[0].Value == expression && itemVectorExpressions[0].Separator == null)
                        {
                            return expression;
                        }
 
                        // otherwise, run the more complex Regex to find item metadata references not contained in transforms
                        using SpanBasedStringBuilder finalResultBuilder = Strings.GetSpanBasedStringBuilder();
 
                        int start = 0;
                        MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options, elementLocation, loggingContext);
 
                        if (itemVectorExpressions != null)
                        {
                            // 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.
                            for (int n = 0; n < itemVectorExpressions.Count; n++)
                            {
                                string vectorExpression = itemVectorExpressions[n].Value;
 
                                // 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, itemVectorExpressions[n].Index - start);
                                string replacementResult = RegularExpressions.NonTransformItemMetadataPattern.Value.Replace(subExpressionToReplaceIn, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata));
 
                                // Append the metadata replacement
                                finalResultBuilder.Append(replacementResult);
 
                                // Expand any metadata that appears in the item vector expression's separator
                                if (itemVectorExpressions[n].Separator != null)
                                {
                                    vectorExpression = RegularExpressions.NonTransformItemMetadataPattern.Value.Replace(itemVectorExpressions[n].Value, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata), -1, itemVectorExpressions[n].SeparatorStart);
                                }
 
                                // Append the item vector expression as is
                                // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)')
                                finalResultBuilder.Append(vectorExpression);
 
                                // Move onto the next part of the expression that isn't an item vector expression
                                start = (itemVectorExpressions[n].Index + itemVectorExpressions[n].Length);
                            }
                        }
 
                        // 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)
                        {
                            string subExpressionToReplaceIn = expression.Substring(start);
                            string replacementResult = RegularExpressions.NonTransformItemMetadataPattern.Value.Replace(subExpressionToReplaceIn, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata));
 
                            finalResultBuilder.Append(replacementResult);
                        }
 
                        result = finalResultBuilder.ToString();
                    }
 
                    // Don't create more strings
                    if (String.Equals(result, expression, StringComparison.Ordinal))
                    {
                        result = expression;
                    }
 
                    return result;
                }
                catch (InvalidOperationException ex)
                {
                    ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotExpandItemMetadata", expression, ex.Message);
                }
 
                return null;
            }
 
            /// <summary>
            /// A functor that returns the value of the metadata in the match
            /// that is contained in the metadata dictionary it was created with.
            /// </summary>
            private class MetadataMatchEvaluator
            {
                /// <summary>
                /// Source of the metadata.
                /// </summary>
                private IMetadataTable _metadata;
 
                /// <summary>
                /// Whether to expand built-in metadata, custom metadata, or both kinds.
                /// </summary>
                private ExpanderOptions _options;
 
                private IElementLocation _elementLocation;
 
                private LoggingContext _loggingContext;
 
                /// <summary>
                /// Constructor taking a source of metadata.
                /// </summary>
                internal MetadataMatchEvaluator(
                    IMetadataTable metadata,
                    ExpanderOptions options,
                    IElementLocation elementLocation,
                    LoggingContext loggingContext)
                {
                    _metadata = metadata;
                    _options = options & (ExpanderOptions.ExpandMetadata | ExpanderOptions.Truncate | ExpanderOptions.LogOnItemMetadataSelfReference);
                    _elementLocation = elementLocation;
                    _loggingContext = loggingContext;
 
                    ErrorUtilities.VerifyThrow(options != ExpanderOptions.Invalid, "Must be expanding metadata of some kind");
                }
 
                /// <summary>
                /// Expands a single item metadata, which may be qualified with an item type.
                /// </summary>
                internal string ExpandSingleMetadata(Match itemMetadataMatch)
                {
                    ErrorUtilities.VerifyThrow(itemMetadataMatch.Success, "Need a valid item metadata.");
 
                    string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value;
                    string itemType = null;
 
                    // check if the metadata is qualified with the item type
                    if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0)
                    {
                        itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value;
                    }
 
                    // look up the metadata - we may not have a value for it
                    string metadataValue = itemMetadataMatch.Value;
 
                    bool isBuiltInMetadata = FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName);
 
                    if (
                        (isBuiltInMetadata && ((_options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) ||
                       (!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0)))
                    {
                        metadataValue = _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) && metadataValue.Length > CharacterLimitPerExpansion)
                        {
                            metadataValue = metadataValue.Substring(0, CharacterLimitPerExpansion - 3) + "...";
                        }
                    }
 
                    return metadataValue;
                }
            }
        }
 
        /// <summary>
        /// Expands property expressions, like $(Configuration) and $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation).
        /// </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>
        /// <typeparam name="T">Type of the properties used to expand the expression.</typeparam>
        private static class PropertyExpander<T>
            where T : class, IProperty
        {
            /// <summary>
            /// This method takes a string which may contain any number of
            /// "$(propertyname)" tags in it.  It replaces all those tags with
            /// the actual property values, and returns a new string.  For example,
            ///
            ///     string processedString =
            ///         propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo).");
            ///
            /// This code might produce:
            ///
            ///     processedString = "Value of NoLogo is true."
            ///
            /// If the sourceString contains an embedded property which doesn't
            /// have a value, then we replace that tag with an empty string.
            ///
            /// This method leaves the result escaped.  Callers may need to unescape on their own as appropriate.
            /// </summary>
            internal static string ExpandPropertiesLeaveEscaped(
                string expression,
                IPropertyProvider<T> properties,
                ExpanderOptions options,
                IElementLocation elementLocation,
                PropertiesUseTracker propertiesUseTracker,
                IFileSystem fileSystem)
            {
                return
                    ConvertToString(
                        ExpandPropertiesLeaveTypedAndEscaped(
                            expression,
                            properties,
                            options,
                            elementLocation,
                            propertiesUseTracker,
                            fileSystem));
            }
 
            /// <summary>
            /// This method takes a string which may contain any number of
            /// "$(propertyname)" tags in it.  It replaces all those tags with
            /// the actual property values, and returns a new string.  For example,
            ///
            ///     string processedString =
            ///         propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo).");
            ///
            /// This code might produce:
            ///
            ///     processedString = "Value of NoLogo is true."
            ///
            /// If the sourceString contains an embedded property which doesn't
            /// have a value, then we replace that tag with an empty string.
            ///
            /// This method leaves the result typed and escaped.  Callers may need to convert to string, and unescape on their own as appropriate.
            /// </summary>
            internal static object ExpandPropertiesLeaveTypedAndEscaped(
                string expression,
                IPropertyProvider<T> properties,
                ExpanderOptions options,
                IElementLocation elementLocation,
                PropertiesUseTracker propertiesUseTracker,
                IFileSystem fileSystem)
            {
                if (((options & ExpanderOptions.ExpandProperties) == 0) || String.IsNullOrEmpty(expression))
                {
                    return expression;
                }
 
                ErrorUtilities.VerifyThrow(properties != null, "Cannot expand properties without providing properties");
 
                // These are also zero-based indices into the expression, but
                // these tell us where the current property tag begins and ends.
                int propertyStartIndex, propertyEndIndex;
 
                // If there are no substitutions, then just return the string.
                propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", CompareOptions.Ordinal);
                if (propertyStartIndex == -1)
                {
                    return expression;
                }
 
                // We will build our set of results as object components
                // so that we can either maintain the object's type in the event
                // that we have a single component, or convert to a string
                // if concatenation is required.
                using Expander<P, I>.SpanBasedConcatenator results = new Expander<P, I>.SpanBasedConcatenator();
 
                // The sourceIndex is the zero-based index into the expression,
                // where we've essentially read up to and copied into the target string.
                int sourceIndex = 0;
 
                // Search for "$(" in the expression.  Loop until we don't find it
                // any more.
                while (propertyStartIndex != -1)
                {
                    // Append the result with the portion of the expression up to
                    // (but not including) the "$(", and advance the sourceIndex pointer.
                    if (propertyStartIndex - sourceIndex > 0)
                    {
                        results.Add(expression.AsMemory(sourceIndex, propertyStartIndex - sourceIndex));
                    }
 
                    // Following the "$(" we need to locate the matching ')'
                    // Scan for the matching closing bracket, skipping any nested ones
                    // This is a very complete, fast validation of parenthesis matching including for nested
                    // function calls.
                    propertyEndIndex = ScanForClosingParenthesis(expression, propertyStartIndex + 2, out bool tryExtractPropertyFunction, out bool tryExtractRegistryFunction);
 
                    if (propertyEndIndex == -1)
                    {
                        // If we didn't find the closing parenthesis, that means this
                        // isn't really a well-formed property tag.  Just literally
                        // copy the remainder of the expression (starting with the "$("
                        // that we found) into the result, and quit.
                        results.Add(expression.AsMemory(propertyStartIndex, expression.Length - propertyStartIndex));
                        sourceIndex = expression.Length;
                    }
                    else
                    {
                        // Aha, we found the closing parenthesis.  All the stuff in
                        // between the "$(" and the ")" constitutes the property body.
                        // Note: Current propertyStartIndex points to the "$", and
                        // propertyEndIndex points to the ")".  That's why we have to
                        // add 2 for the start of the substring, and subtract 2 for
                        // the length.
                        string propertyBody;
 
                        // A property value of null will indicate that we're calling a static function on a type
                        object propertyValue;
 
                        // Compat: $() should return String.Empty
                        if (propertyStartIndex + 2 == propertyEndIndex)
                        {
                            propertyValue = String.Empty;
                        }
                        else if ((expression.Length - (propertyStartIndex + 2)) > 9 && tryExtractRegistryFunction && s_invariantCompareInfo.IndexOf(expression, "Registry:", propertyStartIndex + 2, 9, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2)
                        {
                            propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2);
 
                            // If the property body starts with any of our special objects, then deal with them
                            // This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)
                            propertyValue = ExpandRegistryValue(propertyBody, elementLocation); // This func returns an empty string if not on Windows
                        }
 
                        // Compat hack: as a special case, $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) should return String.Empty
                        // In this case, tryExtractRegistryFunction will be false. Note that very few properties are exactly 77 chars, so this check should be fast.
                        else if ((propertyEndIndex - (propertyStartIndex + 2)) == 77 && s_invariantCompareInfo.IndexOf(expression, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory", propertyStartIndex + 2, 77, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2)
                        {
                            propertyValue = String.Empty;
                        }
 
                        // Compat hack: WebProjects may have an import with a condition like:
                        //       Condition=" '$(Solutions.VSVersion)' == '8.0'"
                        // These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions.
                        // Be compatible by returning an empty string here.
                        else if ((propertyEndIndex - (propertyStartIndex + 2)) == 19 && String.Equals(expression, "$(Solutions.VSVersion)", StringComparison.Ordinal))
                        {
                            propertyValue = String.Empty;
                        }
                        else if (tryExtractPropertyFunction)
                        {
                            propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2);
 
                            // This is likely to be a function expression
                            propertyValue = ExpandPropertyBody(
                                propertyBody,
                                null,
                                properties,
                                options,
                                elementLocation,
                                propertiesUseTracker,
                                fileSystem);
                        }
                        else // This is a regular property
                        {
                            propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, propertiesUseTracker);
                        }
 
                        if (propertyValue != null)
                        {
                            if (IsTruncationEnabled(options))
                            {
                                var value = propertyValue.ToString();
                                if (value.Length > CharacterLimitPerExpansion)
                                {
                                    propertyValue = value.Substring(0, CharacterLimitPerExpansion - 3) + "...";
                                }
                            }
 
                            // Record our result, and advance
                            // our sourceIndex pointer to the character just after the closing
                            // parenthesis.
                            results.Add(propertyValue);
                        }
                        sourceIndex = propertyEndIndex + 1;
                    }
 
                    propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", sourceIndex, CompareOptions.Ordinal);
                }
 
                // If we couldn't find any more property tags in the expression just copy the remainder into the result.
                if (expression.Length - sourceIndex > 0)
                {
                    results.Add(expression.AsMemory(sourceIndex, expression.Length - sourceIndex));
                }
 
                return results.GetResult();
            }
 
            /// <summary>
            /// Expand the body of the property, including any functions that it may contain.
            /// </summary>
            internal static object ExpandPropertyBody(
                string propertyBody,
                object propertyValue,
                IPropertyProvider<T> properties,
                ExpanderOptions options,
                IElementLocation elementLocation,
                PropertiesUseTracker propertiesUseTracker,
                IFileSystem fileSystem)
            {
                Function<T> function = null;
                string propertyName = propertyBody;
 
                // Trim the body for compatibility reasons:
                // Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK.
                // Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any.
                // But we will do a property name lookup on the propertyName that we held onto.
                if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1]))
                {
                    propertyBody = propertyBody.Trim();
                }
 
                // If we don't have a clean propertybody then we'll do deeper checks to see
                // if what we have is a function
                if (!IsValidPropertyName(propertyBody))
                {
                    if (propertyBody.Contains(".") || propertyBody[0] == '[')
                    {
                        if (BuildParameters.DebugExpansion)
                        {
                            Console.WriteLine("Expanding: {0}", propertyBody);
                        }
 
                        // This is a function
                        function = Function<T>.ExtractPropertyFunction(
                            propertyBody,
                            elementLocation,
                            propertyValue,
                            propertiesUseTracker,
                            fileSystem,
                            propertiesUseTracker.LoggingContext);
 
                        // We may not have been able to parse out a function
                        if (function != null)
                        {
                            // We will have either extracted the actual property name
                            // or realized that there is none (static function), and have recorded a null
                            propertyName = function.Receiver;
                        }
                        else
                        {
                            // In the event that we have been handed an unrecognized property body, throw
                            // an invalid function property exception.
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty);
                            return null;
                        }
                    }
                    else if (propertyValue == null && propertyBody.Contains("[")) // a single property indexer
                    {
                        int indexerStart = propertyBody.IndexOf('[');
                        int indexerEnd = propertyBody.IndexOf(']');
 
                        if (indexerStart < 0 || indexerEnd < 0)
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets"));
                        }
                        else
                        {
                            propertyValue = LookupProperty(properties, propertyBody, 0, indexerStart - 1, elementLocation, propertiesUseTracker);
                            propertyBody = propertyBody.Substring(indexerStart);
 
                            // recurse so that the function representing the indexer can be executed on the property value
                            return ExpandPropertyBody(
                                propertyBody,
                                propertyValue,
                                properties,
                                options,
                                elementLocation,
                                propertiesUseTracker,
                                fileSystem);
                        }
                    }
                    else
                    {
                        // In the event that we have been handed an unrecognized property body, throw
                        // an invalid function property exception.
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty);
                        return null;
                    }
                }
 
                // Find the property value in our property collection.  This
                // will automatically return "" (empty string) if the property
                // doesn't exist in the collection, and we're not executing a static function
                if (!String.IsNullOrEmpty(propertyName))
                {
                    propertyValue = LookupProperty(properties, propertyName, elementLocation, propertiesUseTracker);
                }
 
                if (function != null)
                {
                    try
                    {
                        // Because of the rich expansion capabilities of MSBuild, we need to keep things
                        // as strings, since property expansion & string embedding can happen anywhere
                        // propertyValue can be null here, when we're invoking a static function
                        propertyValue = function.Execute(propertyValue, properties, options, elementLocation);
                    }
                    catch (Exception) when (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError))
                    {
                        propertyValue = propertyBody;
                    }
                }
 
                return propertyValue;
            }
 
            /// <summary>
            /// Convert the object into an MSBuild friendly string
            /// Arrays are supported.
            /// Will not return NULL.
            /// </summary>
            internal static string ConvertToString(object valueToConvert)
            {
                if (valueToConvert == null)
                {
                    return String.Empty;
                }
                // If the value is a string, then there is nothing to do
                if (valueToConvert is string stringValue)
                {
                    return stringValue;
                }
 
                string convertedString;
                if (valueToConvert is IDictionary dictionary)
                {
                    // If the return type is an IDictionary, then we convert this to
                    // a semi-colon delimited set of A=B pairs.
                    // Key and Value are converted to string and escaped
                    if (dictionary.Count > 0)
                    {
                        using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder();
 
                        foreach (DictionaryEntry entry in dictionary)
                        {
                            if (builder.Length > 0)
                            {
                                builder.Append(";");
                            }
 
                            // convert and escape each key and value in the dictionary entry
                            builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Key)));
                            builder.Append("=");
                            builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Value)));
                        }
 
                        convertedString = builder.ToString();
                    }
                    else
                    {
                        convertedString = string.Empty;
                    }
                }
                else if (valueToConvert is IEnumerable enumerable)
                {
                    // If the return is enumerable, then we'll convert to semi-colon delimited elements
                    // each of which must be converted, so we'll recurse for each element
                    using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder();
 
                    foreach (object element in enumerable)
                    {
                        if (builder.Length > 0)
                        {
                            builder.Append(";");
                        }
 
                        // we need to convert and escape each element of the array
                        builder.Append(EscapingUtilities.Escape(ConvertToString(element)));
                    }
 
                    convertedString = builder.ToString();
                }
                else
                {
                    // The fall back is always to just convert to a string directly.
                    // Issue: https://github.com/dotnet/msbuild/issues/9757
                    if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_12))
                    {
                        convertedString = Convert.ToString(valueToConvert, CultureInfo.InvariantCulture);
                    }
                    else
                    {
                        convertedString = valueToConvert.ToString();
                    }
                }
 
                return convertedString;
            }
 
            /// <summary>
            /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo).
            /// </summary>
            private static object LookupProperty(IPropertyProvider<T> properties, string propertyName, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker)
            {
                return LookupProperty(properties, propertyName, 0, propertyName.Length - 1, elementLocation, propertiesUseTracker);
            }
 
            /// <summary>
            /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo).
            /// </summary>
            private static object LookupProperty(IPropertyProvider<T> properties, string propertyName, int startIndex, int endIndex, IElementLocation elementLocation, PropertiesUseTracker propertiesUseTracker)
            {
                T property = properties.GetProperty(propertyName, startIndex, endIndex);
 
                object propertyValue;
 
                bool isArtificial = property == null && ((endIndex - startIndex) >= 7) &&
                                   MSBuildNameIgnoreCaseComparer.Default.Equals("MSBuild", propertyName, startIndex, 7);
 
                propertiesUseTracker.TrackRead(propertyName, startIndex, endIndex, elementLocation, property == null, isArtificial);
 
                if (isArtificial)
                {
                    // It could be one of the MSBuildThisFileXXXX properties,
                    // whose values vary according to the file they are in.
                    if (startIndex != 0 || endIndex != propertyName.Length)
                    {
                        propertyValue = ExpandMSBuildThisFileProperty(propertyName.Substring(startIndex, endIndex - startIndex + 1), elementLocation);
                    }
                    else
                    {
                        propertyValue = ExpandMSBuildThisFileProperty(propertyName, elementLocation);
                    }
                }
                else if (property == null)
                {
                    propertyValue = String.Empty;
                }
                else
                {
                    if (property is ProjectPropertyInstance.EnvironmentDerivedProjectPropertyInstance environmentDerivedProperty)
                    {
                        environmentDerivedProperty.loggingContext = propertiesUseTracker.LoggingContext;
                    }
 
                    propertyValue = property.GetEvaluatedValueEscaped(elementLocation);
                }
 
                return propertyValue;
            }
 
            /// <summary>
            /// If the property name provided is one of the special
            /// per file properties named "MSBuildThisFileXXXX" then returns the value of that property.
            /// If the location provided does not have a path (eg., if it comes from a file that has
            /// never been saved) then returns empty string.
            /// If the property name is not one of those properties, returns empty string.
            /// </summary>
            private static object ExpandMSBuildThisFileProperty(string propertyName, IElementLocation elementLocation)
            {
                if (!ReservedPropertyNames.IsReservedProperty(propertyName))
                {
                    return String.Empty;
                }
 
                if (elementLocation.File.Length == 0)
                {
                    return String.Empty;
                }
 
                string value = String.Empty;
 
                // Because String.Equals checks the length first, and these strings are almost
                // all different lengths, this sequence is efficient.
                if (String.Equals(propertyName, ReservedPropertyNames.thisFile, StringComparison.OrdinalIgnoreCase))
                {
                    value = Path.GetFileName(elementLocation.File);
                }
                else if (String.Equals(propertyName, ReservedPropertyNames.thisFileName, StringComparison.OrdinalIgnoreCase))
                {
                    value = Path.GetFileNameWithoutExtension(elementLocation.File);
                }
                else if (String.Equals(propertyName, ReservedPropertyNames.thisFileFullPath, StringComparison.OrdinalIgnoreCase))
                {
                    value = FileUtilities.NormalizePath(elementLocation.File);
                }
                else if (String.Equals(propertyName, ReservedPropertyNames.thisFileExtension, StringComparison.OrdinalIgnoreCase))
                {
                    value = Path.GetExtension(elementLocation.File);
                }
                else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectory, StringComparison.OrdinalIgnoreCase))
                {
                    value = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File));
                }
                else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectoryNoRoot, StringComparison.OrdinalIgnoreCase))
                {
                    string directory = Path.GetDirectoryName(elementLocation.File);
                    int rootLength = Path.GetPathRoot(directory).Length;
                    value = FileUtilities.EnsureTrailingNoLeadingSlash(directory, rootLength);
                }
 
                return value;
            }
 
            /// <summary>
            /// Given a string like "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation", return the value at that location
            /// in the registry. If the value isn't found, returns String.Empty.
            /// Properties may refer to a registry location by using the syntax for example
            /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and
            /// "TaskLocation" is the name of the value.  The name of the value and the preceding "@" may be omitted if
            /// the default value is desired.
            /// </summary>
            private static string ExpandRegistryValue(string registryExpression, IElementLocation elementLocation)
            {
#if RUNTIME_TYPE_NETCORE
                // .NET Core MSBuild used to always return empty, so match that behavior
                // on non-Windows (no registry).
                if (!NativeMethodsShared.IsWindows)
                {
                    return string.Empty;
                }
#endif
 
                // Remove "Registry:" prefix
                string registryLocation = registryExpression.Substring(9);
 
                // Split off the value name -- the part after the "@" sign. If there's no "@" sign, then it's the default value name
                // we want.
                int firstAtSignOffset = registryLocation.IndexOf('@');
                int lastAtSignOffset = registryLocation.LastIndexOf('@');
 
                ProjectErrorUtilities.VerifyThrowInvalidProject(firstAtSignOffset == lastAtSignOffset, elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", String.Empty);
 
                string valueName = lastAtSignOffset == -1 || lastAtSignOffset == registryLocation.Length - 1
                    ? null : registryLocation.Substring(lastAtSignOffset + 1);
 
                // If there's no '@', or '@' is first, then we'll use null or String.Empty for the location; otherwise
                // the location is the part before the '@'
                string registryKeyName = lastAtSignOffset != -1 ? registryLocation.Substring(0, lastAtSignOffset) : registryLocation;
 
                string result = String.Empty;
                if (registryKeyName != null)
                {
                    // We rely on the '@' character to delimit the key and its value, but the registry
                    // allows this character to be used in the names of keys and the names of values.
                    // Hence we use our standard escaping mechanism to allow users to access such keys
                    // and values.
                    registryKeyName = EscapingUtilities.UnescapeAll(registryKeyName);
 
                    if (valueName != null)
                    {
                        valueName = EscapingUtilities.UnescapeAll(valueName);
                    }
 
                    try
                    {
                        // Unless we are running under Windows, don't bother with anything but the user keys
                        if (!NativeMethodsShared.IsWindows && !registryKeyName.StartsWith("HKEY_CURRENT_USER", StringComparison.OrdinalIgnoreCase))
                        {
                            // Fake common requests to HKLM that we can resolve
 
                            // This is the base path of the framework
                            if (registryKeyName.StartsWith(
                                @"HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework",
                                StringComparison.OrdinalIgnoreCase) &&
                                valueName.Equals("InstallRoot", StringComparison.OrdinalIgnoreCase))
                            {
                                return NativeMethodsShared.FrameworkBasePath + Path.DirectorySeparatorChar;
                            }
 
                            return string.Empty;
                        }
 
                        object valueFromRegistry = Registry.GetValue(registryKeyName, valueName, null /* default if key or value name is not found */);
 
                        if (valueFromRegistry != null)
                        {
                            // Convert the result to a string that is reasonable for MSBuild
                            result = ConvertToString(valueFromRegistry);
                        }
                        else
                        {
                            // This means either the key or value was not found in the registry.  In this case,
                            // we simply expand the property value to String.Empty to imitate the behavior of
                            // normal properties.
                            result = String.Empty;
                        }
                    }
                    catch (Exception ex) when (!ExceptionHandling.NotExpectedRegistryException(ex))
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", ex.Message);
                    }
                }
 
                return result;
            }
        }
 
        /// <summary>
        /// Expands item expressions, like @(Compile), possibly with transforms and/or separators.
        ///
        /// Item vectors are composed of a name, an optional transform, and an optional separator i.e.
        ///
        ///     @(&lt;name&gt;->'&lt;transform&gt;','&lt;separator&gt;')
        ///
        /// If a separator is not specified it defaults to a semi-colon. The transform expression is also optional, but if
        /// specified, it allows each item in the vector to have its item-spec converted to a different form. The transform
        /// expression can reference any custom metadata defined on the item, as well as the pre-defined item-spec modifiers.
        ///
        /// NOTE:
        /// 1) white space between &lt;name&gt;, &lt;transform&gt; and &lt;separator&gt; is ignored
        ///    i.e. @(&lt;name&gt;, '&lt;separator&gt;') is valid
        /// 2) the separator is not restricted to be a single character, it can be a string
        /// 3) the separator can be an empty string i.e. @(&lt;name&gt;,'')
        /// 4) specifying an empty transform is NOT the same as specifying no transform -- the former will reduce all item-specs
        ///    to empty strings
        ///
        /// if @(files) is a vector for the files a.txt and b.txt, then:
        ///
        ///     "my list: @(files)"                                 expands to string     "my list: a.txt;b.txt"
        ///
        ///     "my list: @(files,' ')"                             expands to string      "my list: a.txt b.txt"
        ///
        ///     "my list: @(files, '')"                             expands to string      "my list: a.txtb.txt"
        ///
        ///     "my list: @(files, '; ')"                           expands to string      "my list: a.txt; b.txt"
        ///
        ///     "my list: @(files->'%(Filename)')"                  expands to string      "my list: a;b"
        ///
        ///     "my list: @(files -> 'temp\%(Filename).xml', ' ')   expands to string      "my list: temp\a.xml temp\b.xml"
        ///
        ///     "my list: @(files->'')                              expands to string      "my list: ;".
        /// </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 ItemExpander
        {
            /// <summary>
            /// Execute the list of transform functions.
            /// </summary>
            /// <typeparam name="S">class, IItem.</typeparam>
            internal static IEnumerable<KeyValuePair<string, S>> Transform<S>(Expander<P, I> expander, bool includeNullEntries, Stack<TransformFunction<S>> transformFunctionStack, IEnumerable<KeyValuePair<string, S>> itemsOfType)
                where S : class, IItem
            {
                // If we have transforms on our stack, then we'll execute those first
                // This effectively runs backwards through the set
                if (transformFunctionStack.Count > 0)
                {
                    TransformFunction<S> function = transformFunctionStack.Pop();
 
                    foreach (KeyValuePair<string, S> item in Transform(expander, includeNullEntries, transformFunctionStack, function.Execute(expander, includeNullEntries, itemsOfType)))
                    {
                        yield return item;
                    }
                }
                else
                {
                    // When we have no more tranforms on the stack, iterate over the items
                    // that we have to return them
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        yield return item;
                    }
                }
            }
 
            /// <summary>
            /// Expands any item vector in the expression into items.
            ///
            /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list.
            ///
            /// If there is no item vector in the expression (for example a literal "foo.cpp"), returns null.
            /// If the item vector expression expands to no items, returns an empty list.
            /// If item expansion is not allowed by the provided options, returns null.
            /// If there is an item vector but concatenated with something else, throws InvalidProjectFileException.
            /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
            ///
            /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because
            /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means
            /// that the length of the returned list is always the same as the length of the referenced item list in the input string.
            /// That's important for any correlation the caller wants to do.
            ///
            /// If expression was a transform, 'isTransformExpression' is true, otherwise false.
            ///
            /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not
            /// have an item type set on it, it will be given the item type of the item vector to use.
            /// </summary>
            /// <typeparam name="S">Type of the items provided by the item source used for expansion.</typeparam>
            /// <typeparam name="T">Type of the items that should be returned.</typeparam>
            internal static IList<T> ExpandSingleItemVectorExpressionIntoItems<S, T>(
                    Expander<P, I> expander, string expression, IItemProvider<S> items, IItemFactory<S, T> itemFactory, ExpanderOptions options,
                    bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation)
                where S : class, IItem
                where T : class, IItem
            {
                isTransformExpression = false;
 
                var expressionCapture = ExpandSingleItemVectorExpressionIntoExpressionCapture(expression, options, elementLocation);
                if (expressionCapture == null)
                {
                    return null;
                }
 
                return ExpandExpressionCaptureIntoItems(expressionCapture, expander, items, itemFactory, options, includeNullEntries,
                    out isTransformExpression, elementLocation);
            }
 
            internal static ExpressionShredder.ItemExpressionCapture ExpandSingleItemVectorExpressionIntoExpressionCapture(
                    string expression, ExpanderOptions options, IElementLocation elementLocation)
            {
                if (((options & ExpanderOptions.ExpandItems) == 0) || (expression.Length == 0))
                {
                    return null;
                }
 
                List<ExpressionShredder.ItemExpressionCapture> matches;
                if (expression.IndexOf('@') == -1)
                {
                    return null;
                }
                else
                {
                    matches = ExpressionShredder.GetReferencedItemExpressions(expression);
 
                    if (matches == null)
                    {
                        return null;
                    }
                }
 
                ExpressionShredder.ItemExpressionCapture match = matches[0];
 
                // We have a single valid @(itemlist) reference in the given expression.
                // If the passed-in expression contains exactly one item list reference,
                // with nothing else concatenated to the beginning or end, then proceed
                // with itemizing it, otherwise error.
                ProjectErrorUtilities.VerifyThrowInvalidProject(match.Value == expression, elementLocation, "EmbeddedItemVectorCannotBeItemized", expression);
                ErrorUtilities.VerifyThrow(matches.Count == 1, "Expected just one item vector");
 
                return match;
            }
 
            internal static IList<T> ExpandExpressionCaptureIntoItems<S, T>(
                    ExpressionShredder.ItemExpressionCapture expressionCapture, Expander<P, I> expander, IItemProvider<S> items, IItemFactory<S, T> itemFactory,
                    ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation)
                where S : class, IItem
                where T : class, IItem
            {
                ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items");
                isTransformExpression = false;
                bool brokeEarlyNonEmpty;
 
                // If the incoming factory doesn't have an item type that it can use to
                // create items, it's our indication that the caller wants its items to have the type of the
                // expression being expanded. For example, items from expanding "@(Compile") should
                // have the item type "Compile".
                if (itemFactory.ItemType == null)
                {
                    itemFactory.ItemType = expressionCapture.ItemType;
                }
 
 
                IList<T> result;
                if (expressionCapture.Separator != null)
                {
                    // Reference contains a separator, for example @(Compile, ';').
                    // We need to flatten the list into
                    // a scalar and then create a single item. Basically we need this
                    // to be able to convert item lists with user specified separators into properties.
                    string expandedItemVector;
                    using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder();
                    brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, expressionCapture, items, elementLocation, builder, options);
 
                    if (brokeEarlyNonEmpty)
                    {
                        return null;
                    }
 
                    expandedItemVector = builder.ToString();
 
                    result = new List<T>(1);
 
                    if (expandedItemVector.Length > 0)
                    {
                        T newItem = itemFactory.CreateItem(expandedItemVector, elementLocation.File);
 
                        result.Add(newItem);
                    }
 
                    return result;
                }
 
                List<KeyValuePair<string, S>> itemsFromCapture;
                brokeEarlyNonEmpty = ExpandExpressionCapture(expander, expressionCapture, items, elementLocation /* including null items */, options, true, out isTransformExpression, out itemsFromCapture);
 
                if (brokeEarlyNonEmpty)
                {
                    return null;
                }
 
                result = new List<T>(itemsFromCapture.Count);
 
                foreach (var itemTuple in itemsFromCapture)
                {
                    var itemSpec = itemTuple.Key;
                    var originalItem = itemTuple.Value;
 
                    if (itemSpec != null && originalItem == null)
                    {
                        // We have an itemspec, but no base item
                        result.Add(itemFactory.CreateItem(itemSpec, elementLocation.File));
                    }
                    else if (itemSpec != null && originalItem != null)
                    {
                        result.Add(itemSpec.Equals(originalItem.EvaluatedIncludeEscaped)
                            ? itemFactory.CreateItem(originalItem, elementLocation.File) // itemspec came from direct item reference, no transforms
                            : itemFactory.CreateItem(itemSpec, originalItem, elementLocation.File)); // itemspec came from a transform and is different from its original item
                    }
                    else if (includeNullEntries)
                    {
                        // The itemspec is null and the base item doesn't matter
                        result.Add(null);
                    }
                }
 
                return result;
            }
 
            /// <summary>
            /// Expands an expression capture into a list of items
            /// If the capture uses a separator, then all the items are concatenated into one string using that separator.
            ///
            /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early.
            /// </summary>
            /// <param name="isTransformExpression"></param>
            /// <param name="itemsFromCapture">
            /// List of items.
            ///
            /// Item1 represents the item string, escaped
            /// Item2 represents the original item.
            ///
            /// Item1 differs from Item2's string when it is coming from a transform.
            ///
            /// </param>
            /// <param name="expander">The expander whose state will be used to expand any transforms.</param>
            /// <param name="expressionCapture">The <see cref="ExpandSingleItemVectorExpressionIntoExpressionCapture"/> representing the structure of an item expression.</param>
            /// <param name="evaluatedItems"><see cref="IItemProvider{T}"/> to provide the inital items (which may get subsequently transformed, if <paramref name="expressionCapture"/> is a transform expression)>.</param>
            /// <param name="elementLocation">Location of the xml element containing the <paramref name="expressionCapture"/>.</param>
            /// <param name="options">expander options.</param>
            /// <param name="includeNullEntries">Wether to include items that evaluated to empty / null.</param>
            internal static bool ExpandExpressionCapture<S>(
                Expander<P, I> expander,
                ExpressionShredder.ItemExpressionCapture expressionCapture,
                IItemProvider<S> evaluatedItems,
                IElementLocation elementLocation,
                ExpanderOptions options,
                bool includeNullEntries,
                out bool isTransformExpression,
                out List<KeyValuePair<string, S>> itemsFromCapture)
                where S : class, IItem
            {
                ErrorUtilities.VerifyThrow(evaluatedItems != null, "Cannot expand items without providing items");
                // There's something wrong with the expression, and we ended up with a blank item type
                ProjectErrorUtilities.VerifyThrowInvalidProject(!string.IsNullOrEmpty(expressionCapture.ItemType), elementLocation, "InvalidFunctionPropertyExpression");
 
                isTransformExpression = false;
 
                var itemsOfType = evaluatedItems.GetItems(expressionCapture.ItemType);
 
                // If there are no items of the given type, then bail out early
                if (itemsOfType.Count == 0)
                {
                    // ... but only if there isn't a function "Count", since that will want to return something (zero) for an empty list
                    if (expressionCapture.Captures?.Any(capture => string.Equals(capture.FunctionName, "Count", StringComparison.OrdinalIgnoreCase)) != true)
                    {
                        // ...or a function "AnyHaveMetadataValue", since that will want to return false for an empty list.
                        if (expressionCapture.Captures?.Any(capture => string.Equals(capture.FunctionName, "AnyHaveMetadataValue", StringComparison.OrdinalIgnoreCase)) != true)
                        {
                            itemsFromCapture = new List<KeyValuePair<string, S>>();
                            return false;
                        }
                    }
                }
 
                if (expressionCapture.Captures != null)
                {
                    isTransformExpression = true;
                }
 
                itemsFromCapture = new List<KeyValuePair<string, S>>(itemsOfType.Count);
 
                if (!isTransformExpression)
                {
                    // No transform: expression is like @(Compile), so include the item spec without a transform base item
                    foreach (S item in itemsOfType)
                    {
                        if ((item.EvaluatedIncludeEscaped.Length > 0) && (options & ExpanderOptions.BreakOnNotEmpty) != 0)
                        {
                            return true;
                        }
 
                        itemsFromCapture.Add(new KeyValuePair<string, S>(item.EvaluatedIncludeEscaped, item));
                    }
                }
                else
                {
                    Stack<TransformFunction<S>> transformFunctionStack = PrepareTransformStackFromMatch<S>(elementLocation, expressionCapture);
 
                    // iterate over the tranform chain, creating the final items from its results
                    foreach (KeyValuePair<string, S> itemTuple in Transform<S>(expander, includeNullEntries, transformFunctionStack, IntrinsicItemFunctions<S>.GetItemPairEnumerable(itemsOfType)))
                    {
                        if (!string.IsNullOrEmpty(itemTuple.Key) && (options & ExpanderOptions.BreakOnNotEmpty) != 0)
                        {
                            return true; // broke out early; result cannot be trusted
                        }
 
                        itemsFromCapture.Add(itemTuple);
                    }
                }
 
                if (expressionCapture.Separator != null)
                {
                    var joinedItems = string.Join(expressionCapture.Separator, itemsFromCapture.Select(i => i.Key));
                    itemsFromCapture.Clear();
                    itemsFromCapture.Add(new KeyValuePair<string, S>(joinedItems, null));
                }
 
                return false; // did not break early
            }
 
            /// <summary>
            /// Expands all item vectors embedded in the given expression into a single string.
            /// If the expression is empty, returns empty string.
            /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted.
            /// </summary>
            /// <typeparam name="T">Type of the items provided.</typeparam>
            internal static string ExpandItemVectorsIntoString<T>(Expander<P, I> expander, string expression, IItemProvider<T> items, ExpanderOptions options, IElementLocation elementLocation)
                where T : class, IItem
            {
                if ((options & ExpanderOptions.ExpandItems) == 0 || expression.Length == 0)
                {
                    return expression;
                }
 
                ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items");
 
                List<ExpressionShredder.ItemExpressionCapture> matches = ExpressionShredder.GetReferencedItemExpressions(expression);
 
                if (matches == null)
                {
                    return expression;
                }
 
                using SpanBasedStringBuilder builder = Strings.GetSpanBasedStringBuilder();
                // As we walk through the matches, we need to copy out the original parts of the string which
                // are not covered by the match.  This preserves original behavior which did not trim whitespace
                // from between separators.
                int lastStringIndex = 0;
                for (int i = 0; i < matches.Count; i++)
                {
                    if (matches[i].Index > lastStringIndex)
                    {
                        if ((options & ExpanderOptions.BreakOnNotEmpty) != 0)
                        {
                            return null;
                        }
 
                        builder.Append(expression, lastStringIndex, matches[i].Index - lastStringIndex);
                    }
 
                    bool brokeEarlyNonEmpty = ExpandExpressionCaptureIntoStringBuilder(expander, matches[i], items, elementLocation, builder, options);
 
                    if (brokeEarlyNonEmpty)
                    {
                        return null;
                    }
 
                    lastStringIndex = matches[i].Index + matches[i].Length;
                }
 
                builder.Append(expression, lastStringIndex, expression.Length - lastStringIndex);
 
                return builder.ToString();
            }
 
            /// <summary>
            /// Prepare the stack of transforms that will be executed on a given set of items.
            /// </summary>
            /// <typeparam name="S">class, IItem.</typeparam>
            private static Stack<TransformFunction<S>> PrepareTransformStackFromMatch<S>(IElementLocation elementLocation, ExpressionShredder.ItemExpressionCapture match)
                where S : class, IItem
            {
                // There's something wrong with the expression, and we ended up with no function names
                ProjectErrorUtilities.VerifyThrowInvalidProject(match.Captures.Count > 0, elementLocation, "InvalidFunctionPropertyExpression");
 
                Stack<TransformFunction<S>> transformFunctionStack = new Stack<TransformFunction<S>>(match.Captures.Count);
 
                // Create a TransformFunction for each transform in the chain by extracting the relevant information
                // from the regex parsing results
                // Each will be pushed onto a stack in right to left order (i.e. the inner/right most will be on the
                // bottom of the stack, the outer/left most will be on the top
                for (int n = match.Captures.Count - 1; n >= 0; n--)
                {
                    string function = match.Captures[n].Value;
                    string functionName = match.Captures[n].FunctionName;
                    string argumentsExpression = match.Captures[n].FunctionArguments;
 
                    string[] arguments = null;
 
                    if (functionName == null)
                    {
                        functionName = "ExpandQuotedExpressionFunction";
                        arguments = [function];
                    }
                    else if (argumentsExpression != null)
                    {
                        arguments = ExtractFunctionArguments(elementLocation, argumentsExpression, argumentsExpression);
                    }
 
                    IntrinsicItemFunctions<S>.ItemTransformFunction transformFunction = IntrinsicItemFunctions<S>.GetItemTransformFunction(elementLocation, functionName, typeof(S));
 
                    // Push our tranform on to the stack
                    transformFunctionStack.Push(new TransformFunction<S>(elementLocation, functionName, transformFunction, arguments));
                }
 
                return transformFunctionStack;
            }
 
            /// <summary>
            /// Expand the match provided into a string, and append that to the provided InternableString.
            /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early.
            /// </summary>
            /// <typeparam name="S">Type of source items.</typeparam>
            private static bool ExpandExpressionCaptureIntoStringBuilder<S>(
                Expander<P, I> expander,
                ExpressionShredder.ItemExpressionCapture capture,
                IItemProvider<S> evaluatedItems,
                IElementLocation elementLocation,
                SpanBasedStringBuilder builder,
                ExpanderOptions options)
                where S : class, IItem
            {
                List<KeyValuePair<string, S>> itemsFromCapture;
                bool throwaway;
                var brokeEarlyNonEmpty = ExpandExpressionCapture(expander, capture, evaluatedItems, elementLocation /* including null items */, options, true, out throwaway, out itemsFromCapture);
 
                if (brokeEarlyNonEmpty)
                {
                    return true;
                }
 
                int startLength = builder.Length;
                bool truncate = IsTruncationEnabled(options);
 
                // if the capture.Separator is not null, then ExpandExpressionCapture would have joined the items using that separator itself
                for (int i = 0; i < itemsFromCapture.Count; i++)
                {
                    var item = itemsFromCapture[i];
                    if (truncate)
                    {
                        if (i >= ItemLimitPerExpansion)
                        {
                            builder.Append("...");
                            return false;
                        }
                        int currentLength = builder.Length - startLength;
                        if (!string.IsNullOrEmpty(item.Key) && currentLength + item.Key.Length > CharacterLimitPerExpansion)
                        {
                            int truncateIndex = CharacterLimitPerExpansion - currentLength - 3;
                            if (truncateIndex > 0)
                            {
                                builder.Append(item.Key, 0, truncateIndex);
                            }
                            builder.Append("...");
                            return false;
                        }
                    }
                    builder.Append(item.Key);
                    if (i < itemsFromCapture.Count - 1)
                    {
                        builder.Append(";");
                    }
                }
 
                return false;
            }
 
            /// <summary>
            /// The set of functions that called during an item transformation, e.g. @(CLCompile->ContainsMetadata('MetaName', 'metaValue')).
            /// </summary>
            /// <typeparam name="S">class, IItem.</typeparam>
            internal static class IntrinsicItemFunctions<S>
                where S : class, IItem
            {
                /// <summary>
                /// A cache of previously created item function delegates.
                /// </summary>
                private static ConcurrentDictionary<string, ItemTransformFunction> s_transformFunctionDelegateCache = new ConcurrentDictionary<string, ItemTransformFunction>(StringComparer.OrdinalIgnoreCase);
 
                /// <summary>
                /// Delegate that represents the signature of all item transformation functions
                /// This is used to support calling the functions by name.
                /// </summary>
                public delegate IEnumerable<KeyValuePair<string, S>> ItemTransformFunction(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments);
 
                /// <summary>
                /// Get a delegate to the given item transformation function by supplying the name and the
                /// Item type that should be used.
                /// </summary>
                internal static ItemTransformFunction GetItemTransformFunction(IElementLocation elementLocation, string functionName, Type itemType)
                {
                    ItemTransformFunction transformFunction = null;
                    string qualifiedFunctionName = itemType.FullName + "::" + functionName;
 
                    // We may have seen this delegate before, if so grab the one we already created
                    if (!s_transformFunctionDelegateCache.TryGetValue(qualifiedFunctionName, out transformFunction))
                    {
                        if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(functionName))
                        {
                            // Create a delegate to the function we're going to call
                            transformFunction = new ItemTransformFunction(ItemSpecModifierFunction);
                        }
                        else
                        {
                            MethodInfo itemFunctionInfo = typeof(IntrinsicItemFunctions<S>).GetMethod(functionName, BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Static);
 
                            if (itemFunctionInfo == null)
                            {
                                functionName = "ExecuteStringFunction";
                                itemFunctionInfo = typeof(IntrinsicItemFunctions<S>).GetMethod(functionName, BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Static);
                                if (itemFunctionInfo == null)
                                {
                                    ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName);
                                    return null;
                                }
                            }
                            try
                            {
                                // Create a delegate to the function we're going to call
                                transformFunction = (ItemTransformFunction)itemFunctionInfo.CreateDelegate(typeof(ItemTransformFunction));
                            }
                            catch (ArgumentException)
                            {
                                // Prior to porting to .NET Core, this code was passing false as the throwOnBindFailure parameter to Delegate.CreateDelegate.
                                //  Since MethodInfo.CreateDelegate doesn't have this option, we catch the ArgumentException to preserve the previous behavior
                                ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName);
                            }
                        }
 
                        if (transformFunction == null)
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName);
                            return null;
                        }
 
                        // record our delegate for future use
                        s_transformFunctionDelegateCache[qualifiedFunctionName] = transformFunction;
                    }
 
                    return transformFunction;
                }
 
                /// <summary>
                /// Create an enumerator from a base IEnumerable of items into an enumerable
                /// of transformation result which includes the new itemspec and the base item.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> GetItemPairEnumerable(IEnumerable<S> itemsOfType)
                {
                    // iterate over the items, and yield out items in the tuple format
                    foreach (var item in itemsOfType)
                    {
                        if (Traits.Instance.UseLazyWildCardEvaluation)
                        {
                            foreach (var resultantItem in
                                EngineFileUtilities.GetFileListEscaped(
                                    item.ProjectDirectory,
                                    item.EvaluatedIncludeEscaped,
                                    forceEvaluate: true))
                            {
                                yield return new KeyValuePair<string, S>(resultantItem, item);
                            }
                        }
                        else
                        {
                            yield return new KeyValuePair<string, S>(item.EvaluatedIncludeEscaped, item);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns the number of items in the list.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Count(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    yield return new KeyValuePair<string, S>(Convert.ToString(itemsOfType.Count(), CultureInfo.InvariantCulture), null /* no base item */);
                }
 
                /// <summary>
                /// Intrinsic function that returns the specified built-in modifer value of the items in itemsOfType
                /// Tuple is {current item include, item under transformation}.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> ItemSpecModifierFunction(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        // If the item include has become empty,
                        // this is the end of the pipeline for this item
                        if (String.IsNullOrEmpty(item.Key))
                        {
                            continue;
                        }
 
                        string result = null;
 
                        try
                        {
                            // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                            // In that case, 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
                            string directoryToUse = item.Value.ProjectDirectory ?? Directory.GetCurrentDirectory();
                            string definingProjectEscaped = item.Value.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath);
 
                            result = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(directoryToUse, item.Key, definingProjectEscaped, functionName);
                        }
                        // 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.Key, 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
                            yield return new KeyValuePair<string, S>(result, item.Value);
                        }
                        else if (includeNullEntries)
                        {
                            yield return new KeyValuePair<string, S>(null, item.Value);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns the subset of items that actually exist on disk.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Exists(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (String.IsNullOrEmpty(item.Key))
                        {
                            continue;
                        }
 
                        // Unescape as we are passing to the file system
                        string unescapedPath = EscapingUtilities.UnescapeAll(item.Key);
 
                        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, 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
                                string baseDirectoryToUse = item.Value.ProjectDirectory ?? String.Empty;
                                rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath);
                            }
                        }
                        catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, e.Message);
                        }
 
                        if (File.Exists(rootedPath) || Directory.Exists(rootedPath))
                        {
                            yield return item;
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that combines the existing paths of the input items with a given relative path.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Combine(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    string relativePath = arguments[0];
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (String.IsNullOrEmpty(item.Key))
                        {
                            continue;
                        }
 
                        // Unescape as we are passing to the file system
                        string unescapedPath = EscapingUtilities.UnescapeAll(item.Key);
                        string combinedPath = Path.Combine(unescapedPath, relativePath);
                        string escapedPath = EscapingUtilities.Escape(combinedPath);
                        yield return new KeyValuePair<string, S>(escapedPath, null);
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns all ancestor directories of the given items.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> GetPathsOfAllDirectoriesAbove(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    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 (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (String.IsNullOrEmpty(item.Key))
                        {
                            continue;
                        }
 
                        string directoryName = null;
 
                        // Unescape as we are passing to the file system
                        string unescapedPath = EscapingUtilities.UnescapeAll(item.Key);
 
                        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, 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
                                string baseDirectoryToUse = item.Value.ProjectDirectory ?? 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.Key, 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);
                        yield return new KeyValuePair<string, S>(escapedDirectoryPath, null);
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns the DirectoryName of the items in itemsOfType
                /// UNDONE: This can be removed in favor of a built-in %(DirectoryName) metadata in future.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> DirectoryName(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    Dictionary<string, string> directoryNameTable = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        // If the item include has become empty,
                        // this is the end of the pipeline for this item
                        if (String.IsNullOrEmpty(item.Key))
                        {
                            continue;
                        }
 
                        string directoryName;
                        if (!directoryNameTable.TryGetValue(item.Key, out directoryName))
                        {
                            // Unescape as we are passing to the file system
                            string unescapedPath = EscapingUtilities.UnescapeAll(item.Key);
 
                            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, 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
                                    string baseDirectoryToUse = item.Value.ProjectDirectory ?? String.Empty;
                                    rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath);
                                }
 
                                directoryName = Path.GetDirectoryName(rootedPath);
                            }
                            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                            {
                                ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Key, 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
                            yield return new KeyValuePair<string, S>(directoryName, item.Value);
                        }
                        else if (includeNullEntries)
                        {
                            yield return new KeyValuePair<string, S>(null, item.Value);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns the contents of the metadata in specified in argument[0].
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Metadata(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    string metadataName = arguments[0];
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (item.Value != null)
                        {
                            string metadataValue = null;
 
                            try
                            {
                                metadataValue = item.Value.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.IndexOf(';') >= 0)
                                {
                                    var splits = ExpressionShredder.SplitSemiColonSeparatedList(metadataValue);
 
                                    foreach (string itemSpec in splits)
                                    {
                                        // return a result through the enumerator
                                        yield return new KeyValuePair<string, S>(itemSpec, item.Value);
                                    }
                                }
                                else
                                {
                                    // return a result through the enumerator
                                    yield return new KeyValuePair<string, S>(metadataValue, item.Value);
                                }
                            }
                            else if (metadataValue != String.Empty && includeNullEntries)
                            {
                                yield return new KeyValuePair<string, S>(metadataValue, item.Value);
                            }
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple
                /// Using a case sensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> DistinctWithCase(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    return DistinctWithComparer(expander, elementLocation, includeNullEntries, functionName, itemsOfType, arguments, StringComparer.Ordinal);
                }
 
                /// <summary>
                /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Distinct(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    return DistinctWithComparer(expander, elementLocation, includeNullEntries, functionName, itemsOfType, arguments, StringComparer.OrdinalIgnoreCase);
                }
 
                /// <summary>
                /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> DistinctWithComparer(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments, StringComparer comparer)
                {
                    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>(comparer);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (item.Key != null && seenItems.Add(item.Key))
                        {
                            yield return item;
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function reverses the item list.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> Reverse(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
                    return itemsOfType.Reverse();
                }
 
                /// <summary>
                /// Intrinsic function that transforms expressions like the %(foo) in @(Compile->'%(foo)').
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> ExpandQuotedExpressionFunction(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        MetadataMatchEvaluator matchEvaluator;
                        string include = null;
 
                        // If we've been handed a null entry by an uptream tranform
                        // then we don't want to try to tranform it with an itempec modification.
                        // Simply allow the null to be passed along (if, we are including nulls as specified by includeNullEntries
                        if (item.Key != null)
                        {
                            matchEvaluator = new MetadataMatchEvaluator(item.Key, item.Value, elementLocation);
 
                            include = RegularExpressions.ItemMetadataPattern.Value.Replace(arguments[0], matchEvaluator.GetMetadataValueFromMatch);
                        }
 
                        // Include may be empty. Historically we have created items with empty include
                        // and ultimately set them on tasks, but we don't do that anymore as it's broken.
                        // Instead we optionally add a null, so that input and output lists are the same length; this allows
                        // the caller to possibly do correlation.
 
                        // We pass in the existing item so we can copy over its metadata
                        if (!string.IsNullOrEmpty(include))
                        {
                            yield return new KeyValuePair<string, S>(include, item.Value);
                        }
                        else if (includeNullEntries)
                        {
                            yield return new KeyValuePair<string, S>(null, item.Value);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that transforms expressions by invoking methods of System.String on the itemspec
                /// of the item in the pipeline.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> ExecuteStringFunction(
                    Expander<P, I> expander,
                    IElementLocation elementLocation,
                    bool includeNullEntries,
                    string functionName,
                    IEnumerable<KeyValuePair<string, S>> itemsOfType,
                    string[] arguments)
                {
                    // Transform: expression is like @(Compile->'%(foo)'), so create completely new items,
                    // using the Include from the source items
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        Function<P> function = new Function<P>(
                            typeof(string),
                            item.Key,
                            item.Key,
                            functionName,
                            arguments,
                            BindingFlags.Public | BindingFlags.InvokeMethod,
                            string.Empty,
                            expander.PropertiesUseTracker,
                            expander._fileSystem,
                            expander._loggingContext);
 
                        object result = function.Execute(item.Key, expander._properties, ExpanderOptions.ExpandAll, elementLocation);
 
                        string include = Expander<P, I>.PropertyExpander<P>.ConvertToString(result);
 
                        // We pass in the existing item so we can copy over its metadata
                        if (include.Length > 0)
                        {
                            yield return new KeyValuePair<string, S>(include, item.Value);
                        }
                        else if (includeNullEntries)
                        {
                            yield return new KeyValuePair<string, S>(null, item.Value);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns the items from itemsOfType with their metadata cleared, i.e. only the itemspec is retained.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> ClearMetadata(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (includeNullEntries || item.Key != null)
                        {
                            yield return new KeyValuePair<string, S>(item.Key, null);
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns only those items that have a not-blank value for the metadata specified
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> HasMetadata(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    string metadataName = arguments[0];
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        string metadataValue = null;
 
                        try
                        {
                            metadataValue = item.Value.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
                            yield return item;
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns only those items have the given metadata value
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> WithMetadataValue(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    string metadataName = arguments[0];
                    string metadataValueToFind = arguments[1];
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        string metadataValue = null;
 
                        try
                        {
                            metadataValue = item.Value.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
                            yield return item;
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns those items don't have the given metadata value
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> WithoutMetadataValue(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    ProjectErrorUtilities.VerifyThrowInvalidProject(arguments?.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, arguments == null ? 0 : arguments.Length);
 
                    string metadataName = arguments[0];
                    string metadataValueToFind = arguments[1];
 
                    foreach (KeyValuePair<string, S> item in itemsOfType)
                    {
                        string metadataValue = null;
 
                        try
                        {
                            metadataValue = item.Value.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
                            yield return item;
                        }
                    }
                }
 
                /// <summary>
                /// Intrinsic function that returns a boolean to indicate if any of the items have the given metadata value
                /// Using a case insensitive comparison.
                /// </summary>
                internal static IEnumerable<KeyValuePair<string, S>> AnyHaveMetadataValue(Expander<P, I> expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable<KeyValuePair<string, S>> itemsOfType, string[] arguments)
                {
                    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 (KeyValuePair<string, S> item in itemsOfType)
                    {
                        if (item.Value != null)
                        {
                            string metadataValue = null;
 
                            try
                            {
                                metadataValue = item.Value.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
                                yield return new KeyValuePair<string, S>("true", item.Value);
 
                                // break out as soon as we found a match
                                yield break;
                            }
                        }
                    }
 
                    if (!metadataFound)
                    {
                        // We did not locate an item with the required metadata
                        yield return new KeyValuePair<string, S>("false", null);
                    }
                }
            }
 
            /// <summary>
            /// Represents all the components of a transform function, including the ability to execute it.
            /// </summary>
            /// <typeparam name="S">class, IItem.</typeparam>
            internal class TransformFunction<S>
                where S : class, IItem
            {
                /// <summary>
                /// The delegate that points to the transform function.
                /// </summary>
                private IntrinsicItemFunctions<S>.ItemTransformFunction _transform;
 
                /// <summary>
                /// Arguments to pass to the transform function as parsed out of the project file.
                /// </summary>
                private string[] _arguments;
 
                /// <summary>
                /// The element location of the transform expression.
                /// </summary>
                private IElementLocation _elementLocation;
 
                /// <summary>
                /// The name of the function that this class will call.
                /// </summary>
                private string _functionName;
 
                /// <summary>
                /// TransformFunction constructor.
                /// </summary>
                public TransformFunction(IElementLocation elementLocation, string functionName, IntrinsicItemFunctions<S>.ItemTransformFunction transform, string[] arguments)
                {
                    _elementLocation = elementLocation;
                    _functionName = functionName;
                    _transform = transform;
                    _arguments = arguments;
                }
 
                /// <summary>
                /// Arguments to pass to the transform function as parsed out of the project file.
                /// </summary>
                public string[] Arguments
                {
                    get { return _arguments; }
                }
 
                /// <summary>
                /// The element location of the transform expression.
                /// </summary>
                public IElementLocation ElementLocation
                {
                    get { return _elementLocation; }
                }
 
                /// <summary>
                /// Execute this transform function with the arguments contained within this TransformFunction instance.
                /// </summary>
                public IEnumerable<KeyValuePair<string, S>> Execute(Expander<P, I> expander, bool includeNullEntries, IEnumerable<KeyValuePair<string, S>> itemsOfType)
                {
                    // Execute via the delegate
                    return _transform(expander, _elementLocation, includeNullEntries, _functionName, itemsOfType, _arguments);
                }
            }
 
            /// <summary>
            /// A functor that returns the value of the metadata in the match
            /// that is on the item it was created with.
            /// </summary>
            private class MetadataMatchEvaluator
            {
                /// <summary>
                /// The current ItemSpec of the item being matched.
                /// </summary>
                private string _itemSpec;
 
                /// <summary>
                /// Item used as the source of metadata.
                /// </summary>
                private IItem _sourceOfMetadata;
 
                /// <summary>
                /// Location of the match.
                /// </summary>
                private IElementLocation _elementLocation;
 
                /// <summary>
                /// Constructor.
                /// </summary>
                internal MetadataMatchEvaluator(string itemSpec, IItem sourceOfMetadata, IElementLocation elementLocation)
                {
                    _itemSpec = itemSpec;
                    _sourceOfMetadata = sourceOfMetadata;
                    _elementLocation = elementLocation;
                }
 
                /// <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>
                internal string GetMetadataValueFromMatch(Match match)
                {
                    string name = match.Groups[RegularExpressions.NameGroup].Value;
 
                    ProjectErrorUtilities.VerifyThrowInvalidProject(match.Groups[RegularExpressions.ItemSpecificationGroup].Length == 0, _elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name);
 
                    string value = null;
                    try
                    {
                        if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(name))
                        {
                            // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null.
                            // In that case, 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
                            string directoryToUse = _sourceOfMetadata.ProjectDirectory ?? Directory.GetCurrentDirectory();
                            string definingProjectEscaped = _sourceOfMetadata.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath);
 
                            value = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(directoryToUse, _itemSpec, definingProjectEscaped, name);
                        }
                        else
                        {
                            value = _sourceOfMetadata.GetMetadataValueEscaped(name);
                        }
                    }
                    catch (InvalidOperationException ex)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(_elementLocation, "CannotEvaluateItemMetadata", name, ex.Message);
                    }
 
                    return value;
                }
            }
        }
 
        /// <summary>
        /// Regular expressions used by the expander.
        /// The expander currently uses regular expressions rather than a parser to do its work.
        /// </summary>
        private static class RegularExpressions
        {
            /**************************************************************************************************************************
            * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the
            * description of an item vector changes, the expressions must be updated in both places.
            *************************************************************************************************************************/
 
            /// <summary>
            /// Regular expression used to match item metadata references embedded in strings.
            /// For example, %(Compile.DependsOn) or %(DependsOn).
            /// </summary>
            internal static readonly Lazy<Regex> ItemMetadataPattern = new Lazy<Regex>(
                () => new Regex(ItemMetadataSpecification,
                    RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled));
 
            /// <summary>
            /// Name of the group matching the "name" of a metadatum.
            /// </summary>
            internal const string NameGroup = "NAME";
 
            /// <summary>
            /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)".
            /// </summary>
            internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION";
 
            /// <summary>
            /// Name of the group matching the item type in an item expression or metadata expression.
            /// </summary>
            internal const string ItemTypeGroup = "ITEM_TYPE";
 
            /// <summary>
            /// regular expression used to match item metadata references outside of item vector transforms.
            /// </summary>
            /// <remarks>PERF WARNING: this Regex is complex and tends to run slowly.</remarks>
            internal static readonly Lazy<Regex> NonTransformItemMetadataPattern = new Lazy<Regex>(
                () => new Regex(
                    @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" +
                    ItemVectorWithTransformRHS + @")) | ((?<!" + ItemVectorWithTransformLHS + @")" +
                    ItemMetadataSpecification + @"(?=" + ItemVectorWithTransformRHS + @")) | ((?<!" +
                    ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" +
                    ItemVectorWithTransformRHS + @"))",
                    RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture | RegexOptions.Compiled));
 
            /// <summary>
            /// Complete description of an item metadata reference, including the optional qualifying item type.
            /// For example, %(Compile.DependsOn) or %(DependsOn).
            /// </summary>
            private const string ItemMetadataSpecification = @"%\(\s* (?<ITEM_SPECIFICATION>(?<ITEM_TYPE>" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?<NAME>" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)";
 
            /// <summary>
            /// description of an item vector with a transform, left hand side.
            /// </summary>
            private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*";
 
            /// <summary>
            /// description of an item vector with a transform, right hand side.
            /// </summary>
            private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)";
 
            /**************************************************************************************************************************
             * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class.
             *************************************************************************************************************************/
        }
 
        private struct FunctionBuilder<T>
            where T : class, IProperty
        {
            /// <summary>
            /// The type of this function's receiver.
            /// </summary>
            public Type ReceiverType { get; set; }
 
            /// <summary>
            /// The name of the function.
            /// </summary>
            public string Name { get; set; }
 
            /// <summary>
            /// The arguments for the function.
            /// </summary>
            public string[] Arguments { get; set; }
 
            /// <summary>
            /// The expression that this function is part of.
            /// </summary>
            public string Expression { get; set; }
 
            /// <summary>
            /// The property name that this function is applied on.
            /// </summary>
            public string Receiver { get; set; }
 
            /// <summary>
            /// The binding flags that will be used during invocation of this function.
            /// </summary>
            public BindingFlags BindingFlags { get; set; }
 
            /// <summary>
            /// The remainder of the body once the function and arguments have been extracted.
            /// </summary>
            public string Remainder { get; set; }
 
            public IFileSystem FileSystem { get; set; }
 
            public LoggingContext LoggingContext { get; set; }
 
            /// <summary>
            /// List of properties which have been used but have not been initialized yet.
            /// </summary>
            public PropertiesUseTracker PropertiesUseTracker { get; set; }
 
            internal readonly Function<T> Build()
            {
                return new Function<T>(
                    ReceiverType,
                    Expression,
                    Receiver,
                    Name,
                    Arguments,
                    BindingFlags,
                    Remainder,
                    PropertiesUseTracker,
                    FileSystem,
                    LoggingContext);
            }
        }
 
        /// <summary>
        /// This class represents the function as extracted from an expression
        /// It is also responsible for executing the function.
        /// </summary>
        /// <typeparam name="T">Type of the properties used to expand the expression.</typeparam>
        internal class Function<T>
            where T : class, IProperty
        {
            /// <summary>
            /// The type of this function's receiver.
            /// </summary>
            private Type _receiverType;
 
            /// <summary>
            /// The name of the function.
            /// </summary>
            private readonly string _methodMethodName;
 
            /// <summary>
            /// The arguments for the function.
            /// </summary>
            private readonly string[] _arguments;
 
            /// <summary>
            /// The expression that this function is part of.
            /// </summary>
            private readonly string _expression;
 
            /// <summary>
            /// The property name that this function is applied on.
            /// </summary>
            private readonly string _receiver;
 
            /// <summary>
            /// The binding flags that will be used during invocation of this function.
            /// </summary>
            private BindingFlags _bindingFlags;
 
            /// <summary>
            /// The remainder of the body once the function and arguments have been extracted.
            /// </summary>
            private readonly string _remainder;
 
            /// <summary>
            /// List of properties which have been used but have not been initialized yet.
            /// </summary>
            private PropertiesUseTracker _propertiesUseTracker;
 
            private readonly IFileSystem _fileSystem;
 
            private readonly LoggingContext _loggingContext;
 
            /// <summary>
            /// Construct a function that will be executed during property evaluation.
            /// </summary>
            internal Function(
                Type receiverType,
                string expression,
                string receiver,
                string methodName,
                string[] arguments,
                BindingFlags bindingFlags,
                string remainder,
                PropertiesUseTracker propertiesUseTracker,
                IFileSystem fileSystem,
                LoggingContext loggingContext)
            {
                _methodMethodName = methodName;
                if (arguments == null)
                {
                    _arguments = [];
                }
                else
                {
                    _arguments = arguments;
                }
 
                _receiver = receiver;
                _expression = expression;
                _receiverType = receiverType;
                _bindingFlags = bindingFlags;
                _remainder = remainder;
                _propertiesUseTracker = propertiesUseTracker;
                _fileSystem = fileSystem;
                _loggingContext = loggingContext;
            }
 
            /// <summary>
            /// Part of the extraction may result in the name of the property
            /// This accessor is used by the Expander
            /// Examples of expression root:
            ///     [System.Diagnostics.Process]::Start
            ///     SomeMSBuildProperty.
            /// </summary>
            internal string Receiver
            {
                get { return _receiver; }
            }
 
            /// <summary>
            /// Extract the function details from the given property function expression.
            /// </summary>
            internal static Function<T> ExtractPropertyFunction(
                string expressionFunction,
                IElementLocation elementLocation,
                object propertyValue,
                PropertiesUseTracker propertiesUseTracker,
                IFileSystem fileSystem,
                LoggingContext loggingContext)
            {
                // Used to aggregate all the components needed for a Function
                FunctionBuilder<T> functionBuilder = new FunctionBuilder<T> { FileSystem = fileSystem, LoggingContext = loggingContext };
 
                // By default the expression root is the whole function expression
                ReadOnlySpan<char> expressionRoot = expressionFunction == null ? ReadOnlySpan<char>.Empty : expressionFunction.AsSpan();
 
                // The arguments for this function start at the first '('
                // If there are no arguments, then we're a property getter
                var argumentStartIndex = expressionFunction.IndexOf('(');
 
                // If we have arguments, then we only want the content up to but not including the '('
                if (argumentStartIndex > -1)
                {
                    expressionRoot = expressionRoot.Slice(0, argumentStartIndex);
                }
 
                // In case we ended up with something we don't understand
                ProjectErrorUtilities.VerifyThrowInvalidProject(!expressionRoot.IsEmpty, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
 
                functionBuilder.Expression = expressionFunction;
                functionBuilder.PropertiesUseTracker = propertiesUseTracker;
 
                // This is a static method call
                // A static method is the content that follows the last "::", the rest being the type
                if (propertyValue == null && expressionRoot[0] == '[')
                {
                    var typeEndIndex = expressionRoot.IndexOf(']');
 
                    if (typeEndIndex < 1)
                    {
                        // We ended up with something other than a function expression
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty);
                    }
 
                    var typeName = Strings.WeakIntern(expressionRoot.Slice(1, typeEndIndex - 1));
                    var methodStartIndex = typeEndIndex + 1;
 
                    if (expressionRoot.Length > methodStartIndex + 2 && expressionRoot[methodStartIndex] == ':' && expressionRoot[methodStartIndex + 1] == ':')
                    {
                        // skip over the "::"
                        methodStartIndex += 2;
                    }
                    else
                    {
                        // We ended up with something other than a static function expression
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty);
                    }
 
                    ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder);
 
                    // Locate a type that matches the body of the expression.
                    var receiverType = GetTypeForStaticMethod(typeName, functionBuilder.Name);
 
                    if (receiverType == null)
                    {
                        // We ended up with something other than a type
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionTypeUnavailable", expressionFunction, typeName);
                    }
 
                    functionBuilder.ReceiverType = receiverType;
                }
                else if (expressionFunction[0] == '[') // We have an indexer
                {
                    var indexerEndIndex = expressionFunction.IndexOf(']', 1);
                    if (indexerEndIndex < 1)
                    {
                        // We ended up with something other than a function expression
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets"));
                    }
 
                    var methodStartIndex = indexerEndIndex + 1;
 
                    functionBuilder.ReceiverType = propertyValue.GetType();
 
                    ConstructIndexerFunction(expressionFunction, elementLocation, propertyValue, methodStartIndex, indexerEndIndex, ref functionBuilder);
                }
                else // This could be a property reference, or a chain of function calls
                {
                    // Look for an instance function call next, such as in SomeStuff.ToLower()
                    var methodStartIndex = expressionRoot.IndexOf('.');
                    if (methodStartIndex == -1)
                    {
                        // We don't have a function invocation in the expression root, return null
                        return null;
                    }
 
                    // skip over the '.';
                    methodStartIndex++;
 
                    var rootEndIndex = expressionRoot.IndexOf('.');
 
                    // If this is an instance function rather than a static, then we'll capture the name of the property referenced
                    var functionReceiver = Strings.WeakIntern(expressionRoot.Slice(0, rootEndIndex).Trim());
 
                    // If propertyValue is null (we're not recursing), then we're expecting a valid property name
                    if (propertyValue == null && !IsValidPropertyName(functionReceiver))
                    {
                        // We extracted something that wasn't a valid property name, fail.
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
                    }
 
                    // If we are recursively acting on a type that has been already produced then pass that type inwards (e.g. we are interpreting a function call chain)
                    // Otherwise, the receiver of the function is a string
                    var receiverType = propertyValue?.GetType() ?? typeof(string);
 
                    functionBuilder.Receiver = functionReceiver;
                    functionBuilder.ReceiverType = receiverType;
 
                    ConstructFunction(elementLocation, expressionFunction, argumentStartIndex, methodStartIndex, ref functionBuilder);
                }
 
                return functionBuilder.Build();
            }
 
            /// <summary>
            /// Execute the function on the given instance.
            /// </summary>
            internal object Execute(object objectInstance, IPropertyProvider<T> properties, ExpanderOptions options, IElementLocation elementLocation)
            {
                object functionResult = String.Empty;
                object[] args = null;
 
                try
                {
                    // If there is no object instance, then the method invocation will be a static
                    if (objectInstance == null)
                    {
                        // Check that the function that we're going to call is valid to call
                        if (!IsStaticMethodAvailable(_receiverType, _methodMethodName))
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName);
                        }
 
                        _bindingFlags |= BindingFlags.Static;
 
                        // For our intrinsic function we need to support calling of internal methods
                        // since we don't want them to be public
                        if (_receiverType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions))
                        {
                            _bindingFlags |= BindingFlags.NonPublic;
                        }
                    }
                    else
                    {
                        // Check that the function that we're going to call is valid to call
                        if (!IsInstanceMethodAvailable(_methodMethodName))
                        {
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _methodMethodName, _receiverType.FullName);
                        }
 
                        _bindingFlags |= BindingFlags.Instance;
 
                        // The object that we're about to call methods on may have escaped characters
                        // in it, we want to operate on the unescaped string in the function, just as we
                        // want to pass arguments that are unescaped (see below)
                        if (objectInstance is string objectInstanceString)
                        {
                            objectInstance = EscapingUtilities.UnescapeAll(objectInstanceString);
                        }
                    }
 
                    // We have a methodinfo match, need to plug in the arguments
                    args = new object[_arguments.Length];
 
                    // Assemble our arguments ready for passing to our method
                    for (int n = 0; n < _arguments.Length; n++)
                    {
                        object argument = PropertyExpander<T>.ExpandPropertiesLeaveTypedAndEscaped(
                            _arguments[n],
                            properties,
                            options,
                            elementLocation,
                            _propertiesUseTracker,
                            _fileSystem);
 
                        if (argument is string argumentValue)
                        {
                            // Unescape the value since we're about to send it out of the engine and into
                            // the function being called. If a file or a directory function, fix the path
                            if (_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory)
                                || _receiverType == typeof(System.IO.Path))
                            {
                                argumentValue = FileUtilities.FixFilePath(argumentValue);
                            }
 
                            args[n] = EscapingUtilities.UnescapeAll(argumentValue);
                        }
                        else
                        {
                            args[n] = argument;
                        }
                    }
 
                    // Handle special cases where the object type needs to affect the choice of method
                    // The default binder and method invoke, often chooses the incorrect Equals and CompareTo and
                    // fails the comparison, because what we have on the right is generally a string.
                    // This special casing is to realize that its a comparison that is taking place and handle the
                    // argument type coercion accordingly; effectively pre-preparing the argument type so
                    // that it matches the left hand side ready for the default binder’s method invoke.
                    if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", _methodMethodName, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", _methodMethodName, StringComparison.OrdinalIgnoreCase)))
                    {
                        // Support comparison when the lhs is an integer
                        if (IsFloatingPointRepresentation(args[0]))
                        {
                            if (double.TryParse(objectInstance.ToString(), NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double result))
                            {
                                objectInstance = result;
                                _receiverType = objectInstance.GetType();
                            }
                        }
 
                        // change the type of the final unescaped string into the destination
                        args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture);
                    }
 
                    if (_receiverType == typeof(IntrinsicFunctions))
                    {
                        // Special case a few methods that take extra parameters that can't be passed in by the user
                        if (_methodMethodName.Equals("GetPathOfFileAbove") && args.Length == 1)
                        {
                            // Append the IElementLocation as a parameter to GetPathOfFileAbove if the user only
                            // specified the file name.  This is syntactic sugar so they don't have to always
                            // include $(MSBuildThisFileDirectory) as a parameter.
                            string startingDirectory = String.IsNullOrWhiteSpace(elementLocation.File) ? String.Empty : Path.GetDirectoryName(elementLocation.File);
 
                            args = [args[0], startingDirectory];
                        }
                    }
 
                    // If we've been asked to construct an instance, then we
                    // need to locate an appropriate constructor and invoke it
                    if (String.Equals("new", _methodMethodName, StringComparison.OrdinalIgnoreCase))
                    {
                        if (!TryExecuteWellKnownConstructorNoThrow(out functionResult, args))
                        {
                            functionResult = LateBindExecute(null /* no previous exception */, BindingFlags.Public | BindingFlags.Instance, null /* no instance for a constructor */, args, true /* is constructor */);
                        }
                    }
                    else
                    {
                        bool wellKnownFunctionSuccess = false;
 
                        try
                        {
                            // First attempt to recognize some well-known functions to avoid binding
                            // and potential first-chance MissingMethodExceptions.
                            wellKnownFunctionSuccess = TryExecuteWellKnownFunction(out functionResult, objectInstance, args);
 
                            if (!wellKnownFunctionSuccess)
                            {
                                // Some well-known functions need evaluated value from properties.
                                wellKnownFunctionSuccess = TryExecuteWellKnownFunctionWithPropertiesParam(properties, out functionResult, objectInstance, args);
                            }
                        }
                        // we need to preserve the same behavior on exceptions as the actual binder
                        catch (Exception ex)
                        {
                            string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args);
                            if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError))
                            {
                                return partiallyEvaluated;
                            }
 
                            ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message.Replace("\r\n", " "));
                        }
 
                        if (!wellKnownFunctionSuccess)
                        {
                            // Execute the function given converted arguments
                            // The only exception that we should catch to try a late bind here is missing method
                            // otherwise there is the potential of running a function twice!
                            try
                            {
                                // If there are any out parameters, try to figure out their type and create defaults for them as appropriate before calling the method.
                                if (args.Any(a => "out _".Equals(a)))
                                {
                                    IEnumerable<MethodInfo> methods = _receiverType.GetMethods(_bindingFlags).Where(m => m.Name.Equals(_methodMethodName) && m.GetParameters().Length == args.Length);
                                    functionResult = GetMethodResult(objectInstance, methods, args, 0);
                                }
                                else
                                {
                                    // If there are no out parameters, use InvokeMember using the standard binder - this will match and coerce as needed
                                    functionResult = _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture);
                                }
                            }
                            // If we're invoking a method, then there are deeper attempts that can be made to invoke the method.
                            // If not, we were asked to get a property or field but found that we cannot locate it. No further argument coercion is possible, so throw.
                            catch (MissingMethodException ex) when ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
                            {
                                // The standard binder failed, so do our best to coerce types into the arguments for the function
                                // This may happen if the types need coercion, but it may also happen if the object represents a type that contains open type parameters, that is, ContainsGenericParameters returns true.
                                functionResult = LateBindExecute(ex, _bindingFlags, objectInstance, args, false /* is not constructor */);
                            }
                        }
                    }
 
                    // If the result of the function call is a string, then we need to escape the result
                    // so that we maintain the "engine contains escaped data" state.
                    // The exception is that the user is explicitly calling MSBuild::Unescape, MSBuild::Escape, or ConvertFromBase64
                    if (functionResult is string functionResultString &&
                        !String.Equals("Unescape", _methodMethodName, StringComparison.OrdinalIgnoreCase) &&
                        !String.Equals("Escape", _methodMethodName, StringComparison.OrdinalIgnoreCase) &&
                        !String.Equals("ConvertFromBase64", _methodMethodName, StringComparison.OrdinalIgnoreCase))
                    {
                        functionResult = EscapingUtilities.Escape(functionResultString);
                    }
 
                    // We have nothing left to parse, so we'll return what we have
                    if (String.IsNullOrEmpty(_remainder))
                    {
                        return functionResult;
                    }
 
                    // Recursively expand the remaining property body after execution
                    return PropertyExpander<T>.ExpandPropertyBody(
                        _remainder,
                        functionResult,
                        properties,
                        options,
                        elementLocation,
                        _propertiesUseTracker,
                        _fileSystem);
                }
 
                // Exceptions coming from the actual function called are wrapped in a TargetInvocationException
                catch (TargetInvocationException ex)
                {
                    // We ended up with something other than a function expression
                    string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args);
                    if (options.HasFlag(ExpanderOptions.LeavePropertiesUnexpandedOnError))
                    {
                        // If the caller wants to ignore errors (in a log statement for example), just return the partially evaluated value
                        return partiallyEvaluated;
                    }
                    ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.InnerException.Message.Replace("\r\n", " "));
                    return null;
                }
 
                // Any other exception was thrown by trying to call it
                catch (Exception ex) when (!ExceptionHandling.NotExpectedFunctionException(ex))
                {
                    // If there's a :: in the expression, they were probably trying for a static function
                    // invocation. Give them some more relevant info in that case
                    if (s_invariantCompareInfo.IndexOf(_expression, "::", CompareOptions.OrdinalIgnoreCase) > -1)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", _expression, ex.Message.Replace("Microsoft.Build.Evaluation.IntrinsicFunctions.", "[MSBuild]::"));
                    }
                    else
                    {
                        // We ended up with something other than a function expression
                        string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _methodMethodName, args);
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message);
                    }
 
                    return null;
                }
            }
 
            private bool TryExecuteWellKnownFunctionWithPropertiesParam(IPropertyProvider<T> properties, out object returnVal, object objectInstance, object[] args)
            {
                returnVal = null;
 
                if (_receiverType == typeof(IntrinsicFunctions))
                {
                    if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.RegisterBuildCheck), StringComparison.OrdinalIgnoreCase))
                    {
                        string projectPath = properties.GetProperty("MSBuildProjectFullPath")?.EvaluatedValue ?? string.Empty;
                        ErrorUtilities.VerifyThrow(_loggingContext != null, $"The logging context is missed. {nameof(IntrinsicFunctions.RegisterBuildCheck)} can not be invoked.");
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = IntrinsicFunctions.RegisterBuildCheck(projectPath, arg0, _loggingContext);
                            return true;
                        }
                    }
                }
 
                return false;
            }
 
            private object GetMethodResult(object objectInstance, IEnumerable<MethodInfo> methods, object[] args, int index)
            {
                for (int i = index; i < args.Length; i++)
                {
                    if (args[i].Equals("out _"))
                    {
                        object toReturn = null;
                        foreach (MethodInfo method in methods)
                        {
                            Type t = method.GetParameters()[i].ParameterType;
                            args[i] = t.IsValueType ? Activator.CreateInstance(t) : null;
                            object currentReturnValue = GetMethodResult(objectInstance, methods, args, i + 1);
                            if (currentReturnValue is not null)
                            {
                                if (toReturn is null)
                                {
                                    toReturn = currentReturnValue;
                                }
                                else if (!toReturn.Equals(currentReturnValue))
                                {
                                    // There were multiple methods that seemed viable and gave different results. We can't differentiate between them so throw.
                                    ErrorUtilities.ThrowArgument("CouldNotDifferentiateBetweenCompatibleMethods", _methodMethodName, args.Length);
                                    return null;
                                }
                            }
                        }
 
                        return toReturn;
                    }
                }
 
                try
                {
                    return _receiverType.InvokeMember(_methodMethodName, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture) ?? "null";
                }
                catch (Exception)
                {
                    // This isn't a viable option, but perhaps another set of parameters will work.
                    return null;
                }
            }
 
            /// <summary>
            /// Shortcut to avoid calling into binding if we recognize some most common functions.
            /// Binding is expensive and throws first-chance MissingMethodExceptions, which is
            /// bad for debugging experience and has a performance cost.
            /// A typical binding operation with exception can take ~1.500 ms; this call is ~0.050 ms
            /// (rough numbers just for comparison).
            /// See https://github.com/dotnet/msbuild/issues/2217.
            /// </summary>
            /// <param name="returnVal">The value returned from the function call.</param>
            /// <param name="objectInstance">Object that the function is called on.</param>
            /// <param name="args">arguments.</param>
            /// <returns>True if the well known function call binding was successful.</returns>
            private bool TryExecuteWellKnownFunction(out object returnVal, object objectInstance, object[] args)
            {
                returnVal = null;
 
                if (objectInstance is string text)
                {
                    if (string.Equals(_methodMethodName, nameof(string.StartsWith), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.StartsWith(arg0);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Replace), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArgs(args, out string arg0, out string arg1))
                        {
                            returnVal = text.Replace(arg0, arg1);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Contains), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.Contains(arg0);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.ToUpperInvariant), StringComparison.OrdinalIgnoreCase))
                    {
                        if (args.Length == 0)
                        {
                            returnVal = text.ToUpperInvariant();
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.ToLowerInvariant), StringComparison.OrdinalIgnoreCase))
                    {
                        if (args.Length == 0)
                        {
                            returnVal = text.ToLowerInvariant();
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.EndsWith), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.EndsWith(arg0);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.ToLower), StringComparison.OrdinalIgnoreCase))
                    {
                        if (args.Length == 0)
                        {
                            returnVal = text.ToLower();
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.IndexOf), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArgs(args, out string arg0, out StringComparison arg1))
                        {
                            returnVal = text.IndexOf(arg0, arg1);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.IndexOfAny), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.IndexOfAny(arg0.ToCharArray());
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.LastIndexOf), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.LastIndexOf(arg0);
                            return true;
                        }
                        else if (TryGetArgs(args, out arg0, out int startIndex))
                        {
                            returnVal = text.LastIndexOf(arg0, startIndex);
                            return true;
                        }
                        else if (TryGetArgs(args, out arg0, out StringComparison arg1))
                        {
                            returnVal = text.LastIndexOf(arg0, arg1);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.LastIndexOfAny), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.LastIndexOfAny(arg0.ToCharArray());
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Length), StringComparison.OrdinalIgnoreCase))
                    {
                        if (args.Length == 0)
                        {
                            returnVal = text.Length;
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Substring), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out int startIndex))
                        {
                            returnVal = text.Substring(startIndex);
                            return true;
                        }
                        else if (TryGetArgs(args, out startIndex, out int length))
                        {
                            returnVal = text.Substring(startIndex, length);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Split), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string separator) && separator.Length == 1)
                        {
                            returnVal = text.Split(separator[0]);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.PadLeft), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out int totalWidth))
                        {
                            returnVal = text.PadLeft(totalWidth);
                            return true;
                        }
                        else if (TryGetArgs(args, out totalWidth, out string paddingChar) && paddingChar.Length == 1)
                        {
                            returnVal = text.PadLeft(totalWidth, paddingChar[0]);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.PadRight), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out int totalWidth))
                        {
                            returnVal = text.PadRight(totalWidth);
                            return true;
                        }
                        else if (TryGetArgs(args, out totalWidth, out string paddingChar) && paddingChar.Length == 1)
                        {
                            returnVal = text.PadRight(totalWidth, paddingChar[0]);
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.TrimStart), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string trimChars) && trimChars.Length > 0)
                        {
                            returnVal = text.TrimStart(trimChars.ToCharArray());
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.TrimEnd), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string trimChars) && trimChars.Length > 0)
                        {
                            returnVal = text.TrimEnd(trimChars.ToCharArray());
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, "get_Chars", StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out int index))
                        {
                            returnVal = text[index];
                            return true;
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(string.Equals), StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out string arg0))
                        {
                            returnVal = text.Equals(arg0);
                            return true;
                        }
                    }
                }
                else if (objectInstance is string[] stringArray)
                {
                    if (string.Equals(_methodMethodName, "GetValue", StringComparison.OrdinalIgnoreCase))
                    {
                        if (TryGetArg(args, out int index))
                        {
                            returnVal = stringArray[index];
                            return true;
                        }
                    }
                }
                else if (objectInstance == null) // Calling a well-known static function
                {
                    if (_receiverType == typeof(string))
                    {
                        if (string.Equals(_methodMethodName, nameof(string.IsNullOrWhiteSpace), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = string.IsNullOrWhiteSpace(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(string.IsNullOrEmpty), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = string.IsNullOrEmpty(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(string.Copy), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = arg0;
                                return true;
                            }
                        }
                    }
                    else if (_receiverType == typeof(Math))
                    {
                        if (string.Equals(_methodMethodName, nameof(Math.Max), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out var arg0, out double arg1))
                            {
                                returnVal = Math.Max(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Math.Min), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out double arg0, out var arg1))
                            {
                                returnVal = Math.Min(arg0, arg1);
                                return true;
                            }
                        }
                    }
                    else if (_receiverType == typeof(IntrinsicFunctions))
                    {
                        if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.EnsureTrailingSlash), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.EnsureTrailingSlash(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.ValueOrDefault), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.ValueOrDefault(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.NormalizePath), StringComparison.OrdinalIgnoreCase))
                        {
                            if (ElementsOfType(args, typeof(string)))
                            {
                                returnVal = IntrinsicFunctions.NormalizePath(Array.ConvertAll(args, o => (string)o));
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetDirectoryNameOfFileAbove), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.GetDirectoryNameOfFileAbove(arg0, arg1, _fileSystem);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetRegistryValueFromView), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length >= 4 &&
                                TryGetArgs(args, out string arg0, out string arg1, enforceLength: false))
                            {
                                returnVal = IntrinsicFunctions.GetRegistryValueFromView(arg0, arg1, args[2], new ArraySegment<object>(args, 3, args.Length - 3));
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.IsRunningFromVisualStudio), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.IsRunningFromVisualStudio();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Escape), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.Escape(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Unescape), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.Unescape(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetPathOfFileAbove), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.GetPathOfFileAbove(arg0, arg1, _fileSystem);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Add), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Add, IntrinsicFunctions.Add, out returnVal))
                            {
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Subtract), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Subtract, IntrinsicFunctions.Subtract, out returnVal))
                            {
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Multiply), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Multiply, IntrinsicFunctions.Multiply, out returnVal))
                            {
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Divide), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Divide, IntrinsicFunctions.Divide, out returnVal))
                            {
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.Modulo), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryExecuteArithmeticOverload(args, IntrinsicFunctions.Modulo, IntrinsicFunctions.Modulo, out returnVal))
                            {
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetCurrentToolsDirectory), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetCurrentToolsDirectory();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetToolsDirectory32), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetToolsDirectory32();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetToolsDirectory64), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetToolsDirectory64();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetMSBuildSDKsPath), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetMSBuildSDKsPath();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetVsInstallRoot), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetVsInstallRoot();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetMSBuildExtensionsPath), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetMSBuildExtensionsPath();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetProgramFiles32), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = IntrinsicFunctions.GetProgramFiles32();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionEquals), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionEquals(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionNotEquals), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionNotEquals(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionGreaterThan), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionGreaterThan(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionGreaterThanOrEquals), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionGreaterThanOrEquals(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionLessThan), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionLessThan(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.VersionLessThanOrEquals), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.VersionLessThanOrEquals(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetTargetFrameworkIdentifier), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.GetTargetFrameworkIdentifier(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetTargetFrameworkVersion), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.GetTargetFrameworkVersion(arg0);
                                return true;
                            }
                            if (TryGetArgs(args, out string arg1, out int arg2))
                            {
                                returnVal = IntrinsicFunctions.GetTargetFrameworkVersion(arg1, arg2);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.IsTargetFrameworkCompatible), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out string arg1))
                            {
                                returnVal = IntrinsicFunctions.IsTargetFrameworkCompatible(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetTargetPlatformIdentifier), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.GetTargetPlatformIdentifier(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.GetTargetPlatformVersion), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.GetTargetPlatformVersion(arg0);
                                return true;
                            }
                            if (TryGetArgs(args, out string arg1, out int arg2))
                            {
                                returnVal = IntrinsicFunctions.GetTargetPlatformVersion(arg1, arg2);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.ConvertToBase64), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.ConvertToBase64(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.ConvertFromBase64), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.ConvertFromBase64(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.StableStringHash), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                // Prevent loading methods refs from StringTools if ChangeWave opted out.
                                returnVal = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10)
                                    ? IntrinsicFunctions.StableStringHash(arg0)
                                    : IntrinsicFunctions.StableStringHashLegacy(arg0);
                                return true;
                            }
                            else if (TryGetArgs(args, out string arg1, out string arg2) && Enum.TryParse<IntrinsicFunctions.StringHashingAlgorithm>(arg2, true, out var hashAlgorithm))
                            {
                                returnVal = IntrinsicFunctions.StableStringHash(arg1, hashAlgorithm);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.AreFeaturesEnabled), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out Version arg0))
                            {
                                returnVal = IntrinsicFunctions.AreFeaturesEnabled(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.SubstringByAsciiChars), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out string arg0, out int arg1, out int arg2))
                            {
                                returnVal = IntrinsicFunctions.SubstringByAsciiChars(arg0, arg1, arg2);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.CheckFeatureAvailability), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.CheckFeatureAvailability(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.BitwiseOr), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.BitwiseOr(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.BitwiseAnd), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.BitwiseAnd(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.BitwiseXor), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.BitwiseXor(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.BitwiseNot), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0))
                            {
                                returnVal = IntrinsicFunctions.BitwiseNot(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.LeftShift), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.LeftShift(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.RightShift), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.RightShift(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.RightShiftUnsigned), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArgs(args, out int arg0, out int arg1))
                            {
                                returnVal = IntrinsicFunctions.RightShiftUnsigned(arg0, arg1);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.NormalizeDirectory), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.NormalizeDirectory(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(IntrinsicFunctions.IsOSPlatform), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = IntrinsicFunctions.IsOSPlatform(arg0);
                                return true;
                            }
                        }
                    }
                    else if (_receiverType == typeof(Path))
                    {
                        if (string.Equals(_methodMethodName, nameof(Path.Combine), StringComparison.OrdinalIgnoreCase))
                        {
                            string arg0, arg1, arg2, arg3;
 
                            // Combine has fast implementations for up to 4 parameters: https://github.com/dotnet/corefx/blob/2c55db90d622fa6279184e6243f0470a3755d13c/src/Common/src/CoreLib/System/IO/Path.cs#L293-L317
                            switch (args.Length)
                            {
                                case 0:
                                    return false;
                                case 1:
                                    if (TryGetArg(args, out arg0))
                                    {
                                        returnVal = Path.Combine(arg0);
                                        return true;
                                    }
                                    break;
                                case 2:
                                    if (TryGetArgs(args, out arg0, out arg1))
                                    {
                                        returnVal = Path.Combine(arg0, arg1);
                                        return true;
                                    }
                                    break;
                                case 3:
                                    if (TryGetArgs(args, out arg0, out arg1, out arg2))
                                    {
                                        returnVal = Path.Combine(arg0, arg1, arg2);
                                        return true;
                                    }
                                    break;
                                case 4:
                                    if (TryGetArgs(args, out arg0, out arg1, out arg2, out arg3))
                                    {
                                        returnVal = Path.Combine(arg0, arg1, arg2, arg3);
                                        return true;
                                    }
                                    break;
                                default:
                                    if (ElementsOfType(args, typeof(string)))
                                    {
                                        returnVal = Path.Combine(Array.ConvertAll(args, o => (string)o));
                                        return true;
                                    }
                                    break;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = Path.DirectorySeparatorChar;
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.GetFullPath), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Path.GetFullPath(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.IsPathRooted), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Path.IsPathRooted(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.GetTempPath), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = Path.GetTempPath();
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.GetFileName), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Path.GetFileName(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.GetDirectoryName), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Path.GetDirectoryName(arg0);
                                return true;
                            }
                        }
                        else if (string.Equals(_methodMethodName, nameof(Path.GetFileNameWithoutExtension), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Path.GetFileNameWithoutExtension(arg0);
                                return true;
                            }
                        }
                    }
                    else if (_receiverType == typeof(Version))
                    {
                        if (string.Equals(_methodMethodName, nameof(Version.Parse), StringComparison.OrdinalIgnoreCase))
                        {
                            if (TryGetArg(args, out string arg0))
                            {
                                returnVal = Version.Parse(arg0);
                                return true;
                            }
                        }
                    }
                    else if (_receiverType == typeof(Guid))
                    {
                        if (string.Equals(_methodMethodName, nameof(Guid.NewGuid), StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 0)
                            {
                                returnVal = Guid.NewGuid();
                                return true;
                            }
                        }
                    }
                    else if (string.Equals(_methodMethodName, nameof(Regex.Replace), StringComparison.OrdinalIgnoreCase) && args.Length == 3)
                    {
                        if (TryGetArg([args[0]], out string arg1) && TryGetArg([args[1]], out string arg2) && TryGetArg([args[2]], out string arg3))
                        {
                            returnVal = Regex.Replace(arg1, arg2, arg3);
                            return true;
                        }
                    }
                }
                else if (string.Equals(_methodMethodName, nameof(Version.ToString), StringComparison.OrdinalIgnoreCase) && objectInstance is Version v)
                {
                    if (TryGetArg(args, out int arg0))
                    {
                        returnVal = v.ToString(arg0);
                        return true;
                    }
                }
                else if (string.Equals(_methodMethodName, nameof(Int32.ToString), StringComparison.OrdinalIgnoreCase) && objectInstance is int i)
                {
                    if (TryGetArg(args, out string arg0))
                    {
                        returnVal = i.ToString(arg0);
                        return true;
                    }
                }
                if (Traits.Instance.LogPropertyFunctionsRequiringReflection)
                {
                    LogFunctionCall("PropertyFunctionsRequiringReflection", objectInstance, args);
                }
 
                return false;
            }
 
            /// <summary>
            /// Shortcut to avoid calling into binding if we recognize some most common constructors.
            /// Analogous to TryExecuteWellKnownFunction but guaranteed to not throw.
            /// </summary>
            /// <param name="returnVal">The instance as created by the constructor call.</param>
            /// <param name="args">Arguments.</param>
            /// <returns>True if the well known constructor call binding was successful.</returns>
            private bool TryExecuteWellKnownConstructorNoThrow(out object returnVal, object[] args)
            {
                returnVal = null;
 
                if (_receiverType == typeof(string))
                {
                    if (args.Length == 0)
                    {
                        returnVal = String.Empty;
                        return true;
                    }
                    if (TryGetArg(args, out string arg0))
                    {
                        returnVal = arg0;
                        return true;
                    }
                }
                return false;
            }
 
            private bool ElementsOfType(object[] args, Type type)
            {
                for (var i = 0; i < args.Length; i++)
                {
                    if (args[i].GetType() != type)
                    {
                        return false;
                    }
                }
 
                return true;
            }
 
            private static bool TryGetArgs(object[] args, out string arg0, out string arg1, bool enforceLength = true)
            {
                arg0 = null;
                arg1 = null;
 
                if (enforceLength && args.Length != 2)
                {
                    return false;
                }
 
                if (args[0] is string value0 &&
                    args[1] is string value1)
                {
                    arg0 = value0;
                    arg1 = value1;
 
                    return true;
                }
 
                return false;
            }
 
            private bool TryGetArgs(object[] args, out string arg0, out string arg1, out string arg2)
            {
                arg0 = null;
                arg1 = null;
                arg2 = null;
 
                if (args.Length != 3)
                {
                    return false;
                }
 
                if (args[0] is string value0 &&
                    args[1] is string value1 &&
                    args[2] is string value2)
                {
                    arg0 = value0;
                    arg1 = value1;
                    arg2 = value2;
 
                    return true;
                }
 
                return false;
            }
 
            private bool TryGetArgs(object[] args, out string arg0, out string arg1, out string arg2, out string arg3)
            {
                arg0 = null;
                arg1 = null;
                arg2 = null;
                arg3 = null;
 
                if (args.Length != 4)
                {
                    return false;
                }
 
                if (args[0] is string value0 &&
                    args[1] is string value1 &&
                    args[2] is string value2 &&
                    args[3] is string value3)
                {
                    arg0 = value0;
                    arg1 = value1;
                    arg2 = value2;
                    arg3 = value3;
 
                    return true;
                }
 
                return false;
            }
 
            private bool TryGetArgs(object[] args, out string arg0, out string arg1)
            {
                arg0 = null;
                arg1 = null;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                if (args[0] is string value0 &&
                    args[1] is string value1)
                {
                    arg0 = value0;
                    arg1 = value1;
 
                    return true;
                }
 
                return false;
            }
 
            private static bool TryGetArgs(object[] args, out string arg0, out int arg1, out int arg2)
            {
                arg0 = null;
                arg1 = 0;
                arg2 = 0;
 
                if (args.Length != 3)
                {
                    return false;
                }
 
                var value1 = args[1] as string;
                var value2 = args[2] as string;
                arg0 = args[0] as string;
                if (value1 != null &&
                    value2 != null &&
                    arg0 != null &&
                    int.TryParse(value1, out arg1) &&
                    int.TryParse(value2, out arg2))
                {
                    return true;
                }
 
                return false;
            }
 
            private static bool TryGetArg(object[] args, out int arg0)
            {
                if (args.Length != 1)
                {
                    arg0 = 0;
                    return false;
                }
 
                return TryConvertToInt(args[0], out arg0);
            }
 
            private static bool TryGetArg(object[] args, out Version arg0)
            {
                if (args.Length != 1)
                {
                    arg0 = default;
                    return false;
                }
 
                return TryConvertToVersion(args[0], out arg0);
            }
 
            private static bool TryConvertToVersion(object value, out Version arg0)
            {
                string val = value as string;
 
                if (string.IsNullOrEmpty(val) || !Version.TryParse(val, out arg0))
                {
                    arg0 = default;
                    return false;
                }
 
                return true;
            }
 
            /// <summary>
            /// Try to convert value to int.
            /// </summary>
            internal static bool TryConvertToInt(object value, out int arg)
            {
                switch (value)
                {
                    case double d:
                        if (d >= int.MinValue && d <= int.MaxValue)
                        {
                            arg = Convert.ToInt32(d);
                            if (Math.Abs(arg - d) == 0)
                            {
                                return true;
                            }
                        }
 
                        break;
                    case long l:
                        if (l >= int.MinValue && l <= int.MaxValue)
                        {
                            arg = Convert.ToInt32(l);
                            return true;
                        }
 
                        break;
                    case int i:
                        arg = i;
                        return true;
                    case string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out arg):
                        return true;
                }
 
                arg = 0;
                return false;
            }
 
            /// <summary>
            /// Try to convert value to long.
            /// </summary>
            internal static bool TryConvertToLong(object value, out long arg)
            {
                switch (value)
                {
                    case double d:
                        if (d >= long.MinValue && d <= long.MaxValue)
                        {
                            arg = (long)d;
                            if (Math.Abs(arg - d) == 0)
                            {
                                return true;
                            }
                        }
 
                        break;
                    case long l:
                        arg = l;
                        return true;
                    case int i:
                        arg = i;
                        return true;
                    case string s when long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out arg):
                        return true;
                }
 
                arg = 0;
                return false;
            }
 
            /// <summary>
            /// Try to convert value to double.
            /// </summary>
            internal static bool TryConvertToDouble(object value, out double arg)
            {
                switch (value)
                {
                    case double unboxed:
                        arg = unboxed;
                        return true;
                    case long l:
                        arg = l;
                        return true;
                    case int i:
                        arg = i;
                        return true;
                    case string str when double.TryParse(str, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out arg):
                        return true;
                    default:
                        arg = 0;
                        return false;
                }
            }
 
            private static bool TryGetArg(object[] args, out string arg0)
            {
                if (args.Length != 1)
                {
                    arg0 = null;
                    return false;
                }
 
                arg0 = args[0] as string;
                return arg0 != null;
            }
 
            private static bool TryGetArgs(object[] args, out string arg0, out StringComparison arg1)
            {
                if (args.Length != 2)
                {
                    arg0 = null;
                    arg1 = default;
 
                    return false;
                }
 
                arg0 = args[0] as string;
 
                // reject enums as ints. In C# this would require a cast, which is not supported in msbuild expressions
                if (arg0 == null || !(args[1] is string comparisonTypeName) || int.TryParse(comparisonTypeName, out _))
                {
                    arg1 = default;
                    return false;
                }
 
                // Allow fully-qualified enum, e.g. "System.StringComparison.OrdinalIgnoreCase"
                if (comparisonTypeName.Contains('.'))
                {
                    comparisonTypeName = comparisonTypeName.Replace("System.StringComparison.", "").Replace("StringComparison.", "");
                }
 
                return Enum.TryParse(comparisonTypeName, out arg1);
            }
 
            private static bool TryGetArgs(object[] args, out int arg0)
            {
                arg0 = 0;
 
                if (args.Length != 1)
                {
                    return false;
                }
 
                return TryConvertToInt(args[0], out arg0);
            }
 
            private static bool TryGetArgs(object[] args, out int arg0, out int arg1)
            {
                arg0 = 0;
                arg1 = 0;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                return TryConvertToInt(args[0], out arg0) &&
                       TryConvertToInt(args[1], out arg1);
            }
 
            private static bool TryGetArgs(object[] args, out double arg0, out double arg1)
            {
                arg0 = 0;
                arg1 = 0;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                return TryConvertToDouble(args[0], out arg0) &&
                       TryConvertToDouble(args[1], out arg1);
            }
 
            private static bool TryGetArgs(object[] args, out int arg0, out string arg1)
            {
                arg0 = 0;
                arg1 = null;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                arg1 = args[1] as string;
                if (arg1 == null && args[1] is char ch)
                {
                    arg1 = ch.ToString();
                }
 
                if (TryConvertToInt(args[0], out arg0) &&
                    arg1 != null)
                {
                    return true;
                }
 
                return false;
            }
 
            private static bool TryGetArgs(object[] args, out string arg0, out int arg1)
            {
                arg0 = null;
                arg1 = 0;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                var value1 = args[1] as string;
                arg0 = args[0] as string;
                if (value1 != null &&
                    arg0 != null &&
                    int.TryParse(value1, out arg1))
                {
                    return true;
                }
 
                return false;
            }
 
            private static bool IsFloatingPointRepresentation(object value)
            {
                return value is double || (value is string str && double.TryParse(str, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double _));
            }
 
            private static bool TryExecuteArithmeticOverload(object[] args, Func<long, long, long> integerOperation, Func<double, double, double> realOperation, out object resultValue)
            {
                resultValue = null;
 
                if (args.Length != 2)
                {
                    return false;
                }
 
                if (TryConvertToLong(args[0], out long argLong0) && TryConvertToLong(args[1], out long argLong1))
                {
                    resultValue = integerOperation(argLong0, argLong1);
                    return true;
                }
 
                if (TryConvertToDouble(args[0], out double argDouble0) && TryConvertToDouble(args[1], out double argDouble1))
                {
                    resultValue = realOperation(argDouble0, argDouble1);
                    return true;
                }
 
                return false;
            }
 
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            private void LogFunctionCall(string fileName, object objectInstance, object[] args)
            {
                var logFile = Path.Combine(Directory.GetCurrentDirectory(), fileName);
 
                var argSignature = args != null
                    ? string.Join(", ", args.Select(a => a?.GetType().Name ?? "null"))
                    : string.Empty;
 
                File.AppendAllText(logFile, $"ReceiverType={_receiverType?.FullName}; ObjectInstanceType={objectInstance?.GetType().FullName}; MethodName={_methodMethodName}({argSignature})\n");
            }
 
            /// <summary>
            /// Given a type name and method name, try to resolve the type.
            /// </summary>
            /// <param name="typeName">May be full name or assembly qualified name.</param>
            /// <param name="simpleMethodName">simple name of the method.</param>
            /// <returns></returns>
            private static Type GetTypeForStaticMethod(string typeName, string simpleMethodName)
            {
                Type receiverType;
                Tuple<string, Type> cachedTypeInformation;
 
                // If we don't have a type name, we already know that we won't be able to find a type.
                // Go ahead and return here -- otherwise the Type.GetType() calls below will throw.
                if (string.IsNullOrWhiteSpace(typeName))
                {
                    return null;
                }
 
                // Check if the type is in the allowlist cache. If it is, use it or load it.
                cachedTypeInformation = AvailableStaticMethods.GetTypeInformationFromTypeCache(typeName, simpleMethodName);
                if (cachedTypeInformation != null)
                {
                    // We need at least one of these set
                    ErrorUtilities.VerifyThrow(cachedTypeInformation.Item1 != null || cachedTypeInformation.Item2 != null, "Function type information needs either string or type represented.");
 
                    // If we have the type information in Type form, then just return that
                    if (cachedTypeInformation.Item2 != null)
                    {
                        return cachedTypeInformation.Item2;
                    }
                    else if (cachedTypeInformation.Item1 != null)
                    {
                        // This is a case where the Type is not available at compile time, so
                        // we are forced to bind by name instead
                        var assemblyQualifiedTypeName = cachedTypeInformation.Item1;
 
                        // Get the type from the assembly qualified type name from AvailableStaticMethods
                        receiverType = Type.GetType(assemblyQualifiedTypeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
 
                        // If the type information from the cache is not loadable, it means the cache information got corrupted somehow
                        // Throw here to prevent adding null types in the cache
                        ErrorUtilities.VerifyThrowInternalNull(receiverType, $"Type information for {typeName} was present in the allowlist cache as {assemblyQualifiedTypeName} but the type could not be loaded.");
 
                        // If we've used it once, chances are that we'll be using it again
                        // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table
                        AvailableStaticMethods.TryAdd(typeName, simpleMethodName, new Tuple<string, Type>(assemblyQualifiedTypeName, receiverType));
 
                        return receiverType;
                    }
                }
 
                // Get the type from mscorlib (or the currently running assembly)
                receiverType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
 
                if (receiverType != null)
                {
                    // DO NOT CACHE THE TYPE HERE!
                    // We don't add the resolved type here in the AvailableStaticMethods table. This is because that table is used
                    // during function parse, but only later during execution do we check for the ability to call specific methods on specific types.
                    // Caching it here would load any type into the allow list.
                    return receiverType;
                }
 
                // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1.
                // This environment variable must not be cached - it should be dynamically settable while the application is executing.
                if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1")
                {
                    // We didn't find the type, so go probing. First in System
                    receiverType = GetTypeFromAssembly(typeName, "System");
 
                    // Next in System.Core
                    if (receiverType == null)
                    {
                        receiverType = GetTypeFromAssembly(typeName, "System.Core");
                    }
 
                    // We didn't find the type, so try to find it using the namespace
                    if (receiverType == null)
                    {
                        receiverType = GetTypeFromAssemblyUsingNamespace(typeName);
                    }
 
                    if (receiverType != null)
                    {
                        // If we've used it once, chances are that we'll be using it again
                        // We can cache the type here, since all functions are enabled
                        AvailableStaticMethods.TryAdd(typeName, new Tuple<string, Type>(typeName, receiverType));
                    }
                }
 
                return receiverType;
            }
 
            /// <summary>
            /// Gets the specified type using the namespace to guess the assembly that its in.
            /// </summary>
            private static Type GetTypeFromAssemblyUsingNamespace(string typeName)
            {
                string baseName = typeName;
                int assemblyNameEnd = baseName.Length;
 
                // If the string has no dot, or is nothing but a dot, we have no
                // namespace to look for, so we can't help.
                if (assemblyNameEnd <= 0)
                {
                    return null;
                }
 
                // We will work our way up the namespace looking for an assembly that matches
                while (assemblyNameEnd > 0)
                {
                    string candidateAssemblyName = baseName.Substring(0, assemblyNameEnd);
 
                    // Try to load the assembly with the computed name
                    Type foundType = GetTypeFromAssembly(typeName, candidateAssemblyName);
 
                    if (foundType != null)
                    {
                        // We have a match, so get the type from that assembly
                        return foundType;
                    }
                    else
                    {
                        // Keep looking as we haven't found a match yet
                        baseName = candidateAssemblyName;
                        assemblyNameEnd = baseName.LastIndexOf('.');
                    }
                }
 
                // We didn't find it, so we need to give up
                return null;
            }
 
            /// <summary>
            /// Get the specified type from the assembly partial name supplied.
            /// </summary>
            [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")]
            private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName)
            {
                Type objectType = null;
 
                // Try to load the assembly with the computed name
#if FEATURE_GAC
#pragma warning disable 618, 612
                // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since
                // Assembly.Load requires the full assembly name to be passed to it.
                // Therefore we must ignore the deprecated warning.
                Assembly candidateAssembly = Assembly.LoadWithPartialName(candidateAssemblyName);
#pragma warning restore 618, 612
#else
                Assembly candidateAssembly = null;
                try
                {
                    candidateAssembly = Assembly.Load(new AssemblyName(candidateAssemblyName));
                }
                catch (FileNotFoundException)
                {
                    // Swallow the error; LoadWithPartialName returned null when the partial name
                    // was not found but Load throws.  Either way we'll provide a nice "couldn't
                    // resolve this" error later.
                }
#endif
 
                if (candidateAssembly != null)
                {
                    objectType = candidateAssembly.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */);
                }
 
                return objectType;
            }
 
            /// <summary>
            /// Extracts the name, arguments, binding flags, and invocation type for an indexer
            /// Also extracts the remainder of the expression that is not part of this indexer.
            /// </summary>
            private static void ConstructIndexerFunction(string expressionFunction, IElementLocation elementLocation, object propertyValue, int methodStartIndex, int indexerEndIndex, ref FunctionBuilder<T> functionBuilder)
            {
                string argumentsContent = expressionFunction.Substring(1, indexerEndIndex - 1);
                string remainder = expressionFunction.Substring(methodStartIndex);
                string functionName;
                string[] functionArguments;
 
                // If there are no arguments, then just create an empty array
                if (String.IsNullOrEmpty(argumentsContent))
                {
                    functionArguments = [];
                }
                else
                {
                    // We will keep empty entries so that we can treat them as null
                    functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent);
                }
 
                // choose the name of the function based on the type of the object that we
                // are using.
                if (propertyValue is Array)
                {
                    functionName = "GetValue";
                }
                else if (propertyValue is string)
                {
                    functionName = "get_Chars";
                }
                else // a regular indexer
                {
                    functionName = "get_Item";
                }
 
                functionBuilder.Name = functionName;
                functionBuilder.Arguments = functionArguments;
                functionBuilder.BindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.InvokeMethod;
                functionBuilder.Remainder = remainder;
            }
 
            /// <summary>
            /// Extracts the name, arguments, binding flags, and invocation type for a static or instance function.
            /// Also extracts the remainder of the expression that is not part of this function.
            /// </summary>
            private static void ConstructFunction(IElementLocation elementLocation, string expressionFunction, int argumentStartIndex, int methodStartIndex, ref FunctionBuilder<T> functionBuilder)
            {
                // The unevaluated and unexpanded arguments for this function
                string[] functionArguments;
 
                // The name of the function that will be invoked
                ReadOnlySpan<char> functionName;
 
                // What's left of the expression once the function has been constructed
                ReadOnlySpan<char> remainder = ReadOnlySpan<char>.Empty;
 
                // The binding flags that we will use for this function's execution
                BindingFlags defaultBindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public;
 
                ReadOnlySpan<char> expressionFunctionAsSpan = expressionFunction.AsSpan();
 
                ReadOnlySpan<char> expressionSubstringAsSpan = argumentStartIndex > -1 ? expressionFunctionAsSpan.Slice(methodStartIndex, argumentStartIndex - methodStartIndex) : ReadOnlySpan<char>.Empty;
 
                // There are arguments that need to be passed to the function
                if (argumentStartIndex > -1 && !expressionSubstringAsSpan.Contains(".".AsSpan(), StringComparison.OrdinalIgnoreCase))
                {
                    // separate the function and the arguments
                    functionName = expressionSubstringAsSpan.Trim();
 
                    // Skip the '('
                    argumentStartIndex++;
 
                    // Scan for the matching closing bracket, skipping any nested ones
                    int argumentsEndIndex = ScanForClosingParenthesis(expressionFunction, argumentStartIndex, out _, out _);
 
                    if (argumentsEndIndex == -1)
                    {
                        ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis"));
                    }
 
                    // We have been asked for a method invocation
                    defaultBindingFlags |= BindingFlags.InvokeMethod;
 
                    // It may be that there are '()' but no actual arguments content
                    if (argumentStartIndex == expressionFunction.Length - 1)
                    {
                        functionArguments = [];
                    }
                    else
                    {
                        // we have content within the '()' so let's extract and deal with it
                        string argumentsContent = expressionFunction.Substring(argumentStartIndex, argumentsEndIndex - argumentStartIndex);
 
                        // If there are no arguments, then just create an empty array
                        if (string.IsNullOrEmpty(argumentsContent))
                        {
                            functionArguments = [];
                        }
                        else
                        {
                            // We will keep empty entries so that we can treat them as null
                            functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent);
                        }
 
                        remainder = expressionFunctionAsSpan.Slice(argumentsEndIndex + 1).Trim();
                    }
                }
                else
                {
                    int nextMethodIndex = expressionFunction.IndexOf('.', methodStartIndex);
                    int methodLength = expressionFunction.Length - methodStartIndex;
                    int indexerIndex = expressionFunction.IndexOf('[', methodStartIndex);
 
                    // We don't want to consume the indexer
                    if (indexerIndex >= 0 && indexerIndex < nextMethodIndex)
                    {
                        nextMethodIndex = indexerIndex;
                    }
 
                    functionArguments = [];
 
                    if (nextMethodIndex > 0)
                    {
                        methodLength = nextMethodIndex - methodStartIndex;
                        remainder = expressionFunctionAsSpan.Slice(nextMethodIndex).Trim();
                    }
 
                    ReadOnlySpan<char> netPropertyName = expressionFunctionAsSpan.Slice(methodStartIndex, methodLength).Trim();
 
                    ProjectErrorUtilities.VerifyThrowInvalidProject(netPropertyName.Length > 0, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
 
                    // We have been asked for a property or a field
                    defaultBindingFlags |= (BindingFlags.GetProperty | BindingFlags.GetField);
 
                    functionName = netPropertyName;
                }
 
                // either there are no functions left or what we have is another function or an indexer
                if (remainder.IsEmpty || remainder[0] == '.' || remainder[0] == '[')
                {
                    functionBuilder.Name = functionName.ToString();
                    functionBuilder.Arguments = functionArguments;
                    functionBuilder.BindingFlags = defaultBindingFlags;
                    functionBuilder.Remainder = remainder.ToString();
                }
                else
                {
                    // We ended up with something other than a function expression
                    ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty);
                }
            }
 
            /// <summary>
            /// Coerce the arguments according to the parameter types
            /// Will only return null if the coercion didn't work due to an InvalidCastException.
            /// </summary>
            private static object[] CoerceArguments(object[] args, ParameterInfo[] parameters)
            {
                object[] coercedArguments = new object[args.Length];
 
                try
                {
                    // Do our best to coerce types into the arguments for the function
                    for (int n = 0; n < parameters.Length; n++)
                    {
                        if (args[n] == null)
                        {
                            // We can't coerce (object)null -- that's as general
                            // as it can get!
                            continue;
                        }
 
                        // Here we have special case conversions on a type basis
                        if (parameters[n].ParameterType == typeof(char[]))
                        {
                            coercedArguments[n] = args[n].ToString().ToCharArray();
                        }
                        else if (parameters[n].ParameterType.GetTypeInfo().IsEnum && args[n] is string && ((string)args[n]).Contains("."))
                        {
                            Type enumType = parameters[n].ParameterType;
                            string typeLeafName = enumType.Name + ".";
                            string typeFullName = enumType.FullName + ".";
 
                            // Enum.parse expects commas between enum components
                            // We'll support the C# type | syntax too
                            // We'll also allow the user to specify the leaf or full type name on the enum
                            string argument = args[n].ToString().Replace('|', ',').Replace(typeFullName, "").Replace(typeLeafName, "");
 
                            // Parse the string representation of the argument into the destination enum
                            coercedArguments[n] = Enum.Parse(enumType, argument);
                        }
                        else
                        {
                            // change the type of the final unescaped string into the destination
                            coercedArguments[n] = Convert.ChangeType(args[n], parameters[n].ParameterType, CultureInfo.InvariantCulture);
                        }
                    }
                }
                // The coercion failed therefore we return null
                catch (InvalidCastException)
                {
                    return null;
                }
                catch (FormatException)
                {
                    return null;
                }
                catch (OverflowException)
                {
                    // https://github.com/dotnet/msbuild/issues/2882
                    // test: PropertyFunctionMathMaxOverflow
                    return null;
                }
 
                return coercedArguments;
            }
 
            /// <summary>
            /// Make an attempt to create a string showing what we were trying to execute when we failed.
            /// This will show any intermediate evaluation which may help the user figure out what happened.
            /// </summary>
            private string GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args)
            {
                string parameters = String.Empty;
                if (args != null)
                {
                    foreach (object arg in args)
                    {
                        if (arg == null)
                        {
                            parameters += "null";
                        }
                        else
                        {
                            string argString = arg.ToString();
                            if (arg is string && argString.Length == 0)
                            {
                                parameters += "''";
                            }
                            else
                            {
                                parameters += arg.ToString();
                            }
                        }
 
                        parameters += ", ";
                    }
 
                    if (parameters.Length > 2)
                    {
                        parameters = parameters.Substring(0, parameters.Length - 2);
                    }
                }
 
                if (objectInstance == null)
                {
                    string typeName = _receiverType.FullName;
 
                    // We don't want to expose the real type name of our intrinsics
                    // so we'll replace it with "MSBuild"
                    if (_receiverType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions))
                    {
                        typeName = "MSBuild";
                    }
                    if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
                    {
                        return "[" + typeName + "]::" + name + "(" + parameters + ")";
                    }
                    else
                    {
                        return "[" + typeName + "]::" + name;
                    }
                }
                else
                {
                    string propertyValue = $"\"{objectInstance as string}\"";
 
                    if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod)
                    {
                        return propertyValue + "." + name + "(" + parameters + ")";
                    }
                    else
                    {
                        return propertyValue + "." + name;
                    }
                }
            }
 
            /// <summary>
            /// Check the property function allowlist whether this method is available.
            /// </summary>
            private static bool IsStaticMethodAvailable(Type receiverType, string methodName)
            {
                if (receiverType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions))
                {
                    // These are our intrinsic functions, so we're OK with those
                    return true;
                }
 
                if (Traits.Instance.EnableAllPropertyFunctions)
                {
                    // anything goes
                    return true;
                }
 
                return AvailableStaticMethods.GetTypeInformationFromTypeCache(receiverType.FullName, methodName) != null;
            }
 
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            private static bool IsInstanceMethodAvailable(string methodName)
            {
                if (Traits.Instance.EnableAllPropertyFunctions)
                {
                    // anything goes
                    return true;
                }
 
                // This could be expanded to an allow / deny list.
                return !string.Equals("GetType", methodName, StringComparison.OrdinalIgnoreCase);
            }
 
            /// <summary>
            /// Construct and instance of objectType based on the constructor or method arguments provided.
            /// Arguments must never be null.
            /// </summary>
            private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor)
            {
                // First let's try for a method where all arguments are strings..
                Type[] types = new Type[_arguments.Length];
                for (int n = 0; n < _arguments.Length; n++)
                {
                    types[n] = typeof(string);
                }
 
                MethodBase memberInfo;
                if (isConstructor)
                {
                    memberInfo = _receiverType.GetConstructor(bindingFlags, null, types, null);
                }
                else
                {
                    memberInfo = _receiverType.GetMethod(_methodMethodName, bindingFlags, null, types, null);
                }
 
                // If we didn't get a match on all string arguments,
                // search for a method with the right number of arguments
                if (memberInfo == null)
                {
                    // Gather all methods that may match
                    IEnumerable<MethodBase> members;
                    if (isConstructor)
                    {
                        members = _receiverType.GetConstructors(bindingFlags);
                    }
                    else if (_receiverType == typeof(IntrinsicFunctions) && IntrinsicFunctionOverload.IsKnownOverloadMethodName(_methodMethodName))
                    {
                        MemberInfo[] foundMembers = _receiverType.FindMembers(
                            MemberTypes.Method,
                            bindingFlags,
                            (info, criteria) => string.Equals(info.Name, (string)criteria, StringComparison.OrdinalIgnoreCase),
                            _methodMethodName);
                        Array.Sort(foundMembers, IntrinsicFunctionOverload.IntrinsicFunctionOverloadMethodComparer);
                        members = foundMembers.Cast<MethodBase>();
                    }
                    else
                    {
                        members = _receiverType.GetMethods(bindingFlags).Where(m => string.Equals(m.Name, _methodMethodName, StringComparison.OrdinalIgnoreCase));
                    }
 
                    foreach (MethodBase member in members)
                    {
                        ParameterInfo[] parameters = member.GetParameters();
 
                        // Simple match on name and number of params, we will be case insensitive
                        if (parameters.Length == _arguments.Length)
                        {
                            // Try to find a method with the right name, number of arguments and
                            // compatible argument types
                            // we have a match on the name and argument number
                            // now let's try to coerce the arguments we have
                            // into the arguments on the matching method
                            object[] coercedArguments = CoerceArguments(args, parameters);
 
                            if (coercedArguments != null)
                            {
                                // We have a complete match
                                memberInfo = member;
                                args = coercedArguments;
                                break;
                            }
                        }
                    }
                }
 
                object functionResult = null;
 
                // We have a match and coerced arguments, let's construct..
                if (memberInfo != null && args != null)
                {
                    if (isConstructor)
                    {
                        functionResult = ((ConstructorInfo)memberInfo).Invoke(args);
                    }
                    else
                    {
                        functionResult = ((MethodInfo)memberInfo).Invoke(objectInstance /* null if static method */, args);
                    }
                }
                else if (!isConstructor)
                {
                    throw ex;
                }
 
                if (functionResult == null && isConstructor)
                {
                    throw new TargetInvocationException(new MissingMethodException());
                }
 
                return functionResult;
            }
        }
    }
 
#nullable enable
 
    internal static class IntrinsicFunctionOverload
    {
        private static readonly string[] s_knownOverloadName = { "Add", "Subtract", "Multiply", "Divide", "Modulo", };
 
        // Order by the TypeCode of the first parameter.
        // When change wave is enabled, order long before double.
        // Otherwise preserve prior behavior of double before long.
        // For reuse, the comparer is cached in a non-generic type.
        // Both comparer instances can be cached to support change wave testing.
        private static IComparer<MemberInfo>? s_comparerLongBeforeDouble;
 
        internal static IComparer<MemberInfo> IntrinsicFunctionOverloadMethodComparer => LongBeforeDoubleComparer;
 
        private static IComparer<MemberInfo> LongBeforeDoubleComparer => s_comparerLongBeforeDouble ??= Comparer<MemberInfo>.Create((key0, key1) => SelectTypeOfFirstParameter(key0).CompareTo(SelectTypeOfFirstParameter(key1)));
 
        internal static bool IsKnownOverloadMethodName(string methodName) => s_knownOverloadName.Any(name => string.Equals(name, methodName, StringComparison.OrdinalIgnoreCase));
 
        private static TypeCode SelectTypeOfFirstParameter(MemberInfo member)
        {
            MethodBase? method = member as MethodBase;
            if (method == null)
            {
                return TypeCode.Empty;
            }
 
            ParameterInfo[] parameters = method.GetParameters();
            return parameters.Length > 0
                ? Type.GetTypeCode(parameters[0].ParameterType)
                : TypeCode.Empty;
        }
    }
}