File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Simplification\Simplifiers\CastSimplifier.cs
Web Access
Project: src\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers;
 
/// <summary>
/// By default the cast simplifier operates under several main principles:
/// <list type="number">
/// <item>The final type that a cast-expression was converted to should be the same as the final
/// type that the underlying expression should convert to without the cast.  This tells us that 
/// the compiler thinks that value should convert to that type implicitly, not just explicitly.</item>
/// <item>Static semantics of the code should remain the same.  This means that things like overload
/// resolution of the invocations the casted expression is contained within should not change.</item>
/// <item>Runtime types and values should not observably change.  This means that if casting the 
/// value would cause a different type to be seen in a <see cref="System.Object.GetType()"/> call, 
/// or a different value could be observed at runtime, then it must remain.</item>
/// </list>
/// 
/// These rules serve as a good foundational intuition about when casts should be kept and when 
/// they should be removable.  However, they are not entirely complete.  There are cases when we
/// can weaken some of the above rules if it would not be observable at runtime.  For example,
/// if it can be proven that calling through an interface method would lead to the exact same
/// call at runtime to a specific implementation of that interface method, then it can be legal to
/// remove such a cast as at runtime this would not be observable.  This does in effect mean that
/// the emitted IL will be different, but this matches the user expectation that the *end* behavior
/// of their code remains the same.
/// </summary>
internal static class CastSimplifier
{
    public static bool IsUnnecessaryCast(ExpressionSyntax cast, SemanticModel semanticModel, CancellationToken cancellationToken)
        => cast switch
        {
            CastExpressionSyntax castExpression => IsUnnecessaryCast(castExpression, semanticModel, cancellationToken),
            BinaryExpressionSyntax binaryExpression => IsUnnecessaryAsCast(binaryExpression, semanticModel, cancellationToken),
            _ => false,
        };
 
    public static bool IsUnnecessaryAsCast(BinaryExpressionSyntax cast, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        return cast.Kind() == SyntaxKind.AsExpression &&
            !cast.WalkUpParentheses().ContainsDiagnostics &&
            IsCastSafeToRemove(cast, cast.Left, semanticModel, cancellationToken);
    }
 
    public static bool IsUnnecessaryCast(CastExpressionSyntax cast, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // Can't remove casts in code that has syntax errors.
        if (cast.WalkUpParentheses().ContainsDiagnostics)
            return false;
 
        // First handle very special cases where casts are safe to remove, but where we violate 
        // the general rules of CastSimplifier.  Specifically, look for cases where there are multiple
        // casts in an expression, which push values out of, but back into the same initial domain,
        // and which can be proven to generate the same resultant values with some of the casts
        // removed.
        //
        // This violates the rule that the same set of instructions would be emitted at runtime.
        // However, it follows the spirit of the rule that this is not observable, and so removing
        // the cast is beneficial to avoid unnecessary work at runtime.
 
        // Special case for: (int)E == 0 case.  Enums can always compare against the constant
        // 0 without needing a cast.
        if (IsEnumCastWithZeroCompare(cast, semanticModel, cancellationToken))
            return true;
 
        // Special case for: (E)~(int)e case.  Enums don't need to be converted to ints to get bitwise negated.
        if (IsRemovableBitwiseEnumNegation(cast, semanticModel, cancellationToken))
            return true;
 
        // Special case for converting a method group to object. The compiler issues a warning if the cast is removed:
        // warning CS8974: Converting method group 'ToString' to non-delegate type 'object'. Did you intend to invoke the method?
        var castExpressionOperation = semanticModel.GetOperation(cast.Expression, cancellationToken);
        if (castExpressionOperation is
            {
                Kind: OperationKind.MethodReference,
                Parent.Kind: OperationKind.DelegateCreation,
                IsImplicit: false,
            })
        {
            var current = castExpressionOperation.Parent.Parent;
            while (current is IConversionOperation { Type.SpecialType: SpecialType.System_Delegate or SpecialType.System_MulticastDelegate })
                current = current.Parent;
 
            if (current is IConversionOperation { Type.SpecialType: SpecialType.System_Object })
            {
                // If we have a double cast, report as unnecessary, e.g:
                // (object)(object)MethodGroup
                // (Delegate)(object)MethodGroup
                // If we have a single object cast, don't report as unnecessary e.g:
                // (object)MethodGroup
                if (current.Parent is IConversionOperation { Type: { } parentConversionType, IsImplicit: false } &&
                    semanticModel.ClassifyConversion(cast.Expression, parentConversionType).Exists)
                {
                    return true;
                }
 
                return false;
            }
        }
 
        return IsCastSafeToRemove(cast, cast.Expression, semanticModel, cancellationToken);
    }
 
    private static bool IsEnumCastWithZeroCompare(
        CastExpressionSyntax castExpression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        var leftOrRightChild = castExpression.WalkUpParentheses();
        if (leftOrRightChild.Parent is BinaryExpressionSyntax(SyntaxKind.EqualsExpression or SyntaxKind.NotEqualsExpression) binary)
        {
            var enumType = semanticModel.GetTypeInfo(castExpression.Expression, cancellationToken).Type as INamedTypeSymbol;
            var castedType = semanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type;
 
            if (Equals(enumType?.EnumUnderlyingType, castedType))
            {
                if (leftOrRightChild == binary.Left && IsConstantZero(binary.Right) ||
                    leftOrRightChild == binary.Right && IsConstantZero(binary.Left))
                {
                    return true;
                }
            }
        }
 
        return false;
 
        bool IsConstantZero(ExpressionSyntax child)
        {
            var constantValue = semanticModel.GetConstantValue(child, cancellationToken);
            return constantValue.HasValue &&
                   IntegerUtilities.IsIntegral(constantValue.Value) &&
                   IntegerUtilities.ToInt64(constantValue.Value) == 0;
        }
    }
 
    private static bool IsRemovableBitwiseEnumNegation(
        CastExpressionSyntax castExpression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        // Special case for: (E)~(int)e case or (E?)~(int)e case.  Enums don't need to be converted to ints to get
        // bitwise negated. The above is equivalent to `~e` as that keeps the same value and the same type. 
 
        if (castExpression.WalkUpParentheses().Parent is PrefixUnaryExpressionSyntax(SyntaxKind.BitwiseNotExpression) parent &&
            parent.WalkUpParentheses().Parent is CastExpressionSyntax parentCast)
        {
            var enumType = semanticModel.GetTypeInfo(castExpression.Expression, cancellationToken).Type as INamedTypeSymbol;
            var castedType = semanticModel.GetTypeInfo(castExpression.Type, cancellationToken).Type;
 
            if (Equals(enumType?.EnumUnderlyingType, castedType))
            {
                var parentCastType = semanticModel.GetTypeInfo(parentCast.Type, cancellationToken).Type;
                if (Equals(enumType, parentCastType.RemoveNullableIfPresent()))
                    return true;
            }
        }
 
        return false;
    }
 
    private static bool IsCastSafeToRemove(
        ExpressionSyntax castNode, ExpressionSyntax castedExpressionNode,
        SemanticModel originalSemanticModel, CancellationToken cancellationToken)
    {
        #region blocked cases that disqualify this cast from being removed.
 
        // callers should have checked this.
        Contract.ThrowIfTrue(castNode.WalkUpParentheses().ContainsDiagnostics);
 
        // Quick syntactic checks we can do before semantic work.
        var isDefaultLiteralCast = castedExpressionNode.WalkDownParentheses().IsKind(SyntaxKind.DefaultLiteralExpression);
 
        // Language does not allow `if (x is default)` ever.  So if we have `if (x is (Y)default)`
        // then we can't remove the cast.  This was special cased in the language due to user 
        // confusion, and so we have to preserve this despite the standard conversion rules
        // indicating this should be fine.
        if (isDefaultLiteralCast && castNode.WalkUpParentheses().Parent is PatternSyntax or CaseSwitchLabelSyntax)
            return false;
 
        #endregion blocked cases
 
        #region allowed cases
 
        // There are cases in the roslyn API where a direct cast does not result in a conversion operation
        // (for example, casting a anonymous-method to a delegate type).  We have to handle these cases
        // specially.
 
        var originalOperation = originalSemanticModel.GetOperation(castNode, cancellationToken);
        if (originalOperation is IConversionOperation originalConversionOperation)
        {
            return IsConversionCastSafeToRemove(
                castNode, castedExpressionNode, originalSemanticModel, originalConversionOperation, cancellationToken);
        }
 
        if (originalOperation is IDelegateCreationOperation originalDelegateCreationOperation)
        {
            return IsDelegateCreationCastSafeToRemove(
                castNode, castedExpressionNode, originalSemanticModel, originalDelegateCreationOperation, cancellationToken);
        }
 
        #endregion allowed cases
 
        return false;
    }
 
