.NET Tools How-To's

What’s New for C# Nullable Reference Types in ReSharper and Rider 2021.2 EAP?

Nullable reference types are one of the biggest and most actively developed C# features. Since their release less than two years ago, a lot has changed. We’ve gotten new attributes such as [MemberNotNullWhen], annotations for generic types, new constraints syntax, and countless small changes in the data flow analysis and compiler warnings that don’t even make it into release notes. Within our .NET team here at JetBrains, we even had to establish a semi-automatic workflow verifying our analysis results against the compiler’s test data before releases.

With the language design finally stabilizing and less changes being made these days, we had the time to catch up on building some long-awaited features, and addressed some pitfalls that are unlikely to be handled by the C# language itself. In this blog post we’ll introduce some of the most interesting changes that will be included in the upcoming 2021.2 release of ReSharper and Rider:

Let’s dive in.

Migration to nullable reference types

Nullable reference types are easy to use in new solutions, but adopting them in large existing codebases is a tough task. However, when your code already contains some form of nullable annotations provided by JetBrains.Annotations attributes, this becomes easier. With ReSharper/Rider 2021.2, you can transform these annotations to nullable reference types’ syntax with a single action, as soon as you enable nullable reference types for your codebase.

Of course, you can also migrate your code base project by project or file by file: ReSharper will utilize all available annotations regardless of whether you are using the new syntax or JetBrains.Annotations attributes. You can learn more about it in our previous blogpost.

Migration to nullable reference types

There’s one case where it might be impossible to express a nullability contract with syntax: generics.

Generics often confuse developers who don’t have a lot of experience with nullable reference types, because they can be substituted with other types that may have their own nullability. Consequently, even if you don’t annotate a generic type with a ?, you still can’t use it safely, because at runtime the actual type argument might be a nullable type – unless you add some compiler-known attributes.

Even annotating a generic type as nullable can be tricky and require new constraints to solve an ambiguity with Nullable<T> syntax. But don’t fret if you don’t know the difference between [NotNull] and [DisallowNull] or what exactly the where T : default constraint means! ReSharper and Rider are able to utilize all these tools to explain the nullability contracts of your code to the compiler.

Use compiler attributes

If you end up with unnecessary compiler attributes, or attributes that can be replaced with nullable reference types’ syntax, ReSharper will notice this and suggest a fix:

Use annotation syntax

Dealing with nullable warning suppressions

As you adopt nullable reference types, you may come across some compiler analysis shortcomings. Analysis may not be able to understand some complex contracts, notice a dependency between variables, or realize that a non-nullable value is assigned within a closure. In such cases, you can use the nullable warning suppression expression – ! – to tell the compiler not to nag you about this case.

As your code base evolves over time, you may find a lot of suppressions in your codebase with no way to check which of them are still required. Maybe you have already refactored code in a way that’s easier for the compiler to understand. Or maybe you have made nullability contracts more stringent.

Starting from the 2021.2 release, ReSharper and Rider will clearly indicate suppressions that are no longer needed, and can be safely removed to keep your code clean:

Remove redundant suppression

As developers, sometimes we can be too optimistic about some code and suppress an actual nullability problem, thinking it will never happen, or take a dependency on an implementation detail that has already changed. When this happens, it’s very hard to backtrack such suppressions, since ! is a very common symbol in source code. To help you, our tools now offer a new action to search for all nullable warning suppressions in a selected scope from any suppression.

Find nullable warning suppressions

For example, you can find out how many nullable warning suppressions are used in Roslyn. Turns out it’s 4200 total, almost half of them are located in source generated files.

Nullable warning suppressions in Roslyn

If you want to always see warning suppressions highlighted as you code, or enforce explicit assertions instead of suppressions, you can configure a new inspection: A suppressed nullable warning might hide an underlying problem. This inspection will not trigger by default, since suppressions are a completely valid way to prevent the compiler from nagging you about some complex code that it cannot analyze.

Refactorings

One final problem we start tackling in this release is code modifications. We are updating a lot of actions and refactorings to play nicely with nullable reference types, and take data flow analysis results into account. Most notably, we’ve introduced a new refactoring to propagate nullability changes to related elements, and made a few changes to the var to type context action.

When we update a nullability annotation somewhere in our project, it might result in lots of cascading warnings. For example, you might have a method that accepts a parameter, passes it to another method that stores it in a collection which is exposed via a property.

If you ever want to change this parameter’s annotation to allow null values or to forbid them, you’ll have to update all call chains using this value to get rid of possible warnings and propagate these changes. This sounds like a lot of work, but now you’ll be able to do this in a single refactoring called Change type. What’s even better, you don’t even need to remember there’s such refactoring because ReSharper and Rider will notice when you change an annotation, and offer to help right away!

Of course you can limit where to propagate the changes if you want to stop at some point and deal with the change yourself.

Change nullability refactoring

Implicitly typed variables (var) are considered nullable by the compiler to allow null assignments without warnings. If you are curious why it’s the case you can check relevant language design. However, it’s often a technical detail and it’s quite confusing when replacing var with an explicit type introduces a nullable variable even if the variable never actually holds null values. The action will now check whether nullability is observable, and try to produce a stricter type whenever possible.

Specify type explicitly

Conclusion

In this post, we have seen some of the most interesting changes that will be included in the upcoming 2021.2 release. Our tools make it easier for you to migrate to nullable reference types, and to switch between annotation syntax and attribute syntax. ReSharper and Rider help deal with nullable warning suppressions, making sure no nullable warnings are accidentally suppressed. And of course, there are several refactorings to change nullability or specify types explicitly when using var.

If you haven’t yet, download the latest ReSharper EAP or Rider EAP, and give these improvements a try!

image description