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

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
#if !FEATURE_MSIOREDIST
using System.IO;
#endif
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Evaluation.Expander;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.NET.StringTools;
using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods;
using ParseArgs = Microsoft.Build.Evaluation.Expander.ArgumentParser;

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

#nullable disable

namespace Microsoft.Build.Evaluation;

internal partial class Expander<P, I>
    where P : class, IProperty
    where I : class, IItem
{
    /// <summary>
    /// This class represents the function as extracted from an expression
    /// It is also responsible for executing the function.
    /// </summary>
    internal class Function
    {
        /// <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 ExtractPropertyFunction(
            string expressionFunction,
            IElementLocation elementLocation,
            object propertyValue,
            PropertiesUseTracker propertiesUseTracker,
            IFileSystem fileSystem,
            LoggingContext loggingContext)
        {
            // Used to aggregate all the components needed for a Function
            FunctionBuilder functionBuilder = new FunctionBuilder { 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>
        /// Determines whether the argument at <paramref name="argIndex"/> for a System.IO.File
        /// or System.IO.Directory method is a file/directory path that should be resolved
        /// against the thread-local working directory.
        /// </summary>
        private static bool IsFileOrDirectoryPathArgument(string methodName, int argIndex)
        {
            // First argument is always a path for all File/Directory static methods.
            if (argIndex == 0)
            {
                return true;
            }

            // Second argument is a destination path for Copy, Move, Replace.
            // CreateSymbolicLink is intentionally excluded — its arg1 (pathToTarget) is the
            // symlink target and relative values are semantically meaningful (stored as-is).
            if (argIndex == 1)
            {
                return string.Equals(methodName, "Copy", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(methodName, "Move", StringComparison.OrdinalIgnoreCase)
                    || string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase);
            }

            // Third argument is the backup path for Replace.
            if (argIndex == 2)
            {
                return string.Equals(methodName, "Replace", StringComparison.OrdinalIgnoreCase);
            }

            return false;
        }

        /// <summary>
        /// Execute the function on the given instance.
        /// </summary>
        internal object Execute(object objectInstance, IPropertyProvider<P> 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(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.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
                        // Use fully qualified type names because FEATURE_MSIOREDIST aliases
                        // Directory and Path to Microsoft.IO.* in this file, but _receiverType
                        // from AvailableStaticMethods is always System.IO.*.
                        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);

                        // In -mt mode, resolve relative path arguments for File/Directory methods
                        // against the thread-local working directory instead of the process-global
                        // Environment.CurrentDirectory which may point to a different project's directory.
                        // In multiprocess mode, CurrentThreadWorkingDirectory is null and
                        // MakeFullPathFromThreadWorkingDirectory returns null — this is a no-op.
                        // This must happen AFTER UnescapeAll so that the working directory path
                        // (a real filesystem path) is not corrupted by MSBuild unescape processing.
                        if ((_receiverType == typeof(System.IO.File) || _receiverType == typeof(System.IO.Directory))
                            && IsFileOrDirectoryPathArgument(_methodMethodName, n))
                        {
                            AbsolutePath? resolved = FileUtilities.MakeFullPathFromThreadWorkingDirectory((string)args[n]);
                            if (resolved.HasValue)
                            {
                                args[n] = (string)resolved.GetValueOrDefault();
                            }
                        }
                    }
                    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 (ParseArgs.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 (!WellKnownFunctions.TryExecuteWellKnownConstructorNoThrow(_receiverType, 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 = WellKnownFunctions.TryExecuteWellKnownFunction(_methodMethodName, _receiverType, _fileSystem, out functionResult, objectInstance, args);

                        if (!wellKnownFunctionSuccess)
                        {
                            // Some well-known functions need evaluated value from properties.
                            wellKnownFunctionSuccess = WellKnownFunctions.TryExecuteWellKnownFunctionWithPropertiesParam(_methodMethodName, _receiverType, _loggingContext, 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.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 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>
        /// 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
                Assumed.True(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
                    Assumed.NotNull(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 functionBuilder)
        {
            ReadOnlyMemory<char> argumentsContent = expressionFunction.AsMemory().Slice(1, indexerEndIndex - 1);
            string[] functionArguments;

            // If there are no arguments, then just create an empty array
            if (argumentsContent.IsEmpty)
            {
                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.
            string functionName;
            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 = expressionFunction.Substring(methodStartIndex);
        }

        /// <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 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(expressionFunctionAsSpan, 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
                    ReadOnlyMemory<char> argumentsContent = expressionFunction.AsMemory().Slice(argumentStartIndex, argumentsEndIndex - argumentStartIndex);

                    // If there are no arguments, then just create an empty array
                    if (argumentsContent.IsEmpty)
                    {
                        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 v && v.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(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(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;
        }
    }
}