    private static bool CastRemovalCouldCauseSignExtensionWarning(ExpressionSyntax castSyntax, IConversionOperation conversionOperation)
    {
        // if we have  `... | (T)x` then disallow this cast if we have a widening numeric cast and both T and x are
        // signed integers.  This can often lead to confusing situations due to sign extension bits getting padded
        // to the front of the value.  The compiler even warns here in many cases.  We don't want to reimplement the
        // entire complex compiler algorithm.  So just look for the general case and disallow entirely.
        //
        // Note: it is intentional that this only triggers when both types are integral, and the value that is 
        // being cast is a signed integer.  In other words, the compiler warns both for (long)int, as well as (ulong)int.
        if (castSyntax.WalkUpParentheses().GetRequiredParent().Kind() is SyntaxKind.BitwiseOrExpression or SyntaxKind.OrAssignmentExpression)
        {
            var conversion = conversionOperation.GetConversion();
            if (conversion.IsImplicit &&
                (conversion.IsNumeric || conversion.IsNullable) &&
                conversionOperation.Type.RemoveNullableIfPresent() is var type1 &&
                conversionOperation.Operand.Type.RemoveNullableIfPresent() is var type2 &&
                type1.IsIntegralType() &&
                type2.IsSignedIntegralType())
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool IsDelegateCreationCastSafeToRemove(
        ExpressionSyntax castNode, ExpressionSyntax castedExpressionNode,
        SemanticModel originalSemanticModel, IDelegateCreationOperation originalDelegateCreationOperation,
        CancellationToken cancellationToken)
    {
        if (originalDelegateCreationOperation.Type?.TypeKind != TypeKind.Delegate)
            return false;
 
        // for a cast of an anonymous method to a delegate, we have to make sure that after cast-removal
        // that we still have the same.
        var (rewrittenSemanticModel, rewrittenExpression) = GetSemanticModelWithCastRemoved(
            castNode, castedExpressionNode, originalSemanticModel, cancellationToken);
        if (rewrittenSemanticModel is null || rewrittenExpression is null)
            return false;
 
        var rewrittenOperation = rewrittenSemanticModel.GetOperation(rewrittenExpression.WalkDownParentheses(), cancellationToken);
        if (rewrittenOperation is not (IAnonymousFunctionOperation or IMethodReferenceOperation))
            return false;
 
        if (rewrittenOperation.Parent is not IDelegateCreationOperation rewrittenDelegateCreationOperation)
            return false;
 
        if (rewrittenDelegateCreationOperation.Type?.TypeKind != TypeKind.Delegate)
            return false;
 
        // having to be converting to the same delegate type.
        if (!Equals(originalDelegateCreationOperation.Type, rewrittenDelegateCreationOperation.Type))
            return false;
 
        // If there are two conversions applied on the same node, ensure both are legal.
        if (originalDelegateCreationOperation.Parent is IConversionOperation conversionOperation &&
            conversionOperation.Syntax == castNode &&
            !IsConversionCastSafeToRemove(castNode, castedExpressionNode, originalSemanticModel, conversionOperation, cancellationToken))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool IsConversionCastSafeToRemove(
        ExpressionSyntax castNode, ExpressionSyntax castedExpressionNode,
        SemanticModel originalSemanticModel, IConversionOperation originalConversionOperation,
        CancellationToken cancellationToken)
    {
        #region blocked cases
 
        // If the conversion doesn't exist then we can't do anything with this as the code isn't
        // semantically valid.
        var originalConversion = originalConversionOperation.GetConversion();
        if (!originalConversion.Exists)
            return false;
 
        // A conversion must either not exist, or it must be explicit or implicit. At this point we
        // have conversions that will always succeed, but which could have impact on the code by 
        // changing the types of things (which can affect other things like overload resolution),
        // or the runtime values of code.  We only want to remove the cast if it will do none of those
        // things.
 
        // Explicit conversions are conversions that cannot be proven to always succeed, conversions
        // that are known to possibly lose information.  As such, we need to preserve this as it 
        // has necessary runtime behavior that must be kept.
        if (IsExplicitCastThatMustBePreserved(originalSemanticModel, castNode, originalConversion, cancellationToken))
            return false;
 
        // we are starting with code like `(X)expr` and converting to just `expr`. Post rewrite we need
        // to ensure that the final converted-type of `expr` matches the final converted type of `(X)expr`.
        var originalConvertedType = originalSemanticModel.GetTypeInfo(castNode.WalkUpParentheses(), cancellationToken).ConvertedType;
        if (originalConvertedType is null || originalConvertedType.TypeKind == TypeKind.Error)
            return false;
 
        // If removing the cast could cause the compiler to issue a new warning, then we have to preserve it.
        if (CastRemovalCouldCauseSignExtensionWarning(castNode, originalConversionOperation))
            return false;
 
        // if the expression being casted is the `null` literal, then we can't remove the cast if the final
        // converted type isn't known to be a reference type.  This can happen with code like: 
        //
        // void Goo<T, S>() where T : class, S
        // {
        //     S y = (T)null;
        // }
        //
        // Effectively, this constrains S to be a reference type (as T could not otherwise derive from it).
        // However, such a invariant isn't understood by the compiler.  So if the (T) cast is removed it will
        // fail as 'null' cannot be converted to an unconstrained generic type.
        var isNullLiteralCast = castedExpressionNode.WalkDownParentheses().IsKind(SyntaxKind.NullLiteralExpression);
        if (isNullLiteralCast && !originalConvertedType.IsReferenceType && !originalConvertedType.IsNullable())
            return false;
 
        // SomeType s = (Action)(() => {});  // Where there's a user defined conversion from Action->SomeType.
        //
        // This cast is necessary.  The language does not allow lambdas to be directly converted to the destination
        // type without explicitly stating the intermediary reified delegate type.
        var isAnonymousFunctionCast = castedExpressionNode.WalkDownParentheses() is AnonymousFunctionExpressionSyntax;
        if (isAnonymousFunctionCast && originalConversion.IsUserDefined)
            return false;
 
        // So far, this looks potentially possible to remove.  Now, actually do the removal and get the
        // semantic model for the rewritten code so we can check it to make sure semantics were preserved.
        var (rewrittenSemanticModel, rewrittenExpression) = GetSemanticModelWithCastRemoved(
            castNode, castedExpressionNode, originalSemanticModel, cancellationToken);
        if (rewrittenSemanticModel is null || rewrittenExpression is null)
            return false;
 
        var (rewrittenConvertedType, rewrittenConversion) = GetRewrittenInfo(
            castNode, rewrittenExpression,
            originalSemanticModel, rewrittenSemanticModel,
            originalConversion, originalConvertedType, cancellationToken);
        if (rewrittenConvertedType is null || rewrittenConvertedType.TypeKind == TypeKind.Error || !rewrittenConversion.Exists)
            return false;
 
        // If removing the conversion caused us to now become an explicit conversion (a conversion that can cause
        // lossyness), then we must block as that's disallowed by the language.
        //
        // Note: compiler API is slightly odd here as they return such an 'IsExplicit+Exists' conversion when casting
        // the expression inside a string interpolation.  So we ignore that case here
        if (rewrittenConversion.IsExplicit && castNode.WalkUpParentheses().Parent is not InterpolationSyntax)
            return false;
 
        if (CastRemovalWouldCauseUnintendedReferenceComparisonWarning(rewrittenExpression, rewrittenSemanticModel, cancellationToken))
            return false;
 
        // The final converted type may be the same even after removing the cast.  However, the cast may 
        // have been necessary to convert the type and/or value in a way that could be observable.  For example:
        //
        // object o1 = (long)expr;  // or (long)0
        // object o1 = (long?)expr; // or (long?)0
        //
        // We need to keep the cast so that the stored value stays the right type.
        if (originalConversion.IsConstantExpression ||
            originalConversion.IsNumeric ||
            originalConversion.IsEnumeration ||
            originalConversion.IsNullable)
        {
            if (rewrittenConversion.IsBoxing)
                return false;
        }
 
        // We have to specially handle formattable string conversions.  If we remove them, we may end up with
        // a string value instead.  For example:
        //
        // object o2 = (IFormattable)$"";
        if (originalConversion.IsInterpolatedString && !rewrittenConversion.IsInterpolatedString)
            return false;
 
        // If we have:
        //
        //      public static implicit operator A(string x)
        //      A x = (string)null;
        //
        // Then the original code has an implicit user defined conversion in it.  We can only remove this
        // if the new code would have the same conversion as well.
        //
        // One special case of this is Span<T> => ReadOnlySpan<T>.  This is technically a user-defined-conversion on
        // Span<T>, but it can be removed for a collection expression as the compiler knows directly how to make that 
        // into a ReadOnlySpan
        if (originalConversionOperation.Parent is IConversionOperation { Conversion.IsUserDefined: true } originalParentConversionOperation)
        {
            var originalParentConversion = originalParentConversionOperation.GetConversion();
            if (originalParentConversion.IsImplicit)
            {
                var isAcceptableSpanConversion = originalConversionOperation.Type.IsSpan() && originalParentConversionOperation.Type.IsReadOnlySpan();
 
                if (!isAcceptableSpanConversion)
                {
                    if (!rewrittenConversion.IsUserDefined)
                        return false;
 
                    if (!Equals(originalParentConversion.MethodSymbol, rewrittenConversion.MethodSymbol))
                        return false;
                }
            }
        }
 
        // Identity fp-casts can actually change the runtime value of the fp number.  This can happen because the
        // runtime is allowed to perform the operations with wider precision than the actual specified fp-precision.
        // i.e. 64-bit doubles can actually be 80 bits at runtime.  Even though the language considers this to be an
        // identity cast, we don't want to remove these because the user may be depending on that truncation.
        if (IsIdentityFloatingPointCastThatMustBePreserved(castNode, castedExpressionNode, originalSemanticModel, cancellationToken))
            return false;
 
        // Identity struct casts will make a copy.  This copy may need to be kept to preserve semantics that only
        // the copy is being manipulated and not the original struct.
        if (IsIdentityStructCastThatMustBePreserved(castNode, castedExpressionNode, originalSemanticModel, cancellationToken))
            return false;
 
        #endregion blocked cases
 
        #region allowed cases that allow this cast to be removed.
 
        // In code like `((X)y).Z()` the cast to (X) can be removed if the same 'Z' method would be called. The rules
        // here can be subtle.  For example, if Z is virtual, and (X) is a cast up the inheritance hierarchy then this
        // is *normally* ok.  However, the language resolve default parameter values from the overridden method.  So if
        // they differ, we can't actually remove the cast.
        //
        // Similarly, if (X) is a cast to an interface, and Z is an impl of that interface method, it might be possible
        // to remove, but only if y's type is sealed, as otherwise the interface method could be reimplemented in a
        // derived type.
        //
        // Note: this path is fundamentally different from the other forms of cast removal we perform.  The casts are
        // removed because statically they make no difference to the meaning of the code.  Here, the code statically
        // changes meaning.  However, we can use our knowledge of how the language/runtime works to know at *runtime*
        // that the user will get the exact same behavior.
        if (castNode.WalkUpParentheses().Parent is MemberAccessExpressionSyntax memberAccessExpression)
        {
            // Note: because this involves virtual calls, it is only safe if the original cast didn't change the runtime
            // representation of the value at all.  So we only allow this for representation preserving casts.  For example,
            // `string->object` preserves representation.  As does `int -> icomparable`
            var isRepresentationPreservingCast = originalConversion.IsIdentityOrImplicitReference() || originalConversion.IsBoxing;
            if (isRepresentationPreservingCast &&
                IsComplementaryMemberAccessAfterCastRemoval(
                    memberAccessExpression, rewrittenExpression, originalSemanticModel, rewrittenSemanticModel, cancellationToken))
            {
                return true;
            }
        }
 
        // In code like `((X)y)()` the cast to (X) can be removed if this was an implicit reference conversion
        // to a complementary delegate (because of delegate variance) *and* the return type of the delegate
        // invoke methods are the same.  For example:
        //
        //      Action<object> a = Console.WriteLine;
        //      ((Action<string>)a)("A");
        //
        // This is safe as delegate variance ensures that any parameter type has an implicit ref conversion to
        // the original delegate type.  However, the following would not be safe:
        //
        //      Func<object, string> a = ...;
        //      var v = ((Func<string, object>)a)("A");
        //
        // Here the type of 'v' would change to 'object' from 'string'.
        //
        // Note: this path is fundamentally different from the other forms of cast removal we perform.  The
        // casts are removed because statically they make no difference to the meaning of the code.  Here,
        // the code statically changes meaning.  However, we can use our knowledge of how the language/runtime
        // works to know at *runtime* that the user will get the exact same behavior.
        if (castNode.WalkUpParentheses().Parent is InvocationExpressionSyntax invocationExpression)
        {
            if (IsComplementaryInvocationAfterCastRemoval(
                    invocationExpression, rewrittenExpression, originalSemanticModel, rewrittenSemanticModel, cancellationToken))
            {
                return true;
            }
        }
 
        // If we have an implicit reference conversion in an 'is' expression then we remove the cast.  For example
        //
        //  if ((object)someRefType is string)
        //
        // However if we have:
        //
        //   List<int> list = null;
        //   if ((object)list is string)
        //
        // then we don't want to remove the cast as it can cause an error.
        if (castNode.WalkUpParentheses().Parent is BinaryExpressionSyntax(SyntaxKind.IsExpression) isExpression &&
            originalConversion.IsIdentityOrImplicitReference())
        {
            var castedExpressionType = originalSemanticModel.GetTypeInfo(castedExpressionNode, cancellationToken).Type;
            var isType = originalSemanticModel.GetTypeInfo(isExpression.Right, cancellationToken).Type;
 
            if (castedExpressionType != null && isType != null &&
                originalSemanticModel.Compilation.ClassifyConversion(castedExpressionType, isType).Exists)
            {
                return true;
            }
        }
 
        // Casts on collection can fundamentally change the runtime representation of the collection.  We do not
        // want to ever remove them in that case as that's precisely the reason a user may have provided them.
        if (originalConversion.IsCollectionExpression || rewrittenConversion.IsCollectionExpression)
        {
            // Both need to be collection expression conversions, both before and after the cast removal.  If not,
            // we have no idea what is going on and should not remove.
            if (!originalConversion.IsCollectionExpression || !rewrittenConversion.IsCollectionExpression)
                return false;
 
            if (IsCollectionExpressionCastThatMustBePreserved(castNode, originalSemanticModel, originalConvertedType, cancellationToken))
                return false;
        }
 
        if (originalConvertedType.Equals(rewrittenConvertedType))
        {
            // If the types of the expressions are exactly the same, then we can remove safely.
            if (originalConvertedType.Equals(rewrittenConvertedType, SymbolEqualityComparer.IncludeNullability))
                return true;
 
            // The types differ on nullability.  But we may still want to remove this.
            //
            // For example:
            //
            //      string Method() => (string?)notNullString;
            //
            // Here we have a non-null type converted to its nullable form, which is target typed back to the non-null
            // type.  Removing this nullable cast is safe and desirable.
            var targetType = castNode.GetTargetType(originalSemanticModel, cancellationToken);
            if (targetType is not null and not IErrorTypeSymbol &&
                rewrittenConvertedType.Equals(targetType, SymbolEqualityComparer.IncludeNullability))
            {
                return true;
            }
        }
 
        // We can safely remove convertion to object in interpolated strings regardless of nullability
        if (castNode.IsParentKind(SyntaxKind.Interpolation) && originalConversionOperation.Type?.SpecialType is SpecialType.System_Object)
            return true;
 
        // There are cases where the types change but things may still be safe to remove.
 
        // Case1.  A value type casted to `object` is safe if it's now getting converted to `dynamic`.
        // At runtime `dynamic` is just an `object` as well, and precasting to `object` will end up
        // with the same value and type still in the `dynamic` final location.
        if (originalConversion.IsBoxing && rewrittenConversion.IsBoxing &&
            originalConvertedType.IsReferenceType && rewrittenConvertedType.TypeKind == TypeKind.Dynamic)
        {
            return true;
        }
 
        // There are cases where a cast does have runtime meaning, but can be removed from one location because
        // the same effective conversion would happen in the code in a different location.  For example:
        //
        //      int? a = b ? (int?)0 : 1
        //
        // remove this cast will change the meaning of that conditional.  It will now produce an int instead of
        // an int?.  However, we know the same integral value will be produced by the conditional, but will then
        // be wrapped with a final conversion back into an int?.
        if (IsConditionalCastSafeToRemove(
                castNode, originalSemanticModel,
                rewrittenExpression, rewrittenSemanticModel, cancellationToken))
        {
            return true;
        }
 
        // Widening a value before bitwise negation produces the same value as bitwise negation
        // followed by the same widening.  For example:
        //
        // public static long P(long a, int b)
        //     => a & ~[|(long)|]b;
        if (IsRemovableWideningSignedBitwiseNegation(
                castNode, originalConversionOperation,
                rewrittenExpression, rewrittenSemanticModel, cancellationToken))
        {
            return true;
        }
 
        // (float?)(int?)2147483647
        //
        // The inner cast is not necessary here because there is already a lifted nullable conversion
        // of the innermost expression to the outer conversion type.
        if (IsMultipleImplicitNullableConversion(originalConversionOperation))
            return true;
 
        #endregion allowed cases.
 
        return false;
    }
 
    private static bool IsCollectionExpressionCastThatMustBePreserved(
        ExpressionSyntax castNode,
        SemanticModel originalSemanticModel,
        ITypeSymbol originalConvertedType,
        CancellationToken cancellationToken)
    {
        // If we can't figure out the type we were casting to, then preserve this cast.
        var castedType = originalSemanticModel.GetTypeInfo(castNode, cancellationToken).Type;
        if (castedType is null)
            return true;
 
        // If the types are exactly the same, like:
        //
        // int[] x = (int[])[1, 2, 3];
        //
        // then this is fine to remove.
        if (originalConvertedType.Equals(castedType, SymbolEqualityComparer.IncludeNullability))
            return false;
 
        // the original and resultant types are *not* the same.  This is sometimes ok to remove depending on which
        // types it is.  Currently, we only support special behavior for named types.
 
        if (castedType is not INamedTypeSymbol namedCastedType ||
            originalConvertedType is not INamedTypeSymbol originalNamedConvertedType)
        {
            return true;
        }
 
        if (namedCastedType.TypeArguments.Length != 1 && originalNamedConvertedType.TypeArguments.Length != 1)
            return true;
 
        if (!originalNamedConvertedType.TypeArguments[0].Equals(namedCastedType.TypeArguments[0], SymbolEqualityComparer.IncludeNullability))
            return true;
 
        // ReadOnlySpan<T> x = (Span<T>)[a, b, c];
        //
        // unnecessary cast to widened span type.  Because it narrows again,
        // this is safe to elide as the compiler will go directly to ReadOnlySpan<T> itself.
        if (originalConvertedType.IsReadOnlySpan() && castedType.IsSpan())
            return false;
 
        // IEnumerable<T> x = (DerivedReadOnlyInterfaceType<T>)[a, b, c]; // also for IReadOnlyCollection and IReadOnlyList
        if (originalNamedConvertedType.OriginalDefinition.SpecialType is SpecialType.System_Collections_Generic_IEnumerable_T &&
            namedCastedType.OriginalDefinition.SpecialType is SpecialType.System_Collections_Generic_IReadOnlyCollection_T or SpecialType.System_Collections_Generic_IReadOnlyList_T)
        {
            return false;
        }
 
        // ICollection<T> x = (IList<T>)[a, b, c];
        if (originalNamedConvertedType.OriginalDefinition.SpecialType is SpecialType.System_Collections_Generic_ICollection_T &&
            namedCastedType.OriginalDefinition.SpecialType is SpecialType.System_Collections_Generic_IList_T)
        {
            return false;
        }
 
        // ICollection<T> x = (List<T>)[a, b, c]; // also for IList
        if (originalNamedConvertedType.OriginalDefinition.SpecialType is SpecialType.System_Collections_Generic_ICollection_T or SpecialType.System_Collections_Generic_IList_T &&
            namedCastedType.OriginalDefinition.Equals(originalSemanticModel.Compilation.ListOfTType()))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool IsIdentityStructCastThatMustBePreserved(
        ExpressionSyntax castNode, ExpressionSyntax castedExpressionNode, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        // Identity struct casts will make a copy.  This copy may need to be kept to preserve semantics that only
        // the copy is being manipulated and not the original struct.
        //
        // Note: this is an innacurate heuristic.  Generally speaking, practically any member accessed off of a
        // struct might mutate it (like accessing .Length on an ImmutableArray).  But practically speaking that is
        // highly unlikely to actually mutate.  To avoid many false negatives from allowing us to simplify pointless
        // struct casts, we only look for a very narrow case that just rises up to be potentially problematic.
        // Specifically, the invocation of a non-known method on a non-known struct type where neitehr the struct
        // nor method are readonly.
 
        var conversion = semanticModel.GetConversion(castedExpressionNode, cancellationToken);
        if (!conversion.IsIdentity)
            return false;
 
        var castType = semanticModel.GetTypeInfo(castNode, cancellationToken).Type;
 
        if (castType is null)
            return false;
 
        // we presume all the built-in types are immutable, so we can skip copying them.
        if (castType.IsSpecialType())
            return false;
 
        // if it's not a struct, then we're not making a copy and can safely remove the cast.
        if (!castType.IsStructType())
            return false;
 
        // if the struct is readonly, then we can safely remove the cast as it's not mutable.
        if (castType.IsReadOnly)
            return false;
 
        // Only have to care if we're actually casting a location (an LVALUE).  If we're operating on an rvalue then
        // we already have a copy.
        var castedSymbol = semanticModel.GetSymbolInfo(castedExpressionNode, cancellationToken).GetAnySymbol();
        if (castedSymbol is not IFieldSymbol and not ILocalSymbol and not IParameterSymbol and not IParameterSymbol { RefKind: RefKind.Ref })
            return false;
 
        // ok, we have some *potentially* mutable struct.  In practice these are rare, but do exist. We'll
        // optimistically presume it's ok to remove thist cast *unless* it's the form: `((X)x).SomeMethod()` (where
        // SomeMethod is not an override from System.Object and is not readonly itself).
        if (castNode.WalkUpParentheses().Parent is not MemberAccessExpressionSyntax { Parent: InvocationExpressionSyntax } memberAccessExpression)
            return false;
 
        var memberSymbol = semanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).GetAnySymbol();
        if (memberSymbol is not IMethodSymbol methodSymbol)
            return false;
 
        // if it's a readonly method, it's fine to call on the original without copying.
        if (methodSymbol.IsReadOnly)
            return false;
 
        for (var current = methodSymbol; current != null; current = current.OverriddenMethod)
        {
            if (current.ContainingType.SpecialType == SpecialType.System_Object)
                return false;
        }
 
        // Ok, calling some method that could mutate this struct.  have to keep this cast.
        return true;
    }
 
    private static bool IsMultipleImplicitNullableConversion(IConversionOperation originalConversionOperation)
    {
        // (float?)(int?)2147483647
 
        var innerOriginalConversion = originalConversionOperation.GetConversion();
        if (!innerOriginalConversion.IsImplicit || !innerOriginalConversion.IsNullable)
            return false;
 
        // if the inner conversion was user defined, we need to keep it as it may have executed user code.
        if (innerOriginalConversion.IsUserDefined)
            return false;
 
        if (originalConversionOperation.Parent is not IConversionOperation outerOriginalConversionOperation)
            return false;
 
        var outerOriginalConversion = outerOriginalConversionOperation.GetConversion();
        if (!outerOriginalConversion.IsImplicit || !outerOriginalConversion.IsNullable)
            return false;
 
        return true;
    }
 
    private static bool IsRemovableWideningSignedBitwiseNegation(
        ExpressionSyntax castNode, IConversionOperation originalConversionOperation,
        ExpressionSyntax rewrittenExpression, SemanticModel rewrittenSemanticModel,
        CancellationToken cancellationToken)
    {
        // Can potentially remove the cast in:
        //
        // public static long P(long a, int b)
        //     => a & ~[|(long)|]b;
        //
        // We need to have an implicit numeric conversion.  Parented by a ~. After removing the cast, we should
        // have the same conversion now implicitly on the outside of the `~`.
        // 
        // Similarly, the casted type needs to be the same type we get post rewrite outside the `~`.
        //
        // Note: this removal only works with signed integers.  With unsigned integers the distinction matters.
        // Consider ~(ulong)uintVal vs (ulong)~uintVal.  the former will extend out the value with 0s, which
        // will all be flipped to 1s.  The latter will flip any leading 0s to 1s, but will then extend out the
        // rest with 1s.
 
        var originalConversion = originalConversionOperation.GetConversion();
        if (!originalConversion.IsImplicit || !originalConversion.IsNumeric)
            return false;
 
        if (!IsSignedIntegralOrIntPtrType(originalConversionOperation.Type) ||
            !IsSignedIntegralOrIntPtrType(originalConversionOperation.Operand.Type))
        {
            return false;
        }
 
        var parent = castNode.WalkUpParentheses().GetRequiredParent();
        if (parent is not PrefixUnaryExpressionSyntax(SyntaxKind.BitwiseNotExpression))
            return false;
 
        // If we were parented by a bitwise negation before, we must also be afterwards.
        var rewrittenBitwiseNotExpression = (PrefixUnaryExpressionSyntax)rewrittenExpression.WalkUpParentheses().GetRequiredParent();
        Debug.Assert(rewrittenBitwiseNotExpression.Kind() == SyntaxKind.BitwiseNotExpression);
 
        var rewrittenOperation = rewrittenSemanticModel.GetOperation(rewrittenBitwiseNotExpression, cancellationToken);
        if (rewrittenOperation is not IUnaryOperation { OperatorKind: UnaryOperatorKind.BitwiseNegation })
            return false;
 
        // Post rewrite we need to have the same conversion outside that `~` that we had inside.
        if (rewrittenOperation.Parent is not IConversionOperation rewrittenBitwiseNotConversionOperation)
            return false;
 
        var rewrittenBitwiseNotConversion = rewrittenBitwiseNotConversionOperation.GetConversion();
        if (originalConversion != rewrittenBitwiseNotConversion)
            return false;
 
        // Ensure the types of the cast-inside is the same as the type outside the rewritten `~`.
        var originalConvertedType = originalConversionOperation.Type;
        var rewrittenBitwiseNotConversionType = rewrittenBitwiseNotConversionOperation.Type;
        if (IsNullOrErrorType(originalConvertedType) ||
            IsNullOrErrorType(rewrittenBitwiseNotConversionType))
        {
            return false;
        }
 
        if (!originalConvertedType.Equals(rewrittenBitwiseNotConversionType, SymbolEqualityComparer.IncludeNullability))
            return false;
 
        return true;
    }
 
    private static bool IsSignedIntegralOrIntPtrType(ITypeSymbol? type)
        => type.IsSignedIntegralType() || type?.SpecialType is SpecialType.System_IntPtr;
 
    private static bool IsConditionalCastSafeToRemove(
        ExpressionSyntax castNode, SemanticModel originalSemanticModel,
        ExpressionSyntax rewrittenExpression, SemanticModel rewrittenSemanticModel, CancellationToken cancellationToken)
    {
        if (castNode is not CastExpressionSyntax castExpression)
            return false;
 
        var parent = castExpression.WalkUpParentheses();
        if (parent.Parent is not ConditionalExpressionSyntax originalConditionalExpression)
            return false;
 
        // if we were parented by a conditional before, we must be parented by a conditional afterwards.
        var rewrittenConditionalExpression = (ConditionalExpressionSyntax)rewrittenExpression.WalkUpParentheses().GetRequiredParent();
 
        if (parent != originalConditionalExpression.WhenFalse && parent != originalConditionalExpression.WhenTrue)
            return false;
 
        if (originalSemanticModel.GetOperation(castExpression, cancellationToken) is not IConversionOperation conversionOperation)
            return false;
 
        var originalConversion = conversionOperation.GetConversion();
        if (!originalConversion.IsNullable && !originalConversion.IsNumeric)
            return false;
 
        if (originalConversion.IsNullable)
        {
            // if we have `a ? (int?)b : default` then we can't remove the nullable cast as it changes the
            // meaning of `default`.
            if (originalConditionalExpression.WhenTrue.WalkDownParentheses().IsKind(SyntaxKind.DefaultLiteralExpression) ||
                originalConditionalExpression.WhenFalse.WalkDownParentheses().IsKind(SyntaxKind.DefaultLiteralExpression))
            {
                return false;
            }
        }
 
        var originalCastExpressionTypeInfo = originalSemanticModel.GetTypeInfo(castExpression, cancellationToken);
        var originalConditionalTypeInfo = originalSemanticModel.GetTypeInfo(originalConditionalExpression, cancellationToken);
        var rewrittenConditionalTypeInfo = rewrittenSemanticModel.GetTypeInfo(rewrittenConditionalExpression, cancellationToken);
 
        if (IsNullOrErrorType(originalCastExpressionTypeInfo) ||
            IsNullOrErrorType(originalConditionalTypeInfo) ||
            IsNullOrErrorType(rewrittenConditionalTypeInfo))
        {
            return false;
        }
 
        // when we have    a ? (T)b : c
        // 
        // then we want the type of the written conditional to be T as well.  And we want the final converted
        // type of `a ? b : c` to be the same as what `a ? (T)b : c` is converted to.
 
        if (!originalConditionalTypeInfo.ConvertedType!.Equals(rewrittenConditionalTypeInfo.ConvertedType, SymbolEqualityComparer.IncludeNullability))
            return false;
 
        var castType = originalSemanticModel.GetTypeInfo(castExpression, cancellationToken).Type;
        if (IsNullOrErrorType(castType))
            return false;
 
        if (rewrittenSemanticModel.GetOperation(rewrittenConditionalExpression, cancellationToken) is not IConditionalOperation rewrittenConditionalOperation)
            return false;
 
        if (castType.Equals(rewrittenConditionalOperation.Type, SymbolEqualityComparer.IncludeNullability))
            return true;
 
        if (rewrittenConditionalOperation.Parent is IConversionOperation conditionalParentConversion &&
            conditionalParentConversion.GetConversion().IsImplicit &&
            castType.Equals(conditionalParentConversion.Type, SymbolEqualityComparer.IncludeNullability))
        {
            return true;
        }
 
        return false;
    }
 
    private static bool IsNullOrErrorType(TypeInfo info)
        => IsNullOrErrorType(info.Type) || IsNullOrErrorType(info.ConvertedType);
 
    private static bool IsNullOrErrorType([NotNullWhen(false)] ITypeSymbol? type)
        => type is null || type is IErrorTypeSymbol;
 
    private static bool CastRemovalWouldCauseUnintendedReferenceComparisonWarning(
        ExpressionSyntax expression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        // Translated from DiagnosticPass.CheckRelationals
        var parentBinary = expression.WalkUpParentheses().GetRequiredParent() as BinaryExpressionSyntax;
        if (parentBinary != null && parentBinary.Kind() is SyntaxKind.EqualsExpression or SyntaxKind.NotEqualsExpression)
        {
            var operation = semanticModel.GetOperation(parentBinary, cancellationToken);
            if (operation.UnwrapImplicitConversion() is IBinaryOperation binaryOperation)
            {
                if (binaryOperation.LeftOperand.Type?.SpecialType == SpecialType.System_Object &&
                    !IsExplicitCast(parentBinary.Left) &&
                    !IsConstantNull(binaryOperation.LeftOperand) &&
                    ConvertedHasUserDefinedEquals(binaryOperation.OperatorKind, binaryOperation.RightOperand))
                {
                    return true;
                }
                else if (binaryOperation.RightOperand.Type?.SpecialType == SpecialType.System_Object &&
                    !IsExplicitCast(parentBinary.Right) &&
                    !IsConstantNull(binaryOperation.RightOperand) &&
                    ConvertedHasUserDefinedEquals(binaryOperation.OperatorKind, binaryOperation.LeftOperand))
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
    private static bool ConvertedHasUserDefinedEquals(BinaryOperatorKind operatorKind, IOperation operation)
    {
        // translated from DiagnosticPass.ConvertedHasEqual
 
        if (operation is not IConversionOperation conversionOperation)
            return false;
 
        if (IsExplicitCast(conversionOperation.Syntax))
            return false;
 
        if (conversionOperation.Operand.Type is not INamedTypeSymbol original)
            return false;
 
        if (!original.IsReferenceType || original.TypeKind == TypeKind.Interface)
            return false;
 
        var opName = operatorKind == BinaryOperatorKind.Equals
            ? WellKnownMemberNames.EqualityOperatorName
            : WellKnownMemberNames.InequalityOperatorName;
        for (var type = original; type != null; type = type.BaseType)
        {
            foreach (var sym in type.GetMembers(opName))
            {
                if (sym is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator } op)
                {
                    var parameters = op.GetParameters();
                    if (parameters.Length == 2 &&
                        type.Equals(parameters[0].Type) &&
                        type.Equals(parameters[1].Type))
                    {
                        return true;
                    }
                }
            }
        }
 
        return false;
    }
 
    private static bool IsConstantNull(IOperation operation)
        => operation.ConstantValue.HasValue && operation.ConstantValue.Value is null;
 
    private static bool IsExplicitCast(SyntaxNode node)
        => node is ExpressionSyntax expression && expression.WalkDownParentheses().Kind() is SyntaxKind.CastExpression or SyntaxKind.AsExpression;
 
    private static bool IsExplicitCastThatMustBePreserved(
        SemanticModel semanticModel,
        ExpressionSyntax castOrAsNode,
        Conversion conversion,
        CancellationToken cancellationToken)
    {
        if (!conversion.IsExplicit)
            return false;
 
        // Some explicit casts are safe to remove as they still will have no runtime impact, (or the compiler would
        // insert the implicit cast for it later due to surrounding context).
 
        // Explicit identity casts arise with things like `(string?)""`.  In this case, there is no runtime impact,
        // just type system impact.  This is a candidate for removal, and our later checks will ensure the same 
        // types remain.
        if (conversion.IsIdentity)
            return false;
 
        // Explicit nullable casts arise with things like `(int?)0`.  These will succeed at runtime, but are potentially
        // removable if the language would insert such a cast anyways (for things like `x ? (int?)0 : null`).  In C# 9
        // and above this will create a legal conditional conversion that implicitly adds that cast.
        //
        // Note: this does not apply for `as byte?`.  This is an explicit as-cast that can produce null values and
        // so it should be maintained.
        if (conversion.IsNullable && castOrAsNode is CastExpressionSyntax castExpression)
        {
            var parent = castOrAsNode.WalkUpParentheses();
            if (parent.Parent is ConditionalExpressionSyntax conditionalExpression)
            {
                // If we have `(T?)expr == null` or `null == (T?)expr` then we can potentially remove this cast as
                // the lang will implicitly create such a cast with an appropriate type and null.
                var (castSide, otherSide) = conditionalExpression.WhenTrue == parent
                    ? (conditionalExpression.WhenTrue, conditionalExpression.WhenFalse)
                    : (conditionalExpression.WhenFalse, conditionalExpression.WhenTrue);
 
                if (otherSide.WalkDownParentheses().Kind() == SyntaxKind.NullLiteralExpression)
                    return false;
 
                // if we have `(T?)TExpr == nullableTExpr` then we can also remove this cast as the language will
                // insert the same nullable widening cast implicitly.
                //
                // If we have `(T?)TExpr == TExpr` then we can potentially remove this cast if the caller determines
                // that there is an outer contextual cast to `T?` higher up.
                var castSideType = semanticModel.GetTypeInfo(castSide, cancellationToken).Type;
                var castedExpressionType = semanticModel.GetTypeInfo(castExpression.Expression, cancellationToken).Type;
 
                if (castSideType.IsNullable(out var underlyingType) && Equals(underlyingType, castedExpressionType))
                {
                    var otherSideType = semanticModel.GetTypeInfo(otherSide, cancellationToken).Type;
                    if (Equals(castSideType, otherSideType) || Equals(underlyingType, otherSideType))
                        return false;
                }
            }
        }
 
        return true;
    }
 
    private static bool IsIdentityFloatingPointCastThatMustBePreserved(
       ExpressionSyntax castNode, ExpressionSyntax castedExpressionNode,
       SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        var conversion = semanticModel.GetConversion(castedExpressionNode, cancellationToken);
        if (!conversion.IsIdentity)
            return false;
 
        var castType = semanticModel.GetTypeInfo(castNode, cancellationToken).Type;
        var castedExpressionType = semanticModel.GetTypeInfo(castedExpressionNode, cancellationToken).Type;
 
        // Floating point casts can have subtle runtime behavior, even between the same fp types. For example, a
        // cast from float-to-float can still change behavior because it may take a higher precision computation and
        // truncate it to 32bits.
        //
        // Because of this we keep floating point conversions unless we can prove that it's safe.  The only safe
        // times are when we're loading or storing into a location we know has the same size as the cast size
        // (i.e. reading/writing into a field).
        if (!IsFloatingPointType(castedExpressionType) ||
            !IsFloatingPointType(castType))
        {
            // wasn't a floating point conversion.
            return false;
        }
 
        // Identity fp conversion is safe if this is a read from a fp field/array
        if (IsFieldOrArrayElement(semanticModel, castedExpressionNode, cancellationToken))
            return false;
 
        // Boxing the result will automatically truncate this as well as this must be stored into a real 32bit or
        // 64bit location.  As such, the explicit cast to truncate to 32/64 isn't necessary.  See
        // https://github.com/dotnet/roslyn/pull/56932#discussion_r725241921 for more details.
        var parentConversion = semanticModel.GetConversion(castNode, cancellationToken);
        if (parentConversion.Exists && parentConversion.IsBoxing)
            return false;
 
        // It wasn't a read from a fp/field/array.  But it might be a write into one.
 
        castNode = castNode.WalkUpParentheses();
        if (castNode.Parent is AssignmentExpressionSyntax assignmentExpression &&
            assignmentExpression.Right == castNode)
        {
            // Identity fp conversion is safe if this is a write to a fp field/array
            if (IsFieldOrArrayElement(semanticModel, assignmentExpression.Left, cancellationToken))
                return false;
        }
        else if (castNode.Parent is InitializerExpressionSyntax(SyntaxKind.ArrayInitializerExpression) arrayInitializer)
        {
            // Identity fp conversion is safe if this is in an array initializer.
            var typeInfo = semanticModel.GetTypeInfo(arrayInitializer, cancellationToken);
            return typeInfo.Type?.Kind == SymbolKind.ArrayType;
        }
        else if (castNode.Parent is EqualsValueClauseSyntax equalsValue &&
                 equalsValue.Value == castNode &&
                 equalsValue.Parent is VariableDeclaratorSyntax variableDeclarator)
        {
            // Identity fp conversion is safe if this is in a field initializer.
            var symbol = semanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken);
            if (symbol?.Kind == SymbolKind.Field)
                return false;
        }
 
        // We have to preserve this cast.
        return true;
    }
 
    private static bool IsFloatingPointType(ITypeSymbol? type)
        => type?.SpecialType is SpecialType.System_Double or SpecialType.System_Single;
 
    private static bool IsFieldOrArrayElement(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
    {
        var operation = semanticModel.GetOperation(expression.WalkDownParentheses(), cancellationToken);
        return operation is IFieldReferenceOperation or IArrayElementReferenceOperation;
    }
 
    private static bool IntroducedConditionalExpressionConversion(
        ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        for (SyntaxNode? current = expression; current != null; current = current.Parent)
        {
            var conversion = semanticModel.GetConversion(current, cancellationToken);
            if (conversion.IsConditionalExpression)
                return true;
        }
 
        return false;
    }
 
    private static bool IntroducedAmbiguity(
        ExpressionSyntax castNode, ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel, SemanticModel rewrittenSemanticModel,
        CancellationToken cancellationToken)
    {
        for (SyntaxNode? currentOld = castNode.WalkUpParentheses().Parent, currentNew = rewrittenExpression.WalkUpParentheses().Parent;
             currentOld != null && currentNew != null;
             currentOld = currentOld.Parent, currentNew = currentNew.Parent)
        {
            Debug.Assert(currentOld.Kind() == currentNew.Kind());
            var oldSymbolInfo = originalSemanticModel.GetSymbolInfo(currentOld, cancellationToken);
            if (oldSymbolInfo.Symbol != null)
            {
                // if previously we bound to a single symbol, but now we don't, then we introduced an
                // error of some sort.  Have to bail out immediately and keep the cast.
                var newSymbolInfo = rewrittenSemanticModel.GetSymbolInfo(currentNew, cancellationToken);
                if (newSymbolInfo.Symbol is null)
                    return true;
            }
 
            if (currentOld is InterpolatedStringExpressionSyntax && currentNew is InterpolatedStringExpressionSyntax)
            {
                // In the case of interpolations, we need to dive into the operation level to determine if the meaning
                // of the the interpolation stayed the same in the case of interpolation handlers.
                if (originalSemanticModel.GetOperation(currentOld, cancellationToken) is not IInterpolatedStringOperation oldInterpolationOperation)
                    return true;
 
                if (rewrittenSemanticModel.GetOperation(currentNew, cancellationToken) is not IInterpolatedStringOperation newInterpolationOperation)
                    return true;
 
                if (oldInterpolationOperation.Parts.Length != newInterpolationOperation.Parts.Length)
                    return true;
 
                for (int i = 0, n = oldInterpolationOperation.Parts.Length; i < n; i++)
                {
                    var oldInterpolationPart = oldInterpolationOperation.Parts[i];
                    var newInterpolationPart = newInterpolationOperation.Parts[i];
                    if (oldInterpolationPart.Kind != newInterpolationPart.Kind)
                        return true;
 
                    // If we were calling some interpolation AppendFormatted helper, and now we're not, we introduced a problem.
                    if (oldInterpolationPart is IInterpolatedStringAppendOperation { AppendCall: not IInvalidOperation } &&
                        newInterpolationPart is IInterpolatedStringAppendOperation { AppendCall: IInvalidOperation })
                    {
                        return true;
                    }
                }
            }
        }
 
        return false;
    }
 
    private static bool ChangedOverloadResolution(
        ExpressionSyntax castNode, ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel, SemanticModel rewrittenSemanticModel,
        CancellationToken cancellationToken)
    {
        // walk upwards checking overload resolution results.  note: we skip until we hit the first argument
        // as we don't care about symbol resolution changing when removing a cast in something like `((D)b).X()`
        var haveHitArgumentNode = false;
        for (SyntaxNode? currentOld = castNode.WalkUpParentheses().Parent, currentNew = rewrittenExpression.WalkUpParentheses().Parent;
             currentOld != null && currentNew != null;
             currentOld = currentOld.Parent, currentNew = currentNew.Parent)
        {
            Debug.Assert(currentOld.Kind() == currentNew.Kind());
            if (!haveHitArgumentNode && currentOld.Kind() != SyntaxKind.Argument)
                continue;
 
            haveHitArgumentNode = true;
 
            var oldSymbolInfo = originalSemanticModel.GetSymbolInfo(currentOld, cancellationToken).Symbol;
            var newSymbolInfo = rewrittenSemanticModel.GetSymbolInfo(currentNew, cancellationToken).Symbol;
 
            // ignore local functions.  First, we can't test them for equality in speculative situations, but also we 
            // can't end up with an overload resolution issue for them as they don't have overloads.
            if (oldSymbolInfo is IMethodSymbol method &&
                method.MethodKind is not (MethodKind.LocalFunction or MethodKind.LambdaMethod) &&
                !Equals(oldSymbolInfo, newSymbolInfo))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool ChangedForEachResolution(
        ExpressionSyntax castNode, ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel, SemanticModel rewrittenSemanticModel)
    {
        for (SyntaxNode? currentOld = castNode.WalkUpParentheses().Parent, currentNew = rewrittenExpression.WalkUpParentheses().Parent;
             currentOld != null && currentNew != null;
             currentOld = currentOld.Parent, currentNew = currentNew.Parent)
        {
            Debug.Assert(currentOld.Kind() == currentNew.Kind());
            if (currentOld is CommonForEachStatementSyntax oldForEach &&
                currentNew is CommonForEachStatementSyntax newForEach)
            {
                // TODO(cyrusn): Do we need to validate anything else in the foreach infos?
                var oldForEachInfo = originalSemanticModel.GetForEachStatementInfo(oldForEach);
                var newForEachInfo = rewrittenSemanticModel.GetForEachStatementInfo(newForEach);
 
                var oldConversion = oldForEachInfo.ElementConversion;
                var newConversion = newForEachInfo.ElementConversion;
 
                if (oldConversion.IsUserDefined != newConversion.IsUserDefined)
                    return true;
 
                if (!Equals(oldConversion.MethodSymbol, newConversion.MethodSymbol))
                    return true;
            }
        }
 
        return false;
    }
 
    private static bool IsComplementaryMemberAccessAfterCastRemoval(
        MemberAccessExpressionSyntax memberAccessExpression,
        ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel,
        SemanticModel rewrittenSemanticModel,
        CancellationToken cancellationToken)
    {
        var originalMemberSymbol = originalSemanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol;
        if (originalMemberSymbol is null)
            return false;
 
        var rewrittenMemberAccessExpression = (MemberAccessExpressionSyntax)rewrittenExpression.WalkUpParentheses().GetRequiredParent();
        var rewrittenMemberSymbol = rewrittenSemanticModel.GetSymbolInfo(rewrittenMemberAccessExpression, cancellationToken).Symbol;
        if (rewrittenMemberSymbol is null)
            return false;
 
        if (originalMemberSymbol.Kind != rewrittenMemberSymbol.Kind)
            return false;
 
        // check for: ((X)expr).Invoke(...);
        if (IsComplementaryDelegateInvoke(originalMemberSymbol, rewrittenMemberSymbol))
            return true;
 
        // Ok, we had two good member symbols before/after the cast removal.  In other words we have:
        //
        //      ((X)expr).Y
        //      (expr).Y
 
        // Next, see if this is a call to an interface method.
        if (originalMemberSymbol.ContainingType.TypeKind == TypeKind.Interface)
        {
            var rewrittenType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).Type;
            if (IsNullOrErrorType(rewrittenType))
                return false;
 
            // If we don't have a reference type, then it may not be safe to remove the cast.  The cast could
            // could have been boxing the value and removing that could cause us to operate not on the copy.
            //
            // Note: intrinsics and enums are also safe as we know they don't have state and thus
            // will have the same semantics whether or not they're boxed.
            //
            // It is also safe if we know the value is already a copy to begin with.
            //
            // TODO(cyrusn): this may not be true of floating point numbers.  Are we sure that it's
            // safe to remove an interface cast in that case?  Could that cast narrow the precision of 
            // a wider FP number to a narrower amount (like 80bit FP to 64bit)?
 
            if (!rewrittenType.IsReferenceType &&
                !IsIntrinsicOrEnum(rewrittenType) &&
                !IsCopy(rewrittenSemanticModel, rewrittenExpression, rewrittenType, cancellationToken))
            {
                return false;
            }
 
            // if we are still calling through to the same interface method, then this is safe to call.
            if (Equals(originalMemberSymbol, rewrittenMemberSymbol))
                return true;
 
            // Ok, we have a type casted to an interface.  It may be safe to remove this interface cast
            // if we still call into the implementation of that interface member afterwards.  Note: the
            // type has to be sealed, otherwise the interface method may have been reimplemented lower
            // in the inheritance hierarchy.
            //
            // However, if this was an object creation expression, then we know the exact type that was
            // created, and don't have to worry about subclassing.
 
            var isSealed =
                rewrittenType.IsSealed ||
                rewrittenType.IsValueType ||
                rewrittenType.TypeKind == TypeKind.Array ||
                IsIntrinsicOrEnum(rewrittenType) ||
                rewrittenExpression.WalkDownParentheses() is ObjectCreationExpressionSyntax;
 
            if (!isSealed)
                return false;
 
            // Then look for the current implementation of that interface member.
            var rewrittenContainingType = rewrittenMemberSymbol.ContainingType;
            var implementationMember = rewrittenContainingType.FindImplementationForInterfaceMember(originalMemberSymbol);
            if (implementationMember is null)
                return false;
 
            // if that's not the method we're currently calling, then this definitely isn't safe to remove.
            return
                Equals(implementationMember, rewrittenMemberSymbol) &&
                ParameterNamesAndDefaultValuesAndReturnTypesMatch(
                    memberAccessExpression, originalSemanticModel, originalMemberSymbol, rewrittenMemberSymbol, cancellationToken);
        }
 
        // Second, check if this is a virtual call to a different location in the inheritance hierarchy.
        // Importantly though, because of covariant return types, we have to make sure the overrides 
        // agree on the return type, or else this could change the final type of hte expression.
        for (var current = rewrittenMemberSymbol; current != null; current = current.GetOverriddenMember())
        {
            if (Equals(originalMemberSymbol, current))
            {
                // we're calling into a override of a higher up virtual in the original code.
                // This is safe as long as the names of the parameters and all default values
                // are the same.  This is because the compiler uses the names and default
                // values of the overridden member, even though it emits a virtual call to the
                // the highest in the inheritance chain.
                return ParameterNamesAndDefaultValuesAndReturnTypesMatch(
                    memberAccessExpression, originalSemanticModel, originalMemberSymbol, rewrittenMemberSymbol, cancellationToken);
            }
        }
 
        return false;
    }
 
    private static bool IsComplementaryInvocationAfterCastRemoval(
        InvocationExpressionSyntax memberAccessExpression,
        ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel,
        SemanticModel rewrittenSemanticModel,
        CancellationToken cancellationToken)
    {
        var originalMemberSymbol = originalSemanticModel.GetSymbolInfo(memberAccessExpression, cancellationToken).Symbol;
        if (originalMemberSymbol is null)
            return false;
 
        var rewrittenMemberAccessExpression = (InvocationExpressionSyntax)rewrittenExpression.WalkUpParentheses().GetRequiredParent();
        var rewrittenMemberSymbol = rewrittenSemanticModel.GetSymbolInfo(rewrittenMemberAccessExpression, cancellationToken).Symbol;
        if (rewrittenMemberSymbol is null)
            return false;
 
        return IsComplementaryDelegateInvoke(originalMemberSymbol, rewrittenMemberSymbol);
    }
 
    private static bool IsComplementaryDelegateInvoke(ISymbol originalMemberSymbol, ISymbol rewrittenMemberSymbol)
    {
        if (originalMemberSymbol is not IMethodSymbol { MethodKind: MethodKind.DelegateInvoke } originalMethodSymbol ||
            rewrittenMemberSymbol is not IMethodSymbol { MethodKind: MethodKind.DelegateInvoke } rewrittenMethodSymbol)
        {
            return false;
        }
 
        // if we're invoking a delegate method, then the removal of the cast is mostly safe (as the 
        // compiler will only allow implicit reference conversions between variant delegates and 
        // variant delegates will only allow different implicit reference conversions of their
        // parameters and return type.
 
        // However, if the delegate return type differs, then that could change semantics higher
        // up, so we must disallow this if they're not the same.
        return Equals(originalMethodSymbol.ReturnType, rewrittenMethodSymbol.ReturnType);
    }
 
    private static bool IsIntrinsicOrEnum(ITypeSymbol rewrittenType)
        => rewrittenType.IsIntrinsicType() ||
           rewrittenType.IsEnumType() ||
           rewrittenType.SpecialType == SpecialType.System_Enum;
 
    private static bool IsCopy(
        SemanticModel semanticModel,
        ExpressionSyntax expression,
        ITypeSymbol rewrittenType,
        CancellationToken cancellationToken)
    {
        // Checked by caller first.
        Debug.Assert(!rewrittenType.IsReferenceType && !IsIntrinsicOrEnum(rewrittenType));
 
        // Be conservative here.  If we can't prove it's not a copy assume it's a copy.
        expression = expression.WalkDownParentheses();
        var operation = semanticModel.GetOperation(expression, cancellationToken);
        if (operation != null)
        {
            // All operators return a fresh copy.  Note: this may need to be revisited if operators
            // ever can return byref in the future.
            if (operation is IBinaryOperation { OperatorMethod: not null })
                return true;
 
            if (operation is IUnaryOperation { OperatorMethod: not null })
                return true;
 
            // if we're getting the struct through a non-ref property, then it will make a copy.
            if (operation is IPropertyReferenceOperation { Property.RefKind: not RefKind.Ref })
                return true;
 
            // if we're getting the struct as the return value of a non-ref method, then it will make a copy.
            if (operation is IInvocationOperation { TargetMethod.RefKind: not RefKind.Ref })
                return true;
 
            // If we're new'ing up this struct then we have a fresh copy that we can operate on.
            if (operation is IObjectCreationOperation)
                return true;
        }
 
        return false;
    }
 
    private static bool ParameterNamesAndDefaultValuesAndReturnTypesMatch(
        MemberAccessExpressionSyntax memberAccessExpression, SemanticModel semanticModel,
        ISymbol originalMemberSymbol, ISymbol rewrittenMemberSymbol, CancellationToken cancellationToken)
    {
        var originalMemberType = originalMemberSymbol.GetMemberType();
        var rewrittenMemberType = rewrittenMemberSymbol.GetMemberType();
        if (!Equals(originalMemberType, rewrittenMemberType))
            return false;
 
        // if this member actually invoked, ensure that we end up with the same values for default
        // parameters, and that the same names are used.  Note: we technically only need to check
        // default values for arguments not passed, and we only need to check names for those that
        // are passed.
        if (memberAccessExpression.GetRequiredParent() is InvocationExpressionSyntax invocationExpression &&
            semanticModel.GetOperation(invocationExpression, cancellationToken) is IInvocationOperation invocationOperation)
        {
            if (originalMemberSymbol is IMethodSymbol originalMethodSymbol &&
                rewrittenMemberSymbol is IMethodSymbol rewrittenMethodSymbol)
            {
                var originalParameters = originalMethodSymbol.Parameters;
                var rewrittenParameters = rewrittenMethodSymbol.Parameters;
                if (originalParameters.Length != rewrittenParameters.Length)
                    return false;
 
                for (var i = 0; i < originalParameters.Length; i++)
                {
                    var originalParameter = originalParameters[i];
                    var rewrittenParameter = rewrittenParameters[i];
 
                    var argument = invocationOperation.Arguments.FirstOrDefault(a => Equals(originalParameter, a.Parameter));
                    var argumentSyntax = argument?.Syntax as ArgumentSyntax;
 
                    if (originalParameter.Name != rewrittenParameter.Name &&
                        argumentSyntax?.NameColon != null)
                    {
                        // names are different.  this is a problem if the original user code provided a named arg here.
                        return false;
                    }
 
                    if (originalParameter.HasExplicitDefaultValue &&
                        rewrittenParameter.HasExplicitDefaultValue &&
                        !Equals(originalParameter.ExplicitDefaultValue, rewrittenParameter.ExplicitDefaultValue) &&
                        argumentSyntax == null)
                    {
                        // parameter values are different, this is a problem if the original user code did *not* provide
                        // an argument here.
                        return false;
                    }
                }
            }
        }
 
        return true;
    }
 
    private static (ITypeSymbol? rewrittenConvertedType, Conversion rewrittenConversion) GetRewrittenInfo(
        ExpressionSyntax castNode, ExpressionSyntax rewrittenExpression,
        SemanticModel originalSemanticModel, SemanticModel rewrittenSemanticModel,
        Conversion originalConversion, ITypeSymbol originalConvertedType,
        CancellationToken cancellationToken)
    {
        if (castNode.WalkUpParentheses().Parent is InterpolationSyntax)
        {
            // Workaround https://github.com/dotnet/roslyn/issues/56934
            // Compiler does not give a conversion inside an interpolation. However, all values in the interpolation
            // holes are converted to object.
            //
            // Note: this may need to be revisited with improved interpolated strings (as they could take
            // strongly typed args and could avoid the object boxing).
            var convertedType = originalConversion.IsIdentity ? originalConvertedType : originalSemanticModel.Compilation.ObjectType;
            return (convertedType, default);
        }
 
        var rewrittenConvertedType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).ConvertedType;
        var rewrittenConversion = rewrittenSemanticModel.GetConversion(rewrittenExpression, cancellationToken);
 
        return (rewrittenConvertedType, rewrittenConversion);
    }
 
    private static (SemanticModel? rewrittenSemanticModel, ExpressionSyntax? rewrittenExpression) GetSemanticModelWithCastRemoved(
        ExpressionSyntax castNode,
        ExpressionSyntax castedExpressionNode,
        SemanticModel originalSemanticModel,
        CancellationToken cancellationToken)
    {
        var analyzer = new SpeculationAnalyzer(castNode, castedExpressionNode, originalSemanticModel, cancellationToken);
 
        var rewrittenExpression = analyzer.ReplacedExpression;
        var rewrittenSemanticModel = analyzer.SpeculativeSemanticModel;
 
        // Because of error tolerance in the compiler layer, it's possible for an overload resolution error
        // to occur, but all the checks above pass.  Specifically, with overload resolution, the binding layer
        // will still return results (in lambdas especially) for one of the overloads.  For example:
        //
        //    Goo(x => (int)x);
        //    void Goo(Func<int, object> x)
        //    Goo(Func<string, object> x)
        //
        // Here, removing the cast will cause an ambiguity issue. However, the type of 'x' will still appear to
        // be an 'int' because of error tolerance.  To address this, walk up all containing invocations and 
        // make sure they're calls to the same methods.
        if (IntroducedAmbiguity(castNode, rewrittenExpression, originalSemanticModel, rewrittenSemanticModel, cancellationToken))
            return default;
 
        if (ChangedOverloadResolution(castNode, rewrittenExpression, originalSemanticModel, rewrittenSemanticModel, cancellationToken))
            return default;
 
        // It's possible that removing a cast in a foreach collection expression will change how the foreach methods
        // and conversions resolve.  Ensure these stay the same to proceed.
        if (ChangedForEachResolution(castNode, rewrittenExpression, originalSemanticModel, rewrittenSemanticModel))
            return default;
 
        // Removing a cast may cause a conditional-expression conversion to come into existence.  This is
        // fine as long as we're in C# 9 or above.
        if (originalSemanticModel.Compilation.LanguageVersion() < LanguageVersion.CSharp9 &&
            IntroducedConditionalExpressionConversion(rewrittenExpression, rewrittenSemanticModel, cancellationToken))
        {
            return default;
        }
 
        return (rewrittenSemanticModel, rewrittenExpression);
    }
}