Pull to refresh

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

Reading time7 min
Views5K

Если вы используете 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
Comments22

Articles