CA2020: Prevent behavioral change caused by built-in operators of IntPtr/UIntPtr
Property | Value |
---|---|
Rule ID | CA2020 |
Title | Prevent behavioral change caused by built-in operators of IntPtr/UIntPtr |
Category | Reliability |
Fix is breaking or non-breaking | Non-breaking |
Enabled by default in .NET 9 | As suggestion |
Cause
This rule fires when it detects a behavioral change between .NET 6 and .NET 7 introduced by the new built-in operators of IntPtr and UIntPtr.
Rule description
With the numeric IntPtr feature, IntPtr and UIntPtr gained built-in operators for conversions, unary operations, and binary operations. These operators might throw when overflowing within checked context or may not throw in unchecked context compared to the previous user-defined operators in .NET 6 and earlier versions. You might encounter this behavioral change when upgrading to .NET 7.
List of APIs affected
Operator | Context | In .NET 7 | In .NET 6 and earlier | Example |
---|---|---|---|---|
operator +(IntPtr, int) | checked | Throws when overflows | Doesn't throw when overflows | checked(intPtrVariable + 2); |
operator -(IntPtr, int) | checked | Throws when overflows | Doesn't throw when overflows | checked(intPtrVariable - 2); |
explicit operator IntPtr(long) | unchecked | Doesn't throw when overflows | Can throw in 32-bit contexts | (IntPtr)longVariable; |
explicit operator void*(IntPtr) | checked | throws when overflows | Doesn't throw when overflows | checked((void*)intPtrVariable); |
explicit operator IntPtr(void*) | checked | throws when overflows | Doesn't throw when overflows | checked((IntPtr)voidPtrVariable); |
explicit operator int(IntPtr) | unchecked | Doesn't throw when overflows | Can throw in 64-bit contexts | (int)intPtrVariable; |
operator +(UIntPtr, int) | checked | Throws when overflows | Doesn't throw when overflows | checked(uintPtrVariable + 2); |
operator -(UIntPtr, int) | checked | Throws when overflows | Doesn't throw when overflows | checked(uintPtrVariable - 2); |
explicit operator UIntPtr(ulong) | unchecked | Doesn't throw when overflows | Can throw in 32-bit contexts | (UIntPtr)uLongVariable |
explicit operator uint(UIntPtr) | unchecked | Doesn't throw when overflows | Can throw in 64-bit contexts | (uint)uintPtrVariable |
How to fix violations
Examine your code to determine if the flagged expression could cause a behavioral change, and choose an appropriate way to fix the diagnostic from the following options:
Fix options:
- If the expression would not cause a behavioral change:
- If the
IntPtr
orUIntPtr
type is used as a nativeint
oruint
, change the type tonint
ornuint
. - If the
IntPtr
orUIntPtr
type is used as a native pointer, change the type to the corresponding native pointer type. - If you can't change the type of the variable, suppress the warning.
- If the
- If the expression could cause a behavioral change, wrap it with a
checked
orunchecked
statement to preserve the previous behavior.
Example
Violation:
using System;
public unsafe class IntPtrTest
{
IntPtr intPtrVariable;
long longVariable;
void Test ()
{
checked
{
IntPtr result = intPtrVariable + 2; // Warns: Starting with .NET 7 the operator '+' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior.
result = intPtrVariable - 2; // Starting with .NET 7 the operator '-' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior.
void* voidPtrVariable = (void*)intPtrVariable; // Starting with .NET 7 the explicit conversion '(void*)IntPtr' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior.
result = (IntPtr)voidPtrVariable; // Starting with .NET 7 the explicit conversion '(IntPtr)void*' will throw when overflowing in a checked context. Wrap the expression with an 'unchecked' statement to restore the .NET 6 behavior.
}
intPtrVariable = (IntPtr)longVariable; // Starting with .NET 7 the explicit conversion '(IntPtr)Int64' will not throw when overflowing in an unchecked context. Wrap the expression with a 'checked' statement to restore the .NET 6 behavior.
int a = (int)intPtrVariable; // Starting with .NET 7 the explicit conversion '(Int32)IntPtr' will not throw when overflowing in an unchecked context. Wrap the expression with a 'checked' statement to restore the .NET 6 behavior.
}
}
Fix:
- If the expression would not cause a behavioral change and the
IntPtr
orUIntPtr
type is used as a nativeint
oruint
, change the type tonint
ornuint
.
using System;
public unsafe class IntPtrTest
{
nint intPtrVariable; // type changed to nint
long longVariable;
void Test ()
{
checked
{
nint result = intPtrVariable + 2; // no warning
result = intPtrVariable - 2;
void* voidPtrVariable = (void*)intPtrVariable;
result = (nint)voidPtrVariable;
}
intPtrVariable = (nint)longVariable;
int a = (int)intPtrVariable;
}
}
- If the expression could cause a behavioral change, wrap it with a
checked
orunchecked
statement to preserve the previous behavior.
using System;
public unsafe class IntPtrTest
{
IntPtr intPtrVariable;
long longVariable;
void Test ()
{
checked
{
IntPtr result = unchecked(intPtrVariable + 2); // wrap with unchecked
result = unchecked(intPtrVariable - 2);
void* voidPtrVariable = unchecked((void*)intPtrVariable);
result = unchecked((IntPtr)voidPtrVariable);
}
intPtrVariable = checked((IntPtr)longVariable); // wrap with checked
int a = checked((int)intPtrVariable);
}
}
When to suppress warnings
If the expression would not cause a behavioral change, it's safe to suppress a warning from this rule.