Автор: Sergey Teplyakov

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

ErrorProne.NET унаследовал многие возможности из моего другого проекта ExceptionAnalyzer, но в несколько измененном виде. Вот какие правила он содержит:

  • ERP021 Incorrect exception propagation: неправильный проброс исключения с помощью throw ex, вместо throw,
  • ERP022 Unobserved exception in generic exception handler: когда блок catch, который перехватывает все исключения возвращает управления без попытки наблюдения перехваченного исключения.
  • ERP023 Suspicious exception handling: only Message property was observed: когда в обобщенном блоке catch идет обращение лишь к свойству ex.Message исключения.

Теперь обо всех этих исключениях более подробно.

Incorrect exception propagation

На этой проблеме нет смысла останавливаться слишком долго. Если вы проходили хоть одно собеседование в своей жизни или работали в команде из более чем пары человек, то наверняка знаете, в чем разница между throw ex, и throw,:try { throw new Exception(), }
catch (Exception
ex)
{
throw ex,
// ~~
// Incorrect exception propagation. Use throw, instead
}

Тут никаких сюрпризов. throw ex, сломает стек вызовов оригинального исключения в результате чего последующий анализ логов и понимание того, что же пошло не так, будет затруднительным. Поскольку эта известная проблема, то подобные ошибки встречаются на практике довольно редко. Но несмотря на относительную это, я нашел один пример сомнительного проброса исключений в коде Розлина: CompilationWithAnalyzer.cs:602.

Unobserved exception in generic exception handler

Поскольку идея этой тулы в поиске максимально подозрительных мест, большинство правил, связанных с обработкой исключений являются простыми, конкретными и не слишком назойливыми. Так, например, как бы ни хотелось пуристам запретить все блоки catch(Exception) в их коде, на практике сделать это будет довольно сложно ввиду их распространенности. Но, ИМХО, можно и нужно предупреждать, если такие блоки возвращают управления даже не удосужившись проверить, что же за исключение они поймали.

Поэтому, следующий код приводит к выдаче предупреждения:try { throw new Exception(), }
catch (Exception
e)
{
if (e is AggregateException) return,
// ~~~~~~~
// Exit point 'return,' swallows an exception.
throw,
}

Любое обращение к переменной e в блоке catch (типа e.Message, ProcessExcpetion(e), etc) деактивирует это правило. Я почему-то считал, что срабатывать это правило должно относительно нечасто, и был крайне удивлен, что на коде Розлина, оно срабатывает более 60 (!) раз. Вот лишь несколько примеров, для затравки.

  • AssemblyIdentityUtils.cs:40
  • PdbWriter.cs:796
  • AssemblyIdentity.DisplayName.cs:842
  • AnalyzerFileReference.cs:158
  • AbstractAnalyzerAssemblyLoader.cs:102

Да, это же правило будет срабатывать только на блоки catch {}, catch(System.Exception) и catch(System.AggregateException). Для более специфических исключений, вполне нормально возвращать управление без обращения к объекту исключения.

Suspicious exception handling: only Message property was observed

Что может быть хуже проглатывания исключений? Записывание не всей информации об исключении! Каждый раз, когда вы пишите лишь ex.Message в лог файл, всякие непонятные высокие силы делают так, чтобы ваш код грохнулся в продакшне с TypeLoadException и вас подняли в 2 часа ночи для того, чтобы выяснить, что же пошло не так с чудесно проверенной на локальном компе билдом. Чтобы вы с просоня смотрели в лог и долго думали над волшебными записями в логе типа: Exception has been thrown by the target of an invocation., The type initializer for 'MyAwesomeType' threw an exception. или One or more errors occurred. и гадали, А что же, блин, произошло!.

Можно, конечно, сказать, что код мы ревьюим внимательно, что наши статические конструкторы исключений не бросают, TPL мы не признаем, да и вообще мы пишем код с первого раза и без ошибок. На это я обычно привожу пример, который показывает, что исключения заворачиваются даже там, где большинство людей этого не ожидают:class WillThrow
{
public WillThrow()
{
throw new Exception('Oops!'),
}
}
public static T Create<,T>,() where T : new
()
{
return new T(),
}
public void
Sample()
{
try { Create<,WillThrow>,(), }
// Warn for ex.Message: Only ex.Message was observed in the exception block!
catch (Exception
ex)
{
// ex.Message:
// Exception has been thrown by the target of an invocation.
Console.WriteLine(ex.
Message),
// ~~~~~~~~~~
// Only ex.Message property was observed in exception block!
}
}

Поскольку ограничение new() приводит к использованию Activator.CreateInstance, то любое исключение в конструкторе объекта, создаваемого с помощью обобщенных методов, типа Create<,T>,, приведет к обарачиванию исходного исключения в TargetInvocationExcpetion. Такое легко пропустить во время ревью, но гораздо проще заворачивать любой код, который пишет лишь сообщение исключения, и не выводит стек-трейса + внутренних искючений.

И снова я был удивлен, количеством нарушений этого правила в коде Roslyn-а. Более десятка нарушений. Многие из которых, явные опечатки, типа вот этого, когда при генерации нового исключения, явно забыли передать старое исключение в качестве вложенного.

Ссылки

  • ErrorProne.NET on GitHub
  • ErrorProne.NET. Часть 1

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

You have no rights to post comments

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

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