/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Simplification/Simplifiers/CastSimplifier.cs
1 726 строк
88 KB
Ankita Khera
Fix IDE0004 false positive for nullable generic type argument casts (#83326)
01 май 2026, 23:55
Не верифицирован
01 май 2026, 23:55
6c71824
Код
Авторство
О чём код?
// 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.Collections.Immutable; 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)) { // Check if removing the cast changes the nullable type arguments of the expression type. // For example: // // var lines = new List<object?>(); // return new List<object>(lines as List<object>); // // Here the converted types both match `IEnumerable<object>`, but removing the `as List<object>` // cast changes the expression type from `List<object>?` to `List<object?>`. var originalExpressionType = originalSemanticModel.GetTypeInfo(castNode, cancellationToken).Type; var rewrittenExpressionType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).Type; var typeArgumentNullabilityUnchanged = HaveSameTypeArgumentNullability(originalExpressionType, rewrittenExpressionType); // If the types of the expressions are exactly the same, then we can remove safely, as long as // removing the cast doesn't change the nullable type arguments of the expression type. if (originalConvertedType.Equals(rewrittenConvertedType, SymbolEqualityComparer.IncludeNullability) && typeArgumentNullabilityUnchanged) { 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) && typeArgumentNullabilityUnchanged) { return true; } } // We can safely remove conversion 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; } // Similarly, we want to support this for: // // int? a = b switch { true => (int?)0, false => 1 } if (IsSwitchExpressionCaseCastSafeToRemove( 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) { // Defer to common helper to determine if the cast can be removed. This unified processing of `x ? y : z` and // `x switch { .. => y, .. => z, .. => w, ... }` expressions. return IsSwitchOrConditionalCastSafeToRemove( castNode, originalSemanticModel, rewrittenExpression, rewrittenSemanticModel, static parentExpression => parentExpression.Parent is ConditionalExpressionSyntax conditionalExpression && conditionalExpression.Condition != parentExpression ? conditionalExpression : null, static conditionalExpression => [conditionalExpression.WhenTrue, conditionalExpression.WhenFalse], static (conditionalExpression, armExpression) => { Contract.ThrowIfFalse(conditionalExpression.WhenTrue == armExpression || conditionalExpression.WhenFalse == armExpression); return armExpression == conditionalExpression.WhenTrue ? conditionalExpression.WhenFalse : conditionalExpression.WhenTrue; }, cancellationToken); } private static bool IsSwitchExpressionCaseCastSafeToRemove( ExpressionSyntax castNode, SemanticModel originalSemanticModel, ExpressionSyntax rewrittenExpression, SemanticModel rewrittenSemanticModel, CancellationToken cancellationToken) { // Defer to common helper to determine if the cast can be removed. This unified processing of `x ? y : z` and // `x switch { .. => y, .. => z, .. => w, ... }` expressions. return IsSwitchOrConditionalCastSafeToRemove( castNode, originalSemanticModel, rewrittenExpression, rewrittenSemanticModel, static parentExpression => parentExpression.Parent is SwitchExpressionArmSyntax { Parent: SwitchExpressionSyntax switchExpression } ? switchExpression : null, static switchExpression => switchExpression.Arms.SelectAsArray(a => a.Expression), static (switchExpression, armExpression) => { if (switchExpression.Arms.Count < 2) return null; var arm = switchExpression.Arms.Single(a => a.Expression == armExpression); var armIndex = switchExpression.Arms.IndexOf(arm); return armIndex == 0 ? switchExpression.Arms[1].Expression : switchExpression.Arms[armIndex - 1].Expression; }, cancellationToken); } private static bool IsSwitchOrConditionalCastSafeToRemove<TConditionalOrSwitchExpression>( ExpressionSyntax castNode, SemanticModel originalSemanticModel, ExpressionSyntax rewrittenExpression, SemanticModel rewrittenSemanticModel, Func<ExpressionSyntax, TConditionalOrSwitchExpression?> getConditionalOrSwitchExpression, Func<TConditionalOrSwitchExpression, ImmutableArray<ExpressionSyntax>> getArmExpressions, Func<TConditionalOrSwitchExpression, ExpressionSyntax, ExpressionSyntax?> getAlternativeArm, CancellationToken cancellationToken) where TConditionalOrSwitchExpression : ExpressionSyntax { if (castNode is not CastExpressionSyntax castExpression) return false; var parentExpression = castExpression.WalkUpParentheses(); var originalConditionalOrSwitchExpression = getConditionalOrSwitchExpression(parentExpression); var rewrittenConditionalOrSwitchExpression = getConditionalOrSwitchExpression(rewrittenExpression.WalkUpParentheses()); if (originalConditionalOrSwitchExpression is null || rewrittenConditionalOrSwitchExpression is null) return false; if (originalSemanticModel.GetOperation(castExpression, cancellationToken) is not IConversionOperation conversionOperation) return false; return IsConditionalCastSafeToRemoveDueToConversionOfEntireConditionalExpression() || IsConditionalCastSafeToRemoveDueToConversionToOtherBranch(); // Returns true if we have `x ? (T)y : z` and (T) can be removed because the outer expression is being converted // to a (T) and that is pushed through to the branches. bool IsConditionalCastSafeToRemoveDueToConversionOfEntireConditionalExpression() { var originalConversion = conversionOperation.GetConversion(); if (originalConversion is { IsNullable: false, IsNumeric: false }) 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`. foreach (var armExpression in getArmExpressions(originalConditionalOrSwitchExpression)) { if (armExpression.WalkDownParentheses().IsKind(SyntaxKind.DefaultLiteralExpression)) return false; } } var originalCastExpressionTypeInfo = originalSemanticModel.GetTypeInfo(castExpression, cancellationToken); var originalConditionalTypeInfo = originalSemanticModel.GetTypeInfo(originalConditionalOrSwitchExpression, cancellationToken); var rewrittenConditionalTypeInfo = rewrittenSemanticModel.GetTypeInfo(rewrittenConditionalOrSwitchExpression, 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; var rewrittenOperation = rewrittenSemanticModel.GetOperation(rewrittenConditionalOrSwitchExpression, cancellationToken); if (rewrittenOperation is not IConditionalOperation and not ISwitchExpressionOperation) return false; if (castType.Equals(rewrittenOperation.Type, SymbolEqualityComparer.IncludeNullability)) return true; if (rewrittenOperation.Parent is IConversionOperation conditionalParentConversion && conditionalParentConversion.GetConversion().IsImplicit && castType.Equals(conditionalParentConversion.Type, SymbolEqualityComparer.IncludeNullability)) { return true; } return false; } // Returns true if we have `x ? (T)y : z` and (T) can be removed because the 'y' type is the same as the 'z' type, and // both are converted to 'T' outside of the conditional. bool IsConditionalCastSafeToRemoveDueToConversionToOtherBranch() { // Always keep a cast of 'default'. This can end up taking on incorrect values if it uses the type of the other branch // (for example, between `(int?)default` vs `(int)default`). if (castExpression.Expression.WalkDownParentheses().IsKind(SyntaxKind.DefaultLiteralExpression)) return false; var otherSide = getAlternativeArm(originalConditionalOrSwitchExpression, parentExpression); if (otherSide is null) return false; var otherSideType = originalSemanticModel.GetTypeInfo(otherSide, cancellationToken).Type; var thisSideRewrittenType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).Type; if (otherSideType is null || thisSideRewrittenType is null) return false; // Check if 'y' has the same type as 'z'. if (!otherSideType.Equals(thisSideRewrittenType, SymbolEqualityComparer.IncludeNullability)) return false; // Now check that with the (T) cast removed, that the outer `x ? y : z` is still // immediately implicitly converted to a 'T'. If so, we can remove this inner (T) cast. var rewrittenConditionalConvertedType = rewrittenSemanticModel.GetTypeInfo(rewrittenConditionalOrSwitchExpression, cancellationToken).ConvertedType; if (rewrittenConditionalConvertedType is null) return false; var outerConversion = rewrittenSemanticModel.GetConversion(rewrittenConditionalOrSwitchExpression, cancellationToken); if (!outerConversion.IsImplicit) return false; if (!rewrittenConditionalConvertedType.Equals(conversionOperation.Type)) return false; // There's one case where this is still not safe. If we started with `x ? (Span<...>)inline_array : ...` and now // have `x ? inline_array : ...` then this is not safe to remove. This is because the outer Span conversion is // is not legal when dealing with a 'value' (vs variable) as per: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#conversions // // ``` // A new conversion, an inline array conversion, from expression will be added. The inline array conversion is a standard conversion. // // There is an implicit conversion from expression of an inline array type to the following types: System.Span<T>, System.ReadOnlySpan<T> // // However, converting a readonly variable to System.Span<T> or converting *a value* to either type is an error. // ``` if (conversionOperation.Type.IsSpanOrReadOnlySpan() && conversionOperation.GetConversion().IsInlineArray) { return false; } return true; } } private static bool IsNullOrErrorType(TypeInfo info) => IsNullOrErrorType(info.Type) || IsNullOrErrorType(info.ConvertedType); private static bool IsNullOrErrorType([NotNullWhen(false)] ITypeSymbol? type) => type is null or IErrorTypeSymbol; private static bool CastRemovalWouldCauseUnintendedReferenceComparisonWarning( ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) { // Translated from DiagnosticPass.CheckRelationals if (expression.WalkUpParentheses().Parent is BinaryExpressionSyntax(SyntaxKind.EqualsExpression or SyntaxKind.NotEqualsExpression) parentBinary) { 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 HaveSameTypeArgumentNullability(ITypeSymbol? type1, ITypeSymbol? type2) { if (type1 is null || type2 is null) return true; // If the types match including nullability, there's no difference at all. if (type1.Equals(type2, SymbolEqualityComparer.IncludeNullability)) return true; // If the types don't match ignoring nullability, they're fundamentally different types. if (!type1.Equals(type2)) return true; // The types are the same but differ in nullability. Check if the difference is in type arguments // (not just top-level nullability) by normalizing top-level nullability and comparing. var normalized1 = type1.WithNullableAnnotation(NullableAnnotation.NotAnnotated); var normalized2 = type2.WithNullableAnnotation(NullableAnnotation.NotAnnotated); return normalized1.Equals(normalized2, SymbolEqualityComparer.IncludeNullability); } private static bool IsConstantNull(IOperation operation) => operation.ConstantValue is { HasValue: true, Value: 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 is { Exists: true, IsBoxing: true }) 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 { MethodKind: 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); } }