No Modulus Direct Equality
ID |
csharp.no_modulus_direct_equality |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
arithmetic, reliability, suspicious-comparison |
Description
Reports an == or != comparison between a % expression and a non-zero integer literal, in
either operand order and including a negated literal. Comparisons against 0 are not reported,
and neither is a dividend that cannot be negative: an unsigned integral type, a cast to one, or a
collection size.
Rationale
In C# the remainder operator keeps the sign of the dividend, so -3 % 2 is -1 and not 1.
The familiar odd-number test x % 2 == 1 therefore answers "even" for every negative odd value,
and x % 2 == -1 has the mirror-image blind spot for positive ones. Because most test data is
positive, the defect survives review and unit tests and only shows up on real input. Comparing
against zero is immune to the sign rule, which is why it is the dependable form.
A dividend that cannot be negative has a remainder that cannot be negative either, so the
comparison is correct and is not reported. Three shapes qualify: a variable of an unsigned integral
type (uint, ulong, ushort, byte), a cast to one of those, and a collection size — the
Length, LongLength, Count and Capacity properties, and the Count() and LongCount()
methods. Parity and padding tests on s.Length are ordinary code and matching those member names
is deliberate. When the dividend is anything else (a method call, an element access, an unresolved
field), the comparison is still reported rather than assumed safe.
public bool IsOddBroken(int x)
{
return x % 2 == 1; // FLAW — false for -3, -5, -7, ...
}
public bool IsOddUnsigned(uint x)
{
return x % 2 == 1; // OK — an unsigned dividend has no negative remainder
}
public bool IsOddLength(string s)
{
return s.Length % 2 == 1; // OK — a length is never negative
}
public bool IsOdd(int x)
{
return x % 2 != 0; // OK — zero is unaffected by the sign of the dividend
}
public bool EveryThird(int x)
{
return Math.Abs(x % 3) == 1; // OK — the remainder is normalised before comparing
}
Remediation
Rewrite the test against zero: x % 2 != 0 for "odd", x % n == 0 for "divisible by n". When a
specific non-zero remainder really is the question, normalise first — Math.Abs(x % n) == r, or
((x % n) + n) % n == r to get the mathematical (always non-negative) remainder. If the reported
dividend can in fact never be negative but its type could not be resolved here, making the type
explicit (or asserting the range) both silences the finding and documents the assumption.