Pull to refresh

ReSharper: Анализ на NullReferenceException и контракты для него

.NET *
Если вы используете ReSharper, то вы, наверняка, знакомы с его подсветкой «Possible 'NullReferenceException'». В этой статье я кратко расскажу об анализаторе, который выводит предупреждения такого рода, и о том, как ему помочь делать это лучше.

Сразу рассмотрим пример:

public string Bar(bool condition)
{
  string iAmNullSometimes = condition ? "Not null value" : null;
  return iAmNullSometimes.ToUpper();
}


* This source code was highlighted with Source Code Highlighter.

ReSharper справедливо подсветит iAmNullSometimes во второй строке метода с таким предупреждением. Теперь выделим метод:

public string Bar(bool condition)
{
  string iAmNullSometimes = GetNullWhenFalse(condition);
  return iAmNullSometimes.ToUpper();
}

public string GetNullWhenFalse(bool condition)
{
  return condition ? "Not null value" : null;
}


* This source code was highlighted with Source Code Highlighter.

После этой операции предупреждение пропадает. Почему так происходит?


Анализатор


Анализатор пытается выявить, какие значения могут иметь используемые переменные. Уточню, до какого уровня абстракции сокращено знание о значении переменной. С точки зрения анализатора переменная может иметь одно или несколько состояний:

* NULL, NOT_NULL — обозначает, что ссылка имеет нулевое или ненулевое значение;
* TRUE, FALSE — аналогично для типа bool;
* UNKNOWN — значение, введенное для оптимистичного анализа, с помощью которого снижается количество ложных срабатываний.

В результате работы анализатора для каждой точки использования переменных определяется набор возможных их состояний.

В первом листинге iAmNullSometimes после инициализации будет иметь два возможных состояния: NULL и NOT_NULL. Поэтому подсветка «Possible NullReferenceException» говорит нам, что есть хотя бы один путь выполнения программы, в котором iAmNullSometimes будет иметь значение null (в данном случае путь, в котором condition ложно).

Второй случай сложнее. Анализатор не знает, какие значения возвращает GetNullWhenFalse. Конечно, можно его проанализировать и убедиться, что он может вернуть null. Но при увеличении числа методов, которые тоже что-то вызывают, время, затрачиваемое на такой анализ, не позволяет использовать анализатор «на лету» на современных PC (чтобы ReSharper смог установить подсветки возможных ошибок). Тем более, вызываемый метод может оказаться в библиотеке, на которую ссылается наш проект. Не будем же мы ее так же «на лету» ее декомпилировать и анализировать.

Есть еще один вариант. Предполагать, что внешние методы, о которых ничего не известно, возвращают либо NULL, либо NOT_NULL. Так работает пессимистичный анализ.

В ReSharper по-умолчанию используется оптимистичный анализ. В нем, если о методе ничего не известно, то возвращаемое значение будет в специальном состоянии UNKNOWN. Переменная, оказавшаяся в этом состоянии к моменту ее использования, не подсвечивается (если, конечно, нет других путей на которых null ей был присвоен явно или из CanBeNull-метода). Во втором листинге это и заставляет анализатор «потерять бдительность».

Анализатор и его режимы работы требуют отдельной статьи, поэтому о них я напишу отдельно.

Как в случае оптимистичного, так и пессимистичного анализа все-таки хочется как-то знать, на что способен вызываемый метод, чтобы ReSharper находил больше потенциальных ошибок. Тут нам на помощь приходят контракты.

Контракты


Анализатор ReSharper'a может использовать дополнительные знания о вызываемых методах, получая его через контракты вида «метод никогда не возвращает null», «метод может вернуть null», «в параметр нельзя подставить null». В простейшем случае эти контракты задаются с помощью атрибутов JetBrains.Annotations.CanBeNullAttribute и JetBrains.Annotations.NotNullAttribute. Применение атрибута к методу будет говорить о том, может ли он возвращать null. К параметру — о допустимости подстановки нулевого значения. Также их можно применять к свойствам и полям. Эти атрибуты определены в библиотеке JetBrains.Annotations.dll, которая лежит в <ReSharper install directory>\Bin.

Пример, приведенный во втором листинге, можно улучшить, пометив метод GetNullWhenFalse атрибутом CanBeNull:

public string Bar(bool condition)
{
  string iAmNullSometimes = GetNullWhenFalse(condition);
  return iAmNullSometimes.ToUpper();
}

[CanBeNull]
public string GetNullWhenFalse(bool condition)
{
  return condition ? "Not null value" : null;
}


* This source code was highlighted with Source Code Highlighter.

При использовании метода переменной iAmNullSometimes в таком случае появляется подсветка «Possible 'NullReferenceException'».

Если вам не хочется в своем проекте тянуть за собой дополнительную сборку, которая к тому же не добавляет функциональности в рантайме, то вы можете объявить эти атрибуты прямо в своем проекте. Анализатору подойдет использование любых атрибутов из любых сборок, лишь бы их имена совпадали с теми, которые указаны в JetBrains.Annotations.dll. Определения этих атрибутов можно легко получить с помощью кнопки Copy default implementation to clipboard, расположенной на одной из страниц настроек ReSharper'a:



External Annotations


Если вам хочется использовать внешнюю библиотеку (например mscorlib.dll), прописывание контрактов для ее сущностей с помощью атрибутов не представляется возможным. Тут на помощь приходят External Annotations. Эта фича ReSharper позволяет дополнять уже скомпилированные сущности атрибутами используемыми анализатором ReSharper'a. External Annotations дают возможность «обмануть» анализатор — сделать так, чтобы он видел у методов, параметров и других объявлений атрибуты, которые не были объявлены при компиляции библиотеки. Для этого атрибуты нужно прописать в XML-файле, расположенном в директории <ReSharper install directory>\Bin\ExternalAnnotations.

Так определены контракты для стандартных библиотек, которые попадают в эту папку при установке ReSharper. Эти контракты были выведены в результате анализа исходных кодов и Microsoft Contracts. Контракты, полученные в результате первого подхода расположены в файлах с именами *.Generated.xml, в результате второго — в *.Contracts.xml.

Файлы, описывающие дополнительные атрибуты, имеют структуру, похожую на структуру XmlDoc-файлов. Например, для метода XmlReader.Create(Stream input) из сборки System.Xml четвертого фреймворка контракты NotNull задаются так:

<assembly name="System.Xml, Version=4.0.0.0"> <!-- В атрибуте name указывается имя сборки, если не указывать версию, то атрибуты из этого файла применятся ко всем версиям сборки с указанным именем -->
 <member name="M:System.Xml.XmlReader.Create(System.IO.Stream)"> <!-- Здесь указано имя члена, атрибуты которого дополняются; используется нотация такая же, как в XmlDoc-файлах -->
  <attribute ctor="M:JetBrains.Annotations.NotNullAttribute.#ctor" /> <!-- Имена конструкторов атрибутов тоже указываются в XmlDoc-нотации -->
  <parameter name="input">
   <attribute ctor="M:JetBrains.Annotations.NotNullAttribute.#ctor" />
  </parameter>
 </member>
</assembly>


* This source code was highlighted with Source Code Highlighter.

Чтобы ReSharper подхватил файл, его нужно разместить одним из следующих способов: <ReSharper install directory>\Bin\ExternalAnnotations\<Assembly name>.xml или <ReSharper install directory>\Bin\ExternalAnnotations\<Assembly name&gt\<Any name>.xml, где <Assembly name&gt — имя сборки без указания версии. Если располагать файлы вторым способом, то для одной сборки можно указать несколько наборов контрактов. Это может быть необходимо для различия контрактов сборок с разными версиями.

Сейчас редактирование этих файлов не очень удобно и подразумевает много ручной работы, для которой к тому же необходимы права администратора. Но в скором будущем планируется выход в свет инструмента, упрощающего эту работу. Скорее всего он будет оформлен в виде плагина к ReSharper'у.

Применение


Несколько практик применения External Annotations, улучшающих жизнь при работе с ReSharper'ом.

XmlDocument.SelectNodes(string xpath)


Аннотация CanBeNull для этого метода довольно часто является темой для баг-репортов. Дело в том, что SelectNodes является методом класса XmlNode и в общем случае может вернуть null (например для XmlDeclaration). Но чаще всего мы используем этот метод, когда он никогда не возвращает null, — из XmlDocument. Одним из решений может быть удаление соответствующей аннотации из External Annotations или замена ее на NotNull. Но можно поступить и корректнее, написав extension method для 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
  }
}


* This source code was highlighted with Source Code Highlighter.

В данном случае, конечно, было бы хорошо еще сделать методы вида SelectElements и SelectAttributes, чтобы избежать преобразования типов каждый раз, но это уже другая история.

Assertion


Если вы используете в своем проекте собственные (или сторонних производителей) методы Assert, то их можно пометить атрибутами AssertionMethodAttribute и AssertionConditionAttribute. Прямые кандидаты на такую пометку — это методы Contracts.Assert, если вы используете 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>


* This source code was highlighted with Source Code Highlighter.

А еще можно посмотреть в сторону TerminatesProgramAttribute, если у вас есть методы, которые всегда бросают исключение.

И на последок


В одну статью всего не уместить. Я планирую написать об анализаторе и его использовании еще несколько статей. В каком русле пойдет мой рассказ, зависит от того, что будет интересно хабрасообществу: оптимистичный и пессимистичный анализаторы, как программно получить аннотации из исходников или еще что-нибудь.

И да. Аннотируйте ваш код контрактами и будет вам добро!
Tags:
Hubs:
Total votes 53: ↑39 and ↓14 +25
Views 4.7K
Comments Comments 22