Skip to content

BRO1133: Check for null in one form

Null checks are written one way in the whole codebase: x is null and x is not null (the default), or x == null and x != null. Not a StyleCop rule.

Example

Before

public class Cache
{
    private object entry;

    public bool IsEmpty => entry == null;

    public object Get(object fallback)
    {
        if (entry != null)
        {
            return entry;
        }

        return fallback;
    }
}

After

public class Cache
{
    private object entry;

    public bool IsEmpty => entry is null;

    public object Get(object fallback)
    {
        if (entry is not null)
        {
            return entry;
        }

        return fallback;
    }
}

Why

Both forms are common, and a codebase that mixes them makes the reader wonder whether the difference matters (it does when the type defines its own ==, which this rule leaves alone). The default is the form that never calls a user-defined ==, so a null check means the same whatever the type; == null is common too (most of the reference repositories StyleBro is tested on use it), so it's an option. See decisions.md.

How the fix works

Only the operator changes: == becomes is, != becomes is not, and back. With pattern_matching (the default), null == x becomes x is null (there is no null is x); with equality_operator it is left to BRO1103, which puts the constant on the right.

Configuration

[*.cs]
# pattern_matching (default): x is null, x is not null
# equality_operator: x == null, x != null
stylebro_null_check_style = pattern_matching

Not reported

Checks whose meaning or validity would change:

  • A user-defined == on the operand's type (a class with operator ==, records): x is null never calls it, so the result could differ. The core library's operators (string, Type, delegates) are null-safe and are converted, and so are nullable value types (int?, S? with a lifted operator): comparing them with null only reads HasValue.
  • g is null on a nullable value type whose == comes from the type (Guid?, a struct with operator ==): g == null only reads HasValue, but it names the type's operator, which a banned-API list may forbid (Jellyfin bans Guid's ==). int? and other built-in types are converted.
  • dynamic operands: their == is chosen at run time.
  • Inside expression trees (Expression<Func<...>> lambdas, query clauses over IQueryable): patterns don't compile there.
  • x is not null before C# 9 and x is null before C# 7 (the project's LangVersion). A file compiled more than once (several target frameworks, or linked into another project) is fixed only when its LOWEST version allows it: a multi-targeted project's net472 build uses C# 7.3 unless LangVersion is set (LibGit2Sharp's tests stopped compiling before). The warning stays in the copies with a newer version.
  • Operands that aren't null-able: non-nullable value types (i == null for an int, already a compiler warning), type parameters constrained to struct. Unconstrained type parameters are converted: both forms compile and mean the same.
  • Other comparisons: x == default, x == default(T), patterns other than a plain null (is not (null), is null or "", is { }).
  • Where the new form would bind differently: x == null with an operand containing an operator that binds looser than is (a + b == null, o as T == null), and x is null as an operand of ==, <, + and the like (b == o is null would become (b == o) == null).
  • A comment or line break between is and not, and a comment between null and the operator in null == x.

Compared with StyleCop

StyleCop has no such rule. Roslynator's RCS1248 does the same, off by default and with its own option (roslynator_null_check_style). The .NET SDK has no rule for it: IDE0041 only rewrites ReferenceEquals(x, null), IDE0150 x is object, IDE0083 !(x is null), and IDE0078 x == null || x == "", all into the pattern form (checked with dotnet format style, SDK 10.0.401: none of them changes a plain x == null). They point the same way as the default; with equality_operator, turn them off, or BRO1133 turns their output back on the next run. stylebro-migrate turns this rule off, so migrating doesn't change a StyleCop-clean repository.