6 сентября 2010 в 22:13

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>\<Any name>.xml, где <Assembly name> — имя сборки без указания версии. Если располагать файлы вторым способом, то для одной сборки можно указать несколько наборов контрактов. Это может быть необходимо для различия контрактов сборок с разными версиями.

Сейчас редактирование этих файлов не очень удобно и подразумевает много ручной работы, для которой к тому же необходимы права администратора. Но в скором будущем планируется выход в свет инструмента, упрощающего эту работу. Скорее всего он будет оформлен в виде плагина к 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, если у вас есть методы, которые всегда бросают исключение.

И на последок


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

И да. Аннотируйте ваш код контрактами и будет вам добро!
+25
837
13

Комментарии (22)

0
sdvn, #
Если использовать данную аннотацию, то для компиляции кода не понадобится ли установленный ReSharper?
0
lair, #
Нет, не понадобится, аттрибуты для аннотации должны быть описаны в проекте.
0
sdvn, #
Хорошо. Тогда надо попробовать.
+3
lair, #
На самом деле, у решарпера больше аннотаций, нежели здесь указано. Очень, просто непомерно, полезны аннотации, которые указывают на методы форматирования строк — решарпер тогда считает параметры.
0
sdvn, #
Уже жду ваших следующих статей на данную тему, если они будут. =-)
0
lair, #
Вы меня с автором статьи путаете, наверное.

А я так, прочитал код аннотаций, который отдает решарпер.
0
sdvn, #
Каюсь, путаю.
Все пора спать, пора вылазить из визлы и идти спать. =\
0
alexeykuptsov, #
Я немного промахнулся, отвечая на ваш вопрос. =)
+4
alexeykuptsov, #
Аннотации в XML-файлах нужны только анализатору ReSharper'а. На сборку проекта они никак не влияют. Будет ли у Вас код проаннотирован контрактами или нет, Вы получите сборки, совершенно одинаковые по своей функциональности в рантайме.

То есть, вы можете спокойно скопировать JetBrains.Annotations.dll в папку с проектом/решением или написать реализацию атрибутов в своем проекте. А дальше — MSBuild, NAnt или еще что угодно. Им не нужно знать об аннотациях. =)

Надеюсь, я правильно понял Ваш вопрос.
0
centur, #
А вот такой вопрос — можно ли запустить решарпер в режиме глубокого анализа? Т.е. то что описывается, но чтобы он вплоть до фреймворка разбирал и проверял что оттуда возвращается\кидается и выдавал hint'ы.
Т.е. я например на билдовой машине (или на ночь на свое) запускаю анализ, ессесно машина грузится по полной, работать на ней нельзя, но утром следующего дня получаю подробный список возможных лакун и мест, где стоит «впилить» дополнительные проверки или хотя бы сделать fallback'и в случае неисправимых ситуаций.
0
alexeykuptsov, #
Нет, в текущей реализации алгоритма это сделать невозможно. Но планируется плагин, который даст возможность делать примерно то, что вы описали — запускать на ночь анализ, который расставляет атрибуты NotNull/CanBeNull в проекте автоматически.
0
centur, #
Автоматическая расстановка не так интересна, как просто лог «потенциальных проблем», ведь в лог входят не только NullReference но и куча других исключений которые могут выкидывать методы фреймворка.

NullReference это конечно тоже хорошо, хотя я лично против того что что-то будет расставлять аттрибуты в коде — если надо — в Xml файле и во временный каталог решарпера — не у всех он стоит и засорять код — не хочется.
0
alexeykuptsov, #
Никто не будет расставлять атрибуты без вашего ведома. Это ваше право использовать или не использовать атрибуты Решарпера. Но, если вам захочется проаннотировать ими уже имеющийся проект, то почему бы не сделать это автоматически в тех местах, где машина может их вывести. Именно для этой цели разрабатывается упомянутый мной плагин.

Можете описать подробнее, какой лог потенциальных проблем вы хотите получить?
+1
centur, #
Отображение в виде подсказок информации о том, какие исключения могут вылететь из вызова метода — например при работе с файлам — нет проверки что есть права доступа, что файл не залочен…

Вот например — msdn.microsoft.com/en-us/library/b9skfh7s.aspx File.Open() может выкинуть 9 разных задокументированных исключений.
Хотелось бы следующего ( не уверен вообще реализуемо ли, но «хотелка» она такая вот) — в логе видеть данные:
метод File.Open, перечень исключений которые он выкидывает, и напротив каждого — состояние — вылетит ли оно выше или оно обрабатывается как-либо (условия входа в ветку проверяют на определенные кондишены). Т.е. если перед вызовом — стоит проверка на существование файла — напротив DirectoryNotFoundException и FileNotFoundException — пометка — не вылетит. А вот напротив UnauthorizedAccessException — пометка что вылететь может, т.к. нет проверки прав на доступ к файлу.
Что-то типа такого…

Понятно что разбор этого всего дела — будет тяжел и утомителен, но при ревью кода можно принимать во внимание рекомендации решарпера или хотя бы обращать более пристальное внимание на то, что он говорит.

По сути — некий анализ выполнения программы по разным сценариям. Что-то типа такого Microsoft делает в Pex, насколько я понимаю, только там они сразу генерят тесты с «хитрыми» значениями, а тут уведомления\лог получить хочется (в человеко-машино-читаемом формате типа Xml — вообще супер).
0
alexeykuptsov, #
Получается продвинутая спецификация исключений, как в Java. =)

В сценарии, описанном вами, придется использовать очень много информации о вызываемом коде. В том числе нужно ввести пометки о том, что методы не меняют состояния объекта между проверками и местами, где может быть выброшено исключение в случае отсутствия этих проверок. Средствами Решарпера нельзя провести настолько глубокий анализ.

Да и такие сценарии весьма конкретны и сравнительно редки. Не то что подстановка в ненулевой параметр метода переменной, которая может внезапно оказаться null'ом.

0
centur, #
=) «хотелка» она всегда большая… Ну хотя бы информацию о явно выкидываемых (задокументированных для фреймворка и явных throw для своего ) методом исключениях можно показывать? Чтобы не лазить каждый раз в мсдн и глубины кода.?
0
alexeykuptsov, #
Чтобы не лазить в msdn, есть QuickDoc (Ctrl+Shift+F1 в студийной раскладке, Ctrl+Q в идейной)
0
centur, #
O.o круто, CtrlQ не работает (схема решарпера для студии а не «идейная»). А вот первая — очень правильная оказывается. Спасибо.
0
kernel_panic, #
Подсветка «Possible 'NullReferenceException'» при использовании метода переменной iAmNullSometimes в таком случае не появляется.

Наоборот? Атрибут [CanBeNull] был добавлен ради подсветки.
0
alexeykuptsov, #
Да, вы правы. Оговорился.
0
efix, #
Алексей, скажите, а как лучше аннотировать такой метод?

void Guard.IsNotNull(object value, string name);

По идее надо как assertion, так как он ведет себя аналогично Contract.Assert, но это не подходит, потому что нет условия как такового.
0
alexeykuptsov, #
[AssertionMethod]
void Guard.IsNotNull([AssertionCondition(AssertionConditionType.IS_NOT_NULL)] object value, string name);

К счастью, IsNull/IsNotNull параметры в assertion методах поддерживаются.

Только зарегистрированные пользователи могут оставлять комментарии.
Войдите, пожалуйста.