Автор: Sergey Teplyakov

Уже давно хотел поразбираться с анализаторами на основе Розлина. Тем более, что меня уже был опыт создания плагинов для Resharper-а (R# Contract Editor Extension), поэтому хотелось сравнить разные инфраструктуры и удобство использования. Есть идея переписать этот плагин с помощью анализаторов Roslyn-а, но я решил начать с чего-то попроще.

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

  • Повторная генерация исключений с помощью throw ex,
  • Проглатывание всех исключений с помощью пустых блоков catch {} или catch(Exception) {}.
  • Проглатывание исключений в определенных ветках блока catch.
  • Сохранение в логгах только сообщения ex.Message, теряя при этом потенциально важную информацию о месте возникновения исключения.
  • Некорректное пробрасывание новых исключений из блока catch.

Начало работы

Для начала разработки анализатора необходимо установить VS2015 CTP (проще всего взять готовую вирталку), после этого нужно поставить VS 2015 SDK, .NET Compiler Platform SDK Templates и .NET Compiler Platform Syntax Visualizer. Визуализатор будет незаменим для понимания того, как выглядят синтаксические деревья, как их правильно анализировать и как генерировать новые деревья в фиксах. Самое главное, нужно установить правильные версии инструментов (для CTP6 все VSIX пакеты должны быть для CTP6). На главной странице проекта Roslyn всегда будут находиться актуальная инструкция по установке.

Команда разработчиков проделала огромную работу, чтобы создание анализаторов было максимально простым и удобным. Достаточно создать новый проект, выбрать шаблон Extensibility -&gt, Diagnostics with Code Fix (Nuget + VSIX), вбить имя анализатора и все. В результате, будет создано три проекта: сам анализатор, проект с юнит-тестами и проект с установщиком (VSIX). По умолчанию, в проект добавлен пример анализатора, который показывает предупреждение на имена типов с lowercase буквами.

После чего, можно запускать тесты, или же выбрать VSIX-проект в качестве стартового, и нажать F5. Тогда будет запущен еще один экземпляр Visual Studio с установелнным анализатором, который будет выдавать предупреждение на все типы с буквами в нижнем регистре:

clip_image002

Способы установки аналазиторов

Существует три способа установки анализатора:

С помощью VSIX пакета, который может быть скачан с Visual Studio Gallary напрямую, или через Extensions and Updates.

Вручную установить анализатор для каждого проекта:clip_image003

Также анализаторы можно распространять через NuGet вместе с библиотекой, или просто установить через Managed NuGet Packages:clip_image005При этом ссылка на пакет будет закомичена в source control, что позволит всем участникам проекта использовать один набор анализаторов.

Тестируемость

При разработке плагина для ReSharper-а мне больше всего не хватало простых юнит-тестов. Команда JetBrains разработала серьезную инфраструктуру тестирования, но все тесты являются интеграционными. В R# не существует абстрактных тестов, все они относятся к одной из категорий: тестирование доступности контекстного действия, тестирование результата фикса и т.п. При этом тесту обязательно нужно подсунуть cs-файл с кодом, по которому будет прогоняться анализатор и еще один файл для сравнения результатов. Проверить свою бизнес-логику в изоляции невозможно!

В Розлине пошли по более простому пути. Анализаторы работают с синтаксическим и семантическим деревьями (Syntax Tree и Semantic Tree), которые легко создать в тестах из файла или из строки. В результате, в тесте можно проверять сам анализатор, а можно и проверять свою бизнес модель, передавая ей фрагменты синтаксического дерева:[TestMethod]public void SimpleTestWarningOnEmptyBlockThatCatchesException(){var test = @'using System,namespace ConsoleApplication1{ class TypeName { public static void Foo() { try { Console.WriteLine(), } {on}catch(System.Exception) {}
}
}
}'
,
var warningPosition = test.IndexOf('{on}'),
var diagnostic = GetSortedDiagnostics(test.Replace('{on}', '')).Single(),
Assert.AreEqual(EmptyCatchBlockAnalyzer.DiagnosticId, diagnostic.Id),
Assert.AreEqual(''catch(System.Exception)' block is empty.
Do you really know what the app state is?'
,
diagnostic.GetMessage()),
Assert.AreEqual(warningPosition, diagnostic.Location.SourceSpan.Start),
}

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

Полученные возможности

Итак, что может полученный анализатор? На данный момент он поддерживает пять основных правил:

image

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

1. Empty catch block considered harmful!

Самая серьезный code smell при работе с исключениями это полное подавление всех исключений с пустым блоком catch или catch(Exception):

GenericCatch

2. Swallow exceptions considered harmful

Особое место на котле должно быть приготовлено тем, кто перехватывает все исключения с помощью блока catch{}. Фикс в этом случае очень простой: добавление throw,

clip_image007

3. Catch block swallows an exception

Еще один анализатор, который предупреждает о подавлении исключений. Catch-блок может быть не пустым, при этом он все равно может проглатывать исключения. В этом случае довольно тяжело придумать правильный фикс, особенно, когда подавление исключения происходит лишь в одной из веток блока catch:

clip_image009

Да, попытка сделать подобный анализатор без Розлина привела бы к месяцам работы и все равно, он был бы кривым в доску. В Розлине есть встроенная поддержка анализа потока исполнения (control flow analysis), на основе которого сделать этот анализатор было не сложно.

4. Rethrow exception properly

Это одна из самых распространенных ошибок, когда проброс исключения осуществляется с помощью throw ex,, а не с помощью throw. На всякий случай, напомню, что в первом случае стек-трейс исходного исключения будет утерян и будет впечатление, что блок catch является источником.

ThrowExFix

5. Tracing ex.Message considered harmful

Еще одна типовая ошибка обработки исключений, когда в консоль или лог сохраняется только ex.Message и ex.StackTrace. Поскольку исключения очень часто формируют дерево исключений, то сообщение верхнего уровня может вообще не содержать ничего полезного!

Данный анализ выдает предупреждение, если catch-блок использует (наблюдает) свойство Message, но не интересуется подробностями внутренностей исключения.

ExObserver[5]

Именно из-за такой прекрасной модели трассировки исключений мне пришлось выходить на работу первого января и заливать хот-фикс на прод, который бы дал понять, а что же творится с системой. Никогда не логируйте только лишь ex.Message! НИКОГДА!

6. Add catched exception as inner exception

Блок catch может бросать исключение, но даже в этом случае информация об исходном исключении может быть потеряна. Чтобы избежать этого, новое исключение должно содержать исходное исключение в качестве вложенного.

Данный анализатор проверяет код генерации нового исключение и если исходное исключение не используется, то предлагает добавить его в качестве вложенного (естественно, для этого у генерируемого исключения должен быть конструктор, который принимает пару параметров string и Exception):

ThrowNewException

Вместо заключения

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

Дополнительные ссылки
  • Exception Analyzer on github
  • Exception Analyzer on Nuget.org (доступен через Add NuGet Package, только в качестве фида нужно использовать nuget.org, а не api.nuget.org)
  • Exception Analyzer at Visual Studio Gallery

Пара вводных статей в MSDN Magazine о создании анализаторов:

  • Use Roslyn to Write a Live Code Analyzer for Your API
  • Adding a Code Fix to Your Roslyn Analyzer

З.Ы. Если у вас есть пожелания к анализатору исключений, то свистите, я с удовольствием их добавлю.

З.Ы.Ы. Насколько будет интересным цикл постов про разработку анализаторов?

 
Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).