File: System\Dynamic\Utils\ExpressionUtils.cs
Web Access
Project: src\src\libraries\System.Linq.Expressions\src\System.Linq.Expressions.csproj (System.Linq.Expressions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
 
namespace System.Dynamic.Utils
{
    internal static class ExpressionUtils
    {
        /// <summary>
        /// See overload with <see cref="IArgumentProvider"/> for more information.
        /// </summary>
        public static ReadOnlyCollection<ParameterExpression> ReturnReadOnly(IParameterProvider provider, ref object collection)
        {
            if (collection is ParameterExpression tObj)
            {
                // otherwise make sure only one read-only collection ever gets exposed
                Interlocked.CompareExchange(
                    ref collection!,
                    new ReadOnlyCollection<ParameterExpression>(new ListParameterProvider(provider, tObj)),
                    tObj
                );
            }
 
            // and return what is not guaranteed to be a read-only collection
            return (ReadOnlyCollection<ParameterExpression>)collection;
        }
 
        public static ReadOnlyCollection<T> ReturnReadOnly<T>(ref IReadOnlyList<T> collection)
        {
            IReadOnlyList<T> value = collection;
 
            // if it's already read-only just return it.
            if (value is ReadOnlyCollection<T> res)
            {
                return res;
            }
 
            // otherwise make sure only read-only collection every gets exposed
            Interlocked.CompareExchange(ref collection, value.ToReadOnly(), value);
 
            // and return it
            return (ReadOnlyCollection<T>)collection;
        }
 
        /// <summary>
        /// Helper used for ensuring we only return 1 instance of a ReadOnlyCollection of T.
        ///
        /// This is similar to the ReturnReadOnly of T. This version supports nodes which hold
        /// onto multiple Expressions where one is typed to object.  That object field holds either
        /// an expression or a ReadOnlyCollection of Expressions.  When it holds a ReadOnlyCollection
        /// the IList which backs it is a ListArgumentProvider which uses the Expression which
        /// implements IArgumentProvider to get 2nd and additional values.  The ListArgumentProvider
        /// continues to hold onto the 1st expression.
        ///
        /// This enables users to get the ReadOnlyCollection w/o it consuming more memory than if
        /// it was just an array.  Meanwhile The DLR internally avoids accessing  which would force
        /// the read-only collection to be created resulting in a typical memory savings.
        /// </summary>
        public static ReadOnlyCollection<Expression> ReturnReadOnly(IArgumentProvider provider, ref object collection)
        {
            if (collection is Expression tObj)
            {
                // otherwise make sure only one read-only collection ever gets exposed
                Interlocked.CompareExchange(
                    ref collection!,
                    new ReadOnlyCollection<Expression>(new ListArgumentProvider(provider, tObj)),
                    tObj
                );
            }
 
            // and return what is not guaranteed to be a read-only collection
            return (ReadOnlyCollection<Expression>)collection;
        }
 
        /// <summary>
        /// Helper which is used for specialized subtypes which use ReturnReadOnly(ref object, ...).
        /// This is the reverse version of ReturnReadOnly which takes an IArgumentProvider.
        ///
        /// This is used to return the 1st argument.  The 1st argument is typed as object and either
        /// contains a ReadOnlyCollection or the Expression.  We check for the Expression and if it's
        /// present we return that, otherwise we return the 1st element of the ReadOnlyCollection.
        /// </summary>
        public static T ReturnObject<T>(object collectionOrT) where T : class
        {
            if (collectionOrT is T t)
            {
                return t;
            }
 
            return ((ReadOnlyCollection<T>)collectionOrT)[0];
        }
 
        public static void ValidateArgumentTypes(MethodBase method, ExpressionType nodeKind, ref ReadOnlyCollection<Expression> arguments, string? methodParamName)
        {
            Debug.Assert(nodeKind == ExpressionType.Invoke || nodeKind == ExpressionType.Call || nodeKind == ExpressionType.Dynamic || nodeKind == ExpressionType.New);
 
            ParameterInfo[] pis = GetParametersForValidation(method, nodeKind);
 
            ValidateArgumentCount(method, nodeKind, arguments.Count, pis);
 
            Expression[]? newArgs = null;
            for (int i = 0, n = pis.Length; i < n; i++)
            {
                Expression arg = arguments[i];
                ParameterInfo pi = pis[i];
                arg = ValidateOneArgument(method, nodeKind, arg, pi, methodParamName, nameof(arguments), i);
 
                if (newArgs == null && arg != arguments[i])
                {
                    newArgs = new Expression[arguments.Count];
                    for (int j = 0; j < i; j++)
                    {
                        newArgs[j] = arguments[j];
                    }
                }
                if (newArgs != null)
                {
                    newArgs[i] = arg;
                }
            }
            if (newArgs != null)
            {
                arguments = new TrueReadOnlyCollection<Expression>(newArgs);
            }
        }
 
        public static void ValidateArgumentCount(MethodBase method, ExpressionType nodeKind, int count, ParameterInfo[] pis)
        {
            if (pis.Length != count)
            {
                // Throw the right error for the node we were given
                switch (nodeKind)
                {
                    case ExpressionType.New:
                        throw Error.IncorrectNumberOfConstructorArguments();
                    case ExpressionType.Invoke:
                        throw Error.IncorrectNumberOfLambdaArguments();
                    case ExpressionType.Dynamic:
                    case ExpressionType.Call:
                        throw Error.IncorrectNumberOfMethodCallArguments(method, nameof(method));
                    default:
                        throw ContractUtils.Unreachable;
                }
            }
        }
 
        public static Expression ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, string? methodParamName, string argumentParamName, int index = -1)
        {
            RequiresCanRead(arguments, argumentParamName, index);
            Type pType = pi.ParameterType;
            if (pType.IsByRef)
            {
                pType = pType.GetElementType()!;
            }
 
            TypeUtils.ValidateType(pType, methodParamName, allowByRef: true, allowPointer: true);
            if (!TypeUtils.AreReferenceAssignable(pType, arguments.Type))
            {
                if (!TryQuote(pType, ref arguments))
                {
                    // Throw the right error for the node we were given
                    switch (nodeKind)
                    {
                        case ExpressionType.New:
                            throw Error.ExpressionTypeDoesNotMatchConstructorParameter(arguments.Type, pType, argumentParamName, index);
                        case ExpressionType.Invoke:
                            throw Error.ExpressionTypeDoesNotMatchParameter(arguments.Type, pType, argumentParamName, index);
                        case ExpressionType.Dynamic:
                        case ExpressionType.Call:
                            throw Error.ExpressionTypeDoesNotMatchMethodParameter(arguments.Type, pType, method, argumentParamName, index);
                        default:
                            throw ContractUtils.Unreachable;
                    }
                }
            }
            return arguments;
        }
 
        public static void RequiresCanRead(Expression expression, string paramName)
        {
            RequiresCanRead(expression, paramName, -1);
        }
 
        public static void RequiresCanRead(Expression expression, string paramName, int idx)
        {
            ContractUtils.RequiresNotNull(expression, paramName, idx);
 
            // validate that we can read the node
            switch (expression.NodeType)
            {
                case ExpressionType.Index:
                    IndexExpression index = (IndexExpression)expression;
                    if (index.Indexer != null && !index.Indexer.CanRead)
                    {
                        throw Error.ExpressionMustBeReadable(paramName, idx);
                    }
                    break;
                case ExpressionType.MemberAccess:
                    MemberExpression member = (MemberExpression)expression;
                    if (member.Member is PropertyInfo prop)
                    {
                        if (!prop.CanRead)
                        {
                            throw Error.ExpressionMustBeReadable(paramName, idx);
                        }
                    }
                    break;
            }
        }
 
        // Attempts to auto-quote the expression tree. Returns true if it succeeded, false otherwise.
        public static bool TryQuote(Type parameterType, ref Expression argument)
        {
            // We used to allow quoting of any expression, but the behavior of
            // quote (produce a new tree closed over parameter values), only
            // works consistently for lambdas
            Type quoteable = typeof(LambdaExpression);
 
            if (TypeUtils.IsSameOrSubclass(quoteable, parameterType) && parameterType.IsInstanceOfType(argument))
            {
                argument = Expression.Quote(argument);
                return true;
            }
 
            return false;
        }
 
        internal static ParameterInfo[] GetParametersForValidation(MethodBase method, ExpressionType nodeKind)
        {
            ParameterInfo[] pis = method.GetParametersCached();
 
            if (nodeKind == ExpressionType.Dynamic)
            {
                pis = pis.RemoveFirst(); // ignore CallSite argument
            }
            return pis;
        }
 
        internal static bool SameElements<T>(ICollection<T>? replacement, IReadOnlyList<T> current) where T : class
        {
            Debug.Assert(current != null);
            if (replacement == current) // Relatively common case, so particularly useful to take the short-circuit.
            {
                return true;
            }
 
            if (replacement == null) // Treat null as empty.
            {
                return current.Count == 0;
            }
 
            return SameElementsInCollection(replacement, current);
        }
 
        internal static bool SameElements<T>(ref IEnumerable<T>? replacement, IReadOnlyList<T> current) where T : class
        {
            Debug.Assert(current != null);
            if (replacement == current) // Relatively common case, so particularly useful to take the short-circuit.
            {
                return true;
            }
 
            if (replacement == null) // Treat null as empty.
            {
                return current.Count == 0;
            }
 
            // Ensure arguments is safe to enumerate twice.
            // If we have to build a collection, build a TrueReadOnlyCollection<T>
            // so it won't be built a second time if used.
            ICollection<T>? replacementCol = replacement as ICollection<T>;
            if (replacementCol == null)
            {
                replacement = replacementCol = replacement.ToReadOnly();
            }
 
            return SameElementsInCollection(replacementCol, current);
        }
 
        private static bool SameElementsInCollection<T>(ICollection<T> replacement, IReadOnlyList<T> current) where T : class
        {
            int count = current.Count;
            if (replacement.Count != count)
            {
                return false;
            }
 
            if (count != 0)
            {
                int index = 0;
                foreach (T replacementObject in replacement)
                {
                    if (replacementObject != current[index])
                    {
                        return false;
                    }
 
                    index++;
                }
            }
 
            return true;
        }
 
        public static void ValidateArgumentCount(this LambdaExpression lambda)
        {
            if (((IParameterProvider)lambda).ParameterCount >= ushort.MaxValue)
            {
                throw Error.InvalidProgram();
            }
        }
    }
}