.NET Tools
Essential productivity kit for .NET and game developers
ReSharper NullReferenceException Analysis and Its Contracts
If you use ReSharper, you are likely familiar with its Possible ‘NullReferenceException’ inspection. I want to talk about the analysis engine that displays this type of warnings and how you can, with just a bit of work, use it for best results.
Consider this sample code:
public string Bar(bool condition)
{
string iAmNullSometimes = condition ? "Not null value" : null;
return iAmNullSometimes.ToUpper();
}
ReSharper will reasonably highlight iAmNullSometimes
in the second line of the method with the ‘NullReferenceException’ warning. Now let’s extract the method:
public string Bar(bool condition)
{
string iAmNullSometimes = GetNullWhenFalse(condition);
return iAmNullSometimes.ToUpper();
}public string GetNullWhenFalse(bool condition)
{
return condition ? "Not null value" : null;
}
The warning no longer pops up. Why?
The Analysis Engine
The analysis engine tries to determine what values variables could have. Let’s see what level of abstraction its knowledge of variable values is reduced to. From its point of view, a variable can be in one or more of the following states:
- NULL, NOT_NULL — either null or non-null value.
- TRUE, FALSE — the same for bool type.
- UNKNOWN — this value is introduced for optimistic analysis, which is used to minimize the risk of incorrect analysis.
In the end, the analysis engine determines a set of possible states for a variable’s every use point.
In the first code sample above, after iAmNullSometimes
is initialized it will have two possible states: NULL or NOT_NULL. Therefore, the Possible NullReferenceException highlighting tells us that there exists at least one execution path in which iAmNullSometimes
will be null (in this case, when condition is FALSE).
The second code sample is more complex. The analysis engine doesn’t know what kind of values GetNullWhenFalse
returns. Of course, we could analyze it and make sure it could also return a null. But this would increase the number of methods being analyzed (which also call something in their turn), and thus greatly increase analysis time. Unfortunately we cannot afford that kind of on-the-fly analysis on today’s PCs (to have ReSharper highlight all possible errors). Note also that the method being called could potentially belong to a library referenced in our project. It would be unwise to decompile and analyze that library on the fly too.
There is another possible option: to assume that the external methods, which we know nothing about, return either NULL or NOT_NULL. That’s how pessimistic analysis works.
By default, ReSharper uses optimistic analysis. In this kind of analysis, if we don’t know anything about a method, its return value will be in the special state called UNKNOWN. If a variable is in the UNKNOWN state at the time of use, it isn’t highlighted (unless of course there are other ways in which it can be assigned the null value, either explicitly or from a CanBeNull
type of method). That’s what makes the analysis engine ‘let its guard down’ in the second code sample.
Whether we are doing optimistic or pessimistic analysis, we want to somehow find out what the method being called is capable of doing, so that ReSharper can find more potential errors. This is where contracts come in.
Contracts
ReSharper analysis engine can use additional knowledge of the called methods, which it obtains through contracts such as “method never returns null,” “method can return null” or “method doesn’t take null parameters.” In the basic case these contracts are defined through the attributes JetBrains.Annotations.CanBeNullAttribute
and JetBrains.Annotations.NotNullAttribute
. Applying one of these to a method specifies whether it can return a null. Applying one to a parameter specifies whether it can be null. You can also apply these attributes to properties and fields. They are defined in the library JetBrains.Annotations.dll which is located in <ReSharper install directory>Bin.
Our second code sample above can be improved by marking GetNullWhenFalse
with the CanBeNull
attribute:
public string Bar(bool condition)
{
string iAmNullSometimes = GetNullWhenFalse(condition);
return iAmNullSometimes.ToUpper();
}[CanBeNull]
public string GetNullWhenFalse(bool condition)
{
return condition ? "Not null value" : null;
}
Now, when we use a method of iAmNullSometimes
, the Possible ‘NullReferenceException’ highlighting appears.
Unless you want to maintain an extra assembly with your project, which doesn’t do much for your application’s functionality during runtime, you can declare these attributes right in your project. But basically, the analysis engine will make use of any attributes from any assemblies, as long as their names match those in JetBrains.Annotations.dll. The definitions of these attributes can be easily accessed via the Copy default implementation to clipboard button located on this ReSharper Options page:
External Annotations
If you’re using an external library (e.g. mscorlib.dll), it doesn’t seem feasible to specify contracts for its entities using attributes. Enter External Annotations. This ReSharper feature allows you to complement the already compiled entities with attributes used by ReSharper’s analysis engine. External Annotations let you ‘cheat’ the engine, by making it see the attributes (for methods, parameters and other declarations) which weren’t declared at the time the library was compiled. To do this, the attributes must be specified in an XML file located in <ReSharper install directory>BinExternalAnnotations.
Such is the definition of contracts for standard libraries stored in this folder when ReSharper is installed. These contracts are obtained based on source code analysis as well as Microsoft Contracts. The contracts obtained using the former technique are stored in files of the type *.Generated.xml, while those obtained using the latter technique are stored in files of the type *.Contracts.xml.
The files that describe additional attributes have a structure similar to XmlDoc. For example, for the method XmlReader.Create(Stream input)
from the assembly System.Xml
of .NET Framework 4.0, NotNull contracts are defined as follows:
<assembly name="System.Xml, Version=4.0.0.0"> <!—The attribute name contains the assembly name. If you don’t specify the version, this file's attributes will be applied to all versions of the assemblies of that name -->
<member name="M:System.Xml.XmlReader.Create(System.IO.Stream)"> <!—This shows the name of the member whose attributes are complemented; the notation is the same as XmlDoc -->
<attribute ctor="M:JetBrains.Annotations.NotNullAttribute.#ctor" /> <!—attribute constructor names are also specified using XmlDoc notation -->
<parameter name="input">
<attribute ctor="M:JetBrains.Annotations.NotNullAttribute.#ctor" />
</parameter>
</member>
</assembly>
To make sure ReSharper recognizes the file, you should store it either as <ReSharper install directory>BinExternalAnnotations<Assembly name>.xml OR as <ReSharper install directory>BinExternalAnnotations<Assembly name><Any name>.xml, where <Assembly name> is the assembly name without the version. If you use the latter kind of location structure, you will be able to specify multiple sets of contracts for the same assembly. This may be useful if you need to use different contracts for different assembly versions.
Right now, editing these files isn’t as easy as we’d like — it involves a lot of manual work plus requires administrator-level privileges. But we’re planning to release a handy tool to simplify this pretty soon. Most likely it will come in the form of a ReSharper plug-in.
Use Cases
Here are some External Annotations use cases that will help you get more from ReSharper.
XmlDocument.SelectNodes(string xpath)
The CanBeNull annotation for this method gives rise to many bug reports. The thing is that SelectNodes
is a method of XmlNode
and can generally return a null (for example, for XmlDeclaration
). But most of the time we use this method in such a way that it cannot return a null — from XmlDocument
. One solution would be to delete this annotation from External Annotations or replace it with NotNull. But there is a better way: to write an extension method for XmlDocument
:
public static class XmlUtil
{
[NotNull]
public static XmlNodeList SelectNodesEx([NotNull] this XmlDocument xmlDocument, [NotNull] string xpath)
{
// ReSharper disable AssignNullToNotNullAttribute
return xmlDocument.SelectNodes(xpath);
// ReSharper restore AssignNullToNotNullAttribute
}
}
In this case it would also make sense to create additional methods such as SelectElements
and SelectAttributes
, to avoid typecasting every time, but that’s another story.
Assertion
If you use your own Assert methods or those from an external framework, they can be marked with the attributes AssertionMethodAttribute
or AssertionConditionAttribute
. Top of the list candidates for this would be Contracts.Assert
methods, as long as you use Microsoft Contracts:
<assembly name="Microsoft.Contracts">
<member name="M:System.Diagnostics.Contracts.Contract.Assert(System.Boolean)">
<attribute ctor="M:JetBrains.Annotations.AssertionMethodAttribute.#ctor"/>
<parameter name="condition">
<attribute ctor="M:JetBrains.Annotations.AssertionConditionAttribute.#ctor(JetBrains.Annotations.AssertionConditionType)">
<argument>0</argument>
</attribute>
</parameter>
</member>
<member name="M:System.Diagnostics.Contracts.Contract.Assert(System.Boolean,System.String)">
<attribute ctor="M:JetBrains.Annotations.AssertionMethodAttribute.#ctor"/>
<parameter name="condition">
<attribute ctor="M:JetBrains.Annotations.AssertionConditionAttribute.#ctor(JetBrains.Annotations.AssertionConditionType)">
<argument>0</argument>
</attribute>
</parameter>
</member>
</assembly>
If you have methods that always throw exceptions, you can also consider TerminatesProgramAttribute
.
Author: Alexey Kuptsov, ReSharper developer. Translated from original article (in Russian)
Source code in this post is highlighted with Source Code Highlighter.