// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

#if HAS_IOPERATION

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace Analyzer.Utilities.Extensions
{
    internal static partial class IOperationExtensions
    {
        /// <summary>
        /// Gets the receiver type for an invocation expression (i.e. type of 'A' in invocation 'A.B()')
        /// If the invocation actually involves a conversion from A to some other type, say 'C', on which B is invoked,
        /// then this method returns type A if <paramref name="beforeConversion"/> is true, and C if false.
        /// </summary>
        public static INamedTypeSymbol? GetReceiverType(this IInvocationOperation invocation, Compilation compilation, bool beforeConversion, CancellationToken cancellationToken)
        {
            if (invocation.Instance != null)
            {
                return beforeConversion ?
                    GetReceiverType(invocation.Instance.Syntax, compilation, cancellationToken) :
                    invocation.Instance.Type as INamedTypeSymbol;
            }
            else if (invocation.TargetMethod.IsExtensionMethod && !invocation.TargetMethod.Parameters.IsEmpty)
            {
                var firstArg = invocation.Arguments.FirstOrDefault();
                if (firstArg != null)
                {
                    return beforeConversion ?
                        GetReceiverType(firstArg.Value.Syntax, compilation, cancellationToken) :
                        firstArg.Type as INamedTypeSymbol;
                }
                else if (invocation.TargetMethod.Parameters[0].IsParams)
                {
                    return invocation.TargetMethod.Parameters[0].Type as INamedTypeSymbol;
                }
            }

            return null;
        }

        private static INamedTypeSymbol? GetReceiverType(SyntaxNode receiverSyntax, Compilation compilation, CancellationToken cancellationToken)
        {
            var model = compilation.GetSemanticModel(receiverSyntax.SyntaxTree);
            var typeInfo = model.GetTypeInfo(receiverSyntax, cancellationToken);
            return typeInfo.Type as INamedTypeSymbol;
        }

        public static bool HasNullConstantValue(this IOperation operation)
        {
            return operation.ConstantValue.HasValue && operation.ConstantValue.Value == null;
        }

        public static bool TryGetBoolConstantValue(this IOperation operation, out bool constantValue)
        {
            if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is bool value)
            {
                constantValue = value;
                return true;
            }

            constantValue = false;
            return false;
        }

        public static bool HasConstantValue(this IOperation operation, long comparand)
        {
            return operation.HasConstantValue(unchecked((ulong)comparand));
        }

        public static bool HasConstantValue(this IOperation operation, ulong comparand)
        {
            var constantValue = operation.ConstantValue;
            if (!constantValue.HasValue)
            {
                return false;
            }

            if (operation.Type == null || operation.Type.IsErrorType())
            {
                return false;
            }

            if (operation.Type.IsPrimitiveType())
            {
                return HasConstantValue(constantValue, operation.Type, comparand);
            }

            if (operation.Type.TypeKind == TypeKind.Enum)
            {
                var enumUnderlyingType = ((INamedTypeSymbol)operation.Type).EnumUnderlyingType;
                return enumUnderlyingType != null &&
                    enumUnderlyingType.IsPrimitiveType() &&
                    HasConstantValue(constantValue, enumUnderlyingType, comparand);
            }

            return false;
        }

        private static bool HasConstantValue(Optional<object> constantValue, ITypeSymbol constantValueType, ulong comparand)
        {
            if (constantValueType.SpecialType is SpecialType.System_Double or SpecialType.System_Single)
            {
                return (double)constantValue.Value == comparand;
            }

            return DiagnosticHelpers.TryConvertToUInt64(constantValue.Value, constantValueType.SpecialType, out ulong convertedValue) && convertedValue == comparand;
        }

        public static ITypeSymbol? GetElementType(this IArrayCreationOperation? arrayCreation)
        {
            return (arrayCreation?.Type as IArrayTypeSymbol)?.ElementType;
        }

        /// <summary>
        /// Filters out operations that are implicit and have no explicit descendant with a constant value or a non-null type.
        /// </summary>
        public static ImmutableArray<IOperation> WithoutFullyImplicitOperations(this ImmutableArray<IOperation> operations)
        {
            ImmutableArray<IOperation>.Builder? builder = null;
            for (int i = 0; i < operations.Length; i++)
            {
                var operation = operations[i];

                // Check if all descendants are either implicit or are explicit with no constant value or type, indicating it is not user written code.
                if (operation.DescendantsAndSelf().All(o => o.IsImplicit || (!o.ConstantValue.HasValue && o.Type == null)))
                {
                    if (builder == null)
                    {
                        builder = ImmutableArray.CreateBuilder<IOperation>();
                        builder.AddRange(operations, i);
                    }
                }
                else if (builder != null)
                {
                    builder.Add(operation);
                }
            }

            return builder != null ? builder.ToImmutable() : operations;
        }

        /// <summary>
        /// Gets explicit descendants or self of the given <paramref name="operation"/> that have no explicit ancestor in
        /// the operation tree rooted at <paramref name="operation"/>.
        /// </summary>
        /// <param name="operation">Operation</param>
        public static ImmutableArray<IOperation> GetTopmostExplicitDescendants(this IOperation operation)
        {
            if (!operation.IsImplicit)
            {
                return ImmutableArray.Create(operation);
            }

            var builder = ImmutableArray.CreateBuilder<IOperation>();
            var operationsToProcess = new Queue<IOperation>();
            operationsToProcess.Enqueue(operation);

            while (operationsToProcess.Count > 0)
            {
                operation = operationsToProcess.Dequeue();
                if (!operation.IsImplicit)
                {
                    builder.Add(operation);
                }
                else
                {
                    foreach (var child in operation.Children)
                    {
                        operationsToProcess.Enqueue(child);
                    }
                }
            }

            return builder.ToImmutable();
        }

        /// <summary>
        /// True if this operation has no IOperation API support, i.e. <see cref="OperationKind.None"/> and
        /// is the root operation, i.e. <see cref="Operation.Parent"/> is null.
        /// For example, this returns true for attribute operations.
        /// </summary>
        public static bool IsOperationNoneRoot(this IOperation operation)
        {
            return operation.Kind == OperationKind.None && operation.Parent == null;
        }

        /// <summary>
        /// Returns the topmost <see cref="IBlockOperation"/> containing the given <paramref name="operation"/>.
        /// </summary>
        public static IBlockOperation? GetTopmostParentBlock(this IOperation? operation)
        {
            IOperation? currentOperation = operation;
            IBlockOperation? topmostBlockOperation = null;
            while (currentOperation != null)
            {
                if (currentOperation is IBlockOperation blockOperation)
                {
                    topmostBlockOperation = blockOperation;
                }

                currentOperation = currentOperation.Parent;
            }

            return topmostBlockOperation;
        }

        /// <summary>
        /// Gets the first ancestor of this operation with:
        ///  1. Specified OperationKind
        ///  2. If <paramref name="predicate"/> is non-null, it succeeds for the ancestor.
        /// Returns null if there is no such ancestor.
        /// </summary>
        public static TOperation? GetAncestor<TOperation>(this IOperation root, OperationKind ancestorKind, Func<TOperation, bool>? predicate = null)
            where TOperation : class, IOperation
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

            var ancestor = root;
            do
            {
                ancestor = ancestor.Parent;
            } while (ancestor != null && ancestor.Kind != ancestorKind);

            if (ancestor != null)
            {
                if (predicate != null && !predicate((TOperation)ancestor))
                {
                    return GetAncestor(ancestor, ancestorKind, predicate);
                }
                return (TOperation)ancestor;
            }
            else
            {
                return default;
            }
        }

        /// <summary>
        /// Gets the first ancestor of this operation with:
        ///  1. Any OperationKind from the specified <paramref name="ancestorKinds"/>.
        ///  2. If <paramref name="predicate"/> is non-null, it succeeds for the ancestor.
        /// Returns null if there is no such ancestor.
        /// </summary>
        public static IOperation? GetAncestor(this IOperation root, ImmutableArray<OperationKind> ancestorKinds, Func<IOperation, bool>? predicate = null)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

            var ancestor = root;
            do
            {
                ancestor = ancestor.Parent;
            } while (ancestor != null && !ancestorKinds.Contains(ancestor.Kind));

            if (ancestor != null)
            {
                if (predicate != null && !predicate(ancestor))
                {
                    return GetAncestor(ancestor, ancestorKinds, predicate);
                }
                return ancestor;
            }
            else
            {
                return default;
            }
        }

        public static IConditionalAccessOperation? GetConditionalAccess(this IConditionalAccessInstanceOperation operation)
        {
            return operation.GetAncestor(OperationKind.ConditionalAccess, (IConditionalAccessOperation c) => c.Operation.Syntax == operation.Syntax);
        }

        /// <summary>
        /// Gets the operation for the object being created that is being referenced by <paramref name="operation"/>.
        /// If the operation is referencing an implicit or an explicit this/base/Me/MyBase/MyClass instance, then we return "null".
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="isInsideAnonymousObjectInitializer">Flag to indicate if the operation is a descendant of an <see cref="IAnonymousObjectCreationOperation"/>.</param>
        /// <remarks>
        /// PERF: Note that the parameter <paramref name="isInsideAnonymousObjectInitializer"/> is to improve performance by avoiding walking the entire IOperation parent for non-initializer cases.
        /// </remarks>
        public static IOperation? GetInstance(this IInstanceReferenceOperation operation, bool isInsideAnonymousObjectInitializer)
        {
            Debug.Assert(isInsideAnonymousObjectInitializer ==
                (operation.GetAncestor<IAnonymousObjectCreationOperation>(OperationKind.AnonymousObjectCreation) != null));

            if (isInsideAnonymousObjectInitializer)
            {
                for (IOperation current = operation; current != null && current.Kind != OperationKind.Block; current = current.Parent)
                {
                    switch (current.Kind)
                    {
                        // VB object initializer allows accessing the members of the object being created with "." operator.
                        // The syntax of such an IInstanceReferenceOperation points to the object being created.
                        // Check for such an IAnonymousObjectCreationOperation with matching syntax.
                        // For example, instance reference for members ".Field1" and ".Field2" in "New C() With { .Field1 = 0, .Field2 = .Field1 }".
                        case OperationKind.AnonymousObjectCreation:
                            if (current.Syntax == operation.Syntax)
                            {
                                return current;
                            }

                            break;
                    }
                }
            }

            // For all other cases, IInstanceReferenceOperation refers to the implicit or explicit this/base/Me/MyBase/MyClass reference.
            // We return null for such cases.
            return null;
        }

        /// <summary>
        /// Workaround for https://github.com/dotnet/roslyn/issues/22736 (IPropertyReferenceExpressions in IAnonymousObjectCreationExpression are missing a receiver).
        /// Gets the instance for the anonymous object being created that is being referenced by <paramref name="operation"/>.
        /// Otherwise, returns null
        /// </summary>
        public static IAnonymousObjectCreationOperation? GetAnonymousObjectCreation(this IPropertyReferenceOperation operation)
        {
            if (operation.Instance == null &&
                operation.Property.ContainingType.IsAnonymousType)
            {
                var declarationSyntax = operation.Property.ContainingType.DeclaringSyntaxReferences[0].GetSyntax();
                return operation.GetAncestor(OperationKind.AnonymousObjectCreation, (IAnonymousObjectCreationOperation a) => a.Syntax == declarationSyntax);
            }

            return null;
        }

        public static bool IsInsideAnonymousFunction(this IOperation operation)
            => operation.GetAncestor<IAnonymousFunctionOperation>(OperationKind.AnonymousFunction) != null;

        public static bool HasAnyOperationDescendant(this ImmutableArray<IOperation> operationBlocks, Func<IOperation, bool> predicate)
        {
            foreach (var operationBlock in operationBlocks)
            {
                if (operationBlock.HasAnyOperationDescendant(predicate))
                {
                    return true;
                }
            }

            return false;
        }

        public static bool HasAnyOperationDescendant(this IOperation operationBlock, Func<IOperation, bool> predicate)
        {
            return operationBlock.HasAnyOperationDescendant(predicate, out _);
        }

        public static bool HasAnyOperationDescendant(this IOperation operationBlock, Func<IOperation, bool> predicate, [NotNullWhen(returnValue: true)] out IOperation? foundOperation)
        {
            foreach (var descendant in operationBlock.DescendantsAndSelf())
            {
                if (predicate(descendant))
                {
                    foundOperation = descendant;
                    return true;
                }
            }

            foundOperation = null;
            return false;
        }

        public static bool HasAnyOperationDescendant(this ImmutableArray<IOperation> operationBlocks, OperationKind kind)
        {
            return operationBlocks.HasAnyOperationDescendant(predicate: operation => operation.Kind == kind);
        }

        /// <summary>
        /// Indicates if the given <paramref name="binaryOperation"/> is a predicate operation used in a condition.
        /// </summary>
        /// <param name="binaryOperation"></param>
        /// <returns></returns>
        public static bool IsComparisonOperator(this IBinaryOperation binaryOperation)
        {
            switch (binaryOperation.OperatorKind)
            {
                case BinaryOperatorKind.Equals:
                case BinaryOperatorKind.NotEquals:
                case BinaryOperatorKind.ObjectValueEquals:
                case BinaryOperatorKind.ObjectValueNotEquals:
                case BinaryOperatorKind.LessThan:
                case BinaryOperatorKind.LessThanOrEqual:
                case BinaryOperatorKind.GreaterThan:
                case BinaryOperatorKind.GreaterThanOrEqual:
                    return true;

                default:
                    return false;
            }
        }

        public static IOperation GetRoot(this IOperation operation)
        {
            while (operation.Parent != null)
            {
                operation = operation.Parent;
            }

            return operation;
        }

        /// <summary>
        /// PERF: Cache from operation roots to their corresponding <see cref="ControlFlowGraph"/> to enable interprocedural flow analysis
        /// across analyzers and analyzer callbacks to re-use the control flow graph.
        /// </summary>
        /// <remarks>Also see <see cref="IMethodSymbolExtensions.s_methodToTopmostOperationBlockCache"/></remarks>
        private static readonly BoundedCache<Compilation, ConcurrentDictionary<IOperation, ControlFlowGraph?>> s_operationToCfgCache
            = new BoundedCache<Compilation, ConcurrentDictionary<IOperation, ControlFlowGraph?>>();

        public static bool TryGetEnclosingControlFlowGraph(this IOperation operation, [NotNullWhen(returnValue: true)] out ControlFlowGraph? cfg)
        {
            operation = operation.GetRoot();
            var operationToCfgMap = s_operationToCfgCache.GetOrCreateValue(operation.SemanticModel.Compilation);
            cfg = operationToCfgMap.GetOrAdd(operation, CreateControlFlowGraph);
            return cfg != null;
        }

        public static ControlFlowGraph? GetEnclosingControlFlowGraph(this IBlockOperation blockOperation)
        {
            var success = blockOperation.TryGetEnclosingControlFlowGraph(out var cfg);
            Debug.Assert(success);
            Debug.Assert(cfg != null);
            return cfg;
        }

        private static ControlFlowGraph? CreateControlFlowGraph(IOperation operation)
        {
            switch (operation)
            {
                case IBlockOperation blockOperation:
                    return ControlFlowGraph.Create(blockOperation);

                case IMethodBodyOperation methodBodyOperation:
                    return ControlFlowGraph.Create(methodBodyOperation);

                case IConstructorBodyOperation constructorBodyOperation:
                    return ControlFlowGraph.Create(constructorBodyOperation);

                case IFieldInitializerOperation fieldInitializerOperation:
                    return ControlFlowGraph.Create(fieldInitializerOperation);

                case IPropertyInitializerOperation propertyInitializerOperation:
                    return ControlFlowGraph.Create(propertyInitializerOperation);

                case IParameterInitializerOperation:
                    // We do not support flow analysis for parameter initializers
                    return null;

                default:
                    // Attribute blocks have OperationKind.None, but ControlFlowGraph.Create does not
                    // have an overload for such operation roots.
                    // Gracefully return null for this case and fire an assert for any other OperationKind.
                    Debug.Assert(operation.Kind == OperationKind.None, $"Unexpected root operation kind: {operation.Kind}");
                    return null;
            }
        }

        /// <summary>
        /// Gets the symbols captured from the enclosing function(s) by the given lambda or local function.
        /// </summary>
        /// <param name="operation">Operation representing the lambda or local function.</param>
        /// <param name="lambdaOrLocalFunction">Method symbol for the lambda or local function.</param>
        public static PooledHashSet<ISymbol> GetCaptures(this IOperation operation, IMethodSymbol lambdaOrLocalFunction)
        {
            Debug.Assert(operation is IAnonymousFunctionOperation anonymousFunction && anonymousFunction.Symbol.OriginalDefinition.ReturnTypeAndParametersAreSame(lambdaOrLocalFunction.OriginalDefinition) ||
                         operation is ILocalFunctionOperation localFunction && localFunction.Symbol.OriginalDefinition.Equals(lambdaOrLocalFunction.OriginalDefinition));

            lambdaOrLocalFunction = lambdaOrLocalFunction.OriginalDefinition;

            var builder = PooledHashSet<ISymbol>.GetInstance();
            using var nestedLambdasAndLocalFunctions = PooledHashSet<IMethodSymbol>.GetInstance();
            nestedLambdasAndLocalFunctions.Add(lambdaOrLocalFunction);

            foreach (var child in operation.Descendants())
            {
                switch (child.Kind)
                {
                    case OperationKind.LocalReference:
                        ProcessLocalOrParameter(((ILocalReferenceOperation)child).Local);
                        break;

                    case OperationKind.ParameterReference:
                        ProcessLocalOrParameter(((IParameterReferenceOperation)child).Parameter);
                        break;

                    case OperationKind.InstanceReference:
                        builder.Add(lambdaOrLocalFunction.ContainingType);
                        break;

                    case OperationKind.AnonymousFunction:
                        nestedLambdasAndLocalFunctions.Add(((IAnonymousFunctionOperation)child).Symbol);
                        break;

                    case OperationKind.LocalFunction:
                        nestedLambdasAndLocalFunctions.Add(((ILocalFunctionOperation)child).Symbol);
                        break;
                }
            }

            return builder;

            // Local functions.
            void ProcessLocalOrParameter(ISymbol symbol)
            {
                if (symbol.ContainingSymbol?.Kind == SymbolKind.Method &&
                    !nestedLambdasAndLocalFunctions.Contains(symbol.ContainingSymbol.OriginalDefinition))
                {
                    builder.Add(symbol);
                }
            }
        }

        private static readonly ImmutableArray<OperationKind> s_LambdaAndLocalFunctionKinds =
            ImmutableArray.Create(OperationKind.AnonymousFunction, OperationKind.LocalFunction);
        public static bool IsWithinLambdaOrLocalFunction(this IOperation operation, [NotNullWhen(true)] out IOperation? containingLambdaOrLocalFunctionOperation)
        {
            containingLambdaOrLocalFunctionOperation = operation.GetAncestor(s_LambdaAndLocalFunctionKinds);
            return containingLambdaOrLocalFunctionOperation != null;
        }

        public static ITypeSymbol? GetPatternType(this IPatternOperation pattern)
        {
            return pattern switch
            {
#if CODEANALYSIS_V3_OR_BETTER
                IDeclarationPatternOperation declarationPattern => declarationPattern.MatchedType,
                IRecursivePatternOperation recursivePattern => recursivePattern.MatchedType,
                IDiscardPatternOperation discardPattern => discardPattern.InputType,
#else
                IDeclarationPatternOperation declarationPattern => declarationPattern.DeclaredSymbol switch
                {
                    ILocalSymbol local => local.Type,

                    IDiscardSymbol discard => discard.Type,

                    _ => null,
                },
#endif
                IConstantPatternOperation constantPattern => constantPattern.Value.Type,

                _ => null,
            };
        }

        /// <summary>
        /// If the given <paramref name="tupleOperation"/> is a nested tuple,
        /// gets the parenting tuple operation and the tuple element of that parenting tuple
        /// which contains the given tupleOperation as a descendant operation.
        /// </summary>
        public static bool TryGetParentTupleOperation(this ITupleOperation tupleOperation,
            [NotNullWhen(returnValue: true)] out ITupleOperation? parentTupleOperation,
            [NotNullWhen(returnValue: true)] out IOperation? elementOfParentTupleContainingTuple)
        {
            parentTupleOperation = null;
            elementOfParentTupleContainingTuple = null;

            IOperation previousOperation = tupleOperation;
            var currentOperation = tupleOperation.Parent;
            while (currentOperation != null)
            {
                switch (currentOperation.Kind)
                {
                    case OperationKind.Parenthesized:
                    case OperationKind.Conversion:
                    case OperationKind.DeclarationExpression:
                        previousOperation = currentOperation;
                        currentOperation = currentOperation.Parent;
                        continue;

                    case OperationKind.Tuple:
                        parentTupleOperation = (ITupleOperation)currentOperation;
                        elementOfParentTupleContainingTuple = previousOperation;
                        return true;

                    default:
                        return false;
                }
            }

            return false;
        }

        public static bool IsExtensionMethodAndHasNoInstance(this IInvocationOperation invocationOperation)
        {
            // This method exists to abstract away the language specific differences between IInvocationOperation implementations
            // See https://github.com/dotnet/roslyn/issues/23625 for more details
            return invocationOperation.TargetMethod.IsExtensionMethod && (invocationOperation.Language != LanguageNames.VisualBasic || invocationOperation.Instance == null);
        }

        public static IOperation? GetInstance(this IInvocationOperation invocationOperation)
            => invocationOperation.IsExtensionMethodAndHasNoInstance() ? invocationOperation.Arguments[0].Value : invocationOperation.Instance;

        public static SyntaxNode? GetInstanceSyntax(this IInvocationOperation invocationOperation)
            => invocationOperation.GetInstance()?.Syntax;

        public static ITypeSymbol? GetInstanceType(this IOperation operation)
        {
            IOperation? instance = operation switch
            {
                IInvocationOperation invocation => invocation.GetInstance(),

                IPropertyReferenceOperation propertyReference => propertyReference.Instance,

                _ => throw new NotImplementedException()
            };

            return instance?.WalkDownConversion().Type;
        }

        public static ISymbol? GetReferencedMemberOrLocalOrParameter(this IOperation operation)
        {
            return operation switch
            {
                IMemberReferenceOperation memberReference => memberReference.Member,

                IParameterReferenceOperation parameterReference => parameterReference.Parameter,

                ILocalReferenceOperation localReference => localReference.Local,

                IParenthesizedOperation parenthesized => parenthesized.Operand.GetReferencedMemberOrLocalOrParameter(),

                IConversionOperation conversion => conversion.Operand.GetReferencedMemberOrLocalOrParameter(),

                _ => null,
            };
        }

        /// <summary>
        /// Walks down consequtive parenthesized operations until an operand is reached that isn't a parenthesized operation.
        /// </summary>
        /// <param name="operation">The starting operation.</param>
        /// <returns>The inner non parenthesized operation or the starting operation if it wasn't a parenthesized operation.</returns>
        public static IOperation WalkDownParentheses(this IOperation operation)
        {
            while (operation is IParenthesizedOperation parenthesizedOperation)
            {
                operation = parenthesizedOperation.Operand;
            }

            return operation;
        }

        public static IOperation WalkUpParentheses(this IOperation operation)
        {
            while (operation is IParenthesizedOperation parenthesizedOperation)
            {
                operation = parenthesizedOperation.Parent;
            }

            return operation;
        }

        /// <summary>
        /// Walks down consequtive conversion operations until an operand is reached that isn't a conversion operation.
        /// </summary>
        /// <param name="operation">The starting operation.</param>
        /// <returns>The inner non conversion operation or the starting operation if it wasn't a conversion operation.</returns>
        public static IOperation WalkDownConversion(this IOperation operation)
        {
            while (operation is IConversionOperation conversionOperation)
            {
                operation = conversionOperation.Operand;
            }

            return operation;
        }

        public static IOperation WalkUpConversion(this IOperation operation)
        {
            while (operation is IConversionOperation conversionOperation)
            {
                operation = conversionOperation.Parent;
            }

            return operation;
        }

        public static ITypeSymbol? GetThrownExceptionType(this IThrowOperation operation)
        {
            var thrownObject = operation.Exception;

            // Starting C# 8.0, C# compiler wraps the thrown operation within an implicit conversion to System.Exception type.
            if (thrownObject is IConversionOperation conversion &&
                conversion.IsImplicit)
            {
                thrownObject = conversion.Operand;
            }

            return thrownObject?.Type;
        }

        /// <summary>
        /// Determines if the one of the invocation's arguments' values is an argument of the specified type, and if so, find
        /// the first one.
        /// </summary>
        /// <param name="invocationOperation">Invocation operation whose arguments to look through.</param>
        /// <param name="firstFoundArgument">First found IArgumentOperation.Value of the specified type, order by the method's
        /// signature's parameters (as opposed to how arguments are specified when invoked).</param>
        /// <returns>True if one is found, false otherwise.</returns>
        /// <remarks>
        /// IInvocationOperation.Arguments are ordered by how they are specified, which may differ from the order in the method
        /// signature if the caller specifies arguments by name. This will find the first typeof operation ordered by the
        /// method signature's parameters.
        /// </remarks>
        public static bool HasArgument<TOperation>(
            this IInvocationOperation invocationOperation,
            [NotNullWhen(returnValue: true)] out TOperation? firstFoundArgument)
            where TOperation : class, IOperation
        {
            firstFoundArgument = null;
            int minOrdinal = int.MaxValue;
            foreach (IArgumentOperation argumentOperation in invocationOperation.Arguments)
            {
                if (argumentOperation.Parameter.Ordinal < minOrdinal && argumentOperation.Value is TOperation to)
                {
                    minOrdinal = argumentOperation.Parameter.Ordinal;
                    firstFoundArgument = to;
                }
            }

            return firstFoundArgument != null;
        }

        public static bool HasAnyExplicitDescendant(this IOperation operation, Func<IOperation, bool>? descendIntoOperation = null)
        {
            using var stack = ArrayBuilder<IEnumerator<IOperation>>.GetInstance();
            stack.Add(operation.Children.GetEnumerator());

            while (stack.Any())
            {
                var enumerator = stack.Last();
                stack.RemoveLast();
                if (enumerator.MoveNext())
                {
                    var current = enumerator.Current;
                    stack.Add(enumerator);

                    if (current != null &&
                        (descendIntoOperation == null || descendIntoOperation(current)))
                    {
                        if (!current.IsImplicit &&
                            // This prevents non explicit operations like expression to be considered as ok
                            (current.ConstantValue.HasValue || current.Type != null))
                        {
                            return true;
                        }
                        stack.Add(current.Children.GetEnumerator());
                    }
                }
            }

            return false;
        }

        public static bool IsSetMethodInvocation(this IPropertyReferenceOperation operation)
        {
            if (operation.Property.SetMethod is null)
            {
                // This is either invalid code, or an assignment through a ref-returning getter
                return false;
            }

            IOperation potentialLeftSide = operation;
            while (potentialLeftSide.Parent is IParenthesizedOperation or ITupleOperation)
            {
                potentialLeftSide = potentialLeftSide.Parent;
            }

            return potentialLeftSide.Parent switch
            {
                IAssignmentOperation { Target: var target } when target == potentialLeftSide => true,
                _ => false,
            };
        }

        public static IArgumentOperation GetArgumentForParameterAtIndex(
            this ImmutableArray<IArgumentOperation> arguments,
            int parameterIndex)
        {
            Debug.Assert(parameterIndex >= 0);
            Debug.Assert(parameterIndex < arguments.Length);

            foreach (var argument in arguments)
            {
                if (argument.Parameter.Ordinal == parameterIndex)
                {
                    return argument;
                }
            }

            throw new InvalidOperationException();
        }

        /// <summary>
        /// Useful when named arguments used for a method call and you need them in the original parameter order.
        /// </summary>
        /// <param name="arguments">Arguments of the method</param>
        /// <returns>Returns the arguments in parameter order</returns>
        public static ImmutableArray<IArgumentOperation> GetArgumentsInParameterOrder(
            this ImmutableArray<IArgumentOperation> arguments)
        {
            using var parameterOrderedArguments = ArrayBuilder<IArgumentOperation>.GetInstance(arguments.Length, null!);

            foreach (var argument in arguments)
            {
                Debug.Assert(parameterOrderedArguments[argument.Parameter.Ordinal] == null);
                parameterOrderedArguments[argument.Parameter.Ordinal] = argument;
            }

            return parameterOrderedArguments.ToImmutableArray();
        }

        // Copied from roslyn https://github.com/dotnet/roslyn/blob/master/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/OperationExtensions.cs#L25

#if CODEANALYSIS_V3_OR_BETTER
        /// <summary>
        /// Returns the <see cref="ValueUsageInfo"/> for the given operation.
        /// This extension can be removed once https://github.com/dotnet/roslyn/issues/25057 is implemented.
        /// </summary>
        public static ValueUsageInfo GetValueUsageInfo(this IOperation operation, ISymbol containingSymbol)
        {
            /*
            |    code                  | Read | Write | ReadableRef | WritableRef | NonReadWriteRef |
            | x.Prop = 1               |      |  ✔️   |             |             |                 |
            | x.Prop += 1              |  ✔️  |  ✔️   |             |             |                 |
            | x.Prop++                 |  ✔️  |  ✔️   |             |             |                 |
            | Foo(x.Prop)              |  ✔️  |       |             |             |                 |
            | Foo(x.Prop),             |      |       |     ✔️      |             |                 |
               where void Foo(in T v)
            | Foo(out x.Prop)          |      |       |             |     ✔️      |                 |
            | Foo(ref x.Prop)          |      |       |     ✔️      |     ✔️      |                 |
            | nameof(x)                |      |       |             |             |       ✔️        | ️
            | sizeof(x)                |      |       |             |             |       ✔️        | ️
            | typeof(x)                |      |       |             |             |       ✔️        | ️
            | out var x                |      |  ✔️   |             |             |                 | ️
            | case X x:                |      |  ✔️   |             |             |                 | ️
            | obj is X x               |      |  ✔️   |             |             |                 |
            | ref var x =              |      |       |     ✔️      |     ✔️      |                 |
            | ref readonly var x =     |      |       |     ✔️      |             |                 |
 
            */
            if (operation is ILocalReferenceOperation localReference &&
                localReference.IsDeclaration &&
                !localReference.IsImplicit) // Workaround for https://github.com/dotnet/roslyn/issues/30753
            {
                // Declaration expression is a definition (write) for the declared local.
                return ValueUsageInfo.Write;
            }
            else if (operation is IDeclarationPatternOperation)
            {
                switch (operation.Parent)
                {
                    case IPatternCaseClauseOperation _:
                        // A declaration pattern within a pattern case clause is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      switch (obj)
                        //      {
                        //          case X x:
                        //
                        return ValueUsageInfo.Write;

                    case IRecursivePatternOperation _:
                        // A declaration pattern within a recursive pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      (obj) switch
                        //      {
                        //          (X x) => ...
                        //      };
                        //
                        return ValueUsageInfo.Write;

                    case ISwitchExpressionArmOperation _:
                        // A declaration pattern within a switch expression arm is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      obj switch
                        //      {
                        //          X x => ...
                        //
                        return ValueUsageInfo.Write;

                    case IIsPatternOperation _:
                        // A declaration pattern within an is pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      if (obj is X x)
                        //
                        return ValueUsageInfo.Write;

                    case IPropertySubpatternOperation _:
                        // A declaration pattern within a property sub-pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj.Property' below:
                        //      if (obj is { Property : int x })
                        //
                        return ValueUsageInfo.Write;

                    default:
                        Debug.Fail("Unhandled declaration pattern context");

                        // Conservatively assume read/write.
                        return ValueUsageInfo.ReadWrite;
                }
            }

            if (operation.Parent is IAssignmentOperation assignmentOperation &&
                assignmentOperation.Target == operation)
            {
                return operation.Parent.IsAnyCompoundAssignment()
                    ? ValueUsageInfo.ReadWrite
                    : ValueUsageInfo.Write;
            }
            else if (operation.Parent is IIncrementOrDecrementOperation)
            {
                return ValueUsageInfo.ReadWrite;
            }
            else if (operation.Parent is IParenthesizedOperation parenthesizedOperation)
            {
                // Note: IParenthesizedOperation is specific to VB, where the parens cause a copy, so this cannot be classified as a write.
                Debug.Assert(parenthesizedOperation.Language == LanguageNames.VisualBasic);

                return parenthesizedOperation.GetValueUsageInfo(containingSymbol) &
                    ~(ValueUsageInfo.Write | ValueUsageInfo.Reference);
            }
            else if (operation.Parent is INameOfOperation or
                     ITypeOfOperation or
                     ISizeOfOperation)
            {
                return ValueUsageInfo.Name;
            }
            else if (operation.Parent is IArgumentOperation argumentOperation)
            {
                switch (argumentOperation.Parameter.RefKind)
                {
                    case RefKind.RefReadOnly:
                        return ValueUsageInfo.ReadableReference;

                    case RefKind.Out:
                        return ValueUsageInfo.WritableReference;

                    case RefKind.Ref:
                        return ValueUsageInfo.ReadableWritableReference;

                    default:
                        return ValueUsageInfo.Read;
                }
            }
            else if (operation.Parent is IReturnOperation returnOperation)
            {
                return returnOperation.GetRefKind(containingSymbol) switch
                {
                    RefKind.RefReadOnly => ValueUsageInfo.ReadableReference,
                    RefKind.Ref => ValueUsageInfo.ReadableWritableReference,
                    _ => ValueUsageInfo.Read,
                };
            }
            else if (operation.Parent is IConditionalOperation conditionalOperation)
            {
                if (operation == conditionalOperation.WhenTrue
                    || operation == conditionalOperation.WhenFalse)
                {
                    return GetValueUsageInfo(conditionalOperation, containingSymbol);
                }
                else
                {
                    return ValueUsageInfo.Read;
                }
            }
            else if (operation.Parent is IReDimClauseOperation reDimClauseOperation &&
                reDimClauseOperation.Operand == operation)
            {
                return (reDimClauseOperation.Parent as IReDimOperation)?.Preserve == true
                    ? ValueUsageInfo.ReadWrite
                    : ValueUsageInfo.Write;
            }
            else if (operation.Parent is IDeclarationExpressionOperation declarationExpression)
            {
                return declarationExpression.GetValueUsageInfo(containingSymbol);
            }
            else if (operation.IsInLeftOfDeconstructionAssignment(out _))
            {
                return ValueUsageInfo.Write;
            }
            else if (operation.Parent is IVariableInitializerOperation variableInitializerOperation)
            {
                if (variableInitializerOperation.Parent is IVariableDeclaratorOperation variableDeclaratorOperation)
                {
                    switch (variableDeclaratorOperation.Symbol.RefKind)
                    {
                        case RefKind.Ref:
                            return ValueUsageInfo.ReadableWritableReference;

                        case RefKind.RefReadOnly:
                            return ValueUsageInfo.ReadableReference;
                    }
                }
            }

            return ValueUsageInfo.Read;
        }

        public static bool IsInLeftOfDeconstructionAssignment(this IOperation operation, out IDeconstructionAssignmentOperation? deconstructionAssignment)
        {
            deconstructionAssignment = null;

            var previousOperation = operation;
            operation = operation.Parent;

            while (operation != null)
            {
                switch (operation.Kind)
                {
                    case OperationKind.DeconstructionAssignment:
                        deconstructionAssignment = (IDeconstructionAssignmentOperation)operation;
                        return deconstructionAssignment.Target == previousOperation;

                    case OperationKind.Tuple:
                    case OperationKind.Conversion:
                    case OperationKind.Parenthesized:
                        previousOperation = operation;
                        operation = operation.Parent;
                        continue;

                    default:
                        return false;
                }
            }

            return false;
        }

        public static RefKind GetRefKind(this IReturnOperation operation, ISymbol containingSymbol)
        {
            var containingMethod = TryGetContainingAnonymousFunctionOrLocalFunction(operation) ?? (containingSymbol as IMethodSymbol);
            return containingMethod?.RefKind ?? RefKind.None;
        }

        public static IMethodSymbol? TryGetContainingAnonymousFunctionOrLocalFunction(this IOperation? operation)
        {
            operation = operation?.Parent;
            while (operation != null)
            {
                switch (operation.Kind)
                {
                    case OperationKind.AnonymousFunction:
                        return ((IAnonymousFunctionOperation)operation).Symbol;

                    case OperationKind.LocalFunction:
                        return ((ILocalFunctionOperation)operation).Symbol;
                }

                operation = operation.Parent;
            }

            return null;
        }

        /// <summary>
        /// Retursn true if the given operation is a regular compound assignment,
        /// i.e. <see cref="ICompoundAssignmentOperation"/> such as <code>a += b</code>,
        /// or a special null coalescing compoud assignment, i.e. <see cref="ICoalesceAssignmentOperation"/>
        /// such as <code>a ??= b</code>.
        /// </summary>
        public static bool IsAnyCompoundAssignment(this IOperation operation)
        {
            switch (operation)
            {
                case ICompoundAssignmentOperation _:
                case ICoalesceAssignmentOperation _:
                    return true;

                default:
                    return false;
            }
        }
#endif
    }
}

#endif
