Improved Analysis and Hints for Nullable Reference Types

Andrey Dyatlov

If you are already using nullable reference types, you might have noticed that they can help make your code safer. But not automagically… This feature relies heavily on everything called by your code being annotated.

Unannotated APIs are a big problem for nullable reference types, as they never produce warnings. Values from such calls show extremely optimistic non-null hints, even though there are no guarantees that these hints are correct. Here’s a compiler issue for this problem.

For example, if you are using LINQ’s FirstOrDefault() method from .NET Core 3.1, the compiler not only allows you to dereference its result without any warnings, it also tells you that it is not nullable – something we all know might not be true:

Incorrect nullability analysis for FirstOrDefault

The framework API is being annotated right now and the above won’t be a problem if you can use the .NET 5 preview, or migrate to it as soon as it reaches RTM.

However, there are lots of unannotated libraries and large projects that cannot be annotated in a timely manner. They represent a big problem for the nullable type system and the safety net it provides.

Some of these codebases might already have nullability information available in the form of JetBrains.Annotations, but these, unfortunately, cannot be utilized by the compiler.

Luckily, ReSharper (and Rider) warnings don’t have such limitations, so now both will take these into account as well when nullable reference types are enabled. Of course, it will also use external annotations to fix the FirstOrDefault example above even when it isn’t annotated, e.g. in .NET Core 3.1:

Fix incorrect nullability analysis for FirstOrDefault with JetBrains Annotations

You can also make use of ReSharper’s pessimistic analysis mode (Code Inspection | Settings | Value analysis mode in the ReSharper options) to tell it that you want a bit of extra safety with unannotated APIs, in which case it will tell you to check any value that wasn’t explicitly declared safe to use (i.e. any values that don’t have any annotation at all). Use this with caution, however, as it comes with lots of warnings!

Pessimistic code analysis

Showing these warnings has been made possible by extending the allowed states of variables in the analysis with new values representing a value without proper annotations, e.g. coming from a code without #nullable context. Such a value might be annotated with either [NotNull] or [CanBeNull] attributes, or its nullability might be completely unknown.

This implementation has some notable implications. First, since these additional states are part of the nullable analysis, they are tracked at no additional performance cost and do not require a separate analysis pass over the code.

Second, these states are fully integrated in the analysis and take full advantage of the language type system, for example they are utilized by type inference:

Nullability type inference

Finally, as unannotated and non-nullable states are now decoupled, we can tell you when some values are truly not nullable without the risk of misinforming you due to unannotated calls.

For example, code analysis can now tell you if a null check or conditional access is redundant:

Elvis operator (conditional null check) is not required

Or it can tell you that a method never returns null values and so its contract can be stricter. The same is true for local variables where ReSharper and Rider can tell you when a nullable variable is never used as such, and can be declared as non-nullable.

In the following example, ReSharper recognizes that the content variable will never hold a null value (even though it’s declared as nullable), and that the ReadAllText() method can not return null. As a result, nullable annotations can be safely removed for both the variable and the method’s return type:

Nullable annotation can be removed

This opens up an interesting opportunity to improve type hints. As you may already know, implicitly typed variables are now nullable by default, which means they can be reassigned with nullable values regardless of whether they were originally initialized with a nullable or non-nullable value. You can check the following language design notes explaining the motivation behind this change.

Calling attention to this fact via type hints turned out to be confusing for lots of people. They could initialize a variable with a non-nullable value and use it safely without ever reassigning it, yet as the variable was technically nullable and displayed a nullable type hint, it was perceived as not safe to use.

Now, ReSharper and Rider recognize variables that are always safe to use and don’t ever hold any nullable or even unknown values. Starting from 2020.2, ReSharper and Rider will work their magic and provide you with this information by identifying such variables as non-nullable through type hints:

Conclusion

In this post, we have seen that APIs that do not yet use nullable reference types may lead to incorrect code analysis, thus compromising null safety. ReSharper and Rider recognize such APIs and uses JetBrains.Annotations, if available, to ensure null safety.

ReSharper can also be switched to pessimistic analysis mode for extra safety when working with APIs that do not have JetBrains.Annotations. As misleading information from unannotated APIs is no longer a problem for ReSharper, it can now provide redundancy inspections for null checks and nullable annotations, making it possible to tighten nullability contracts. Type hints now indicate whether a variable ever contains a null value, making it clear whether you can always use it safely.

What do you think? Download ReSharper 2020.2 or Rider 2020.2, and give it a try!

Subscribe

Subscribe to .NET Tools updates