четверг, 25 февраля 2016 г.

ErrorProne.NET. Часть 1

У меня уже давно чесались руки сделать анализатор, который поможет отлавливать разные ошибки, в той или иной степени, специфичные для платформы .NET. Многие подобные ошибки уже прекрасно отлавливает R#, но ведь всегда хочется чего-то своего. К тому же, анализаторы Розлина бесшовно интегрируются в билд процесс, могут использоваться по ночам (*), да и могут содержать рулы, специфичные для вашего продукта.

Итак, взяв за основу идеи из R# и из аналогичной библиотеки для Java от гугла под названием Error Prone, я взялся за работу. Ниже представлен первая часть результатов моей работы.

Вызов чистых методов

Отсутствие «наблюдение» результатов вызова чистого метода является одной из наиболее частых ошибок, которые возникают во время локального тестирования. Проблема в том, что просто читая код, очень сложно сказать заранее, является ли вызов someCollection.Union(anotherCollection) «чистым» и возвращает новую коллекцию, или же меняет исходную.

Вот примеры работы этого правила:

clip_image001

Это правило учитывает ряд известных типов из BCL, которые гарантированно являются неизменяемыми или содержат только «чистые» методы. Оно также учитывает атрибут PureAttribute, которым вы можете пометить любые методы. Я также добавил пару эвристик: все типы с приставкой Immutable считаются неизменяемыми, все методы с приставкой With, который возвращают тип первого аргумента, а также все методы расширения неизменяемых типов является чистыми. У правила бывают ложные срабатывания, но их не так и много, а пользы - достаточно.

К сожалению, пока не понятно, как использовать существующие аннотации чистоты из библиотеки Code Contracts и нужно подумать, как сделать решение более расширяемым. Но даже сейчас правило нашло десяток мелких ошибок в коде моего проекта (все они были в тестах и во второстепенном коде, но все же).

Создание объектов без сохранения значения

Частным случаем предыдущего правила, является правило, которое ищет вызовы конструкторов объектов, без использования полученного результата, вида new SomeObject();

К сожалению, выдавать предупреждения на любой стоящий особняком вызов new нельзя, поскольку люди часто делают страшные вещи, в виде каких-то операций с побочными эффектами в конструкторах. Но в некоторых случаях, можно точно сказать, что никаких побочных эффектов у конструктора нет. Это относится к вызовам конструкторов по умолчанию значимых типов, коллекций, примитивных объектов или неизменяемых типов.

clip_image002

Особняком стоит частный случай этого правила, которое выдает ошибку при создании объекта исключения:

clip_image003

Форматирование строк

Еще один популярный вид ошибок – это неправильные аргументы при вызове string.Format и подобных методов. Да, частота использования string.Format заметно упала при появлении интерполяции строк в C# 6.0, но легаси кода много, да и в других местах форматные строки используются часто.

ErrorProne.NET содержит три правила:

  • Неизвестные аргументы в строке формата
  • Лишние аргументы, которые не используются в строке формата
  • Невалидная строка формата

(Да, вторая проблема не проявляется во время исполнения и не ведет к генерации исключения FormatException, но, ИМХО, это может лишь скрыть ошибки, да и проявляться на практике может даже чаще, чем другие варианты. Именно это правило позволило найти явный баг в коде Ролзина. Вот тикет, кому интересно).

Неизвестные аргументы:

clip_image004

Лишние аргументы:

clip_image005

Невалидная форматная строка:

clip_image006

И есть еще отдельное правило, валидирующее паттерн регулярного выражения:

clip_image007

Обратите внимание, что правило учитывает атрибут JetBrains.Annotations.StringFormatMethodAttribute, который можно получить через NuGet, или же просто создать такой атрибут в вашем собственном коде (используется такая себе утиная типизация).

-----------------

(*) Да, я знаю о существовании ReSharper Command Line Tools

Заключение

Я прекрасно осознаю, что подобных инструментов на рынке довольно много; конкурировать с R#, или анализаторами от PVS-Studio сложно, да и цели такой нет. Просто задача эта довольно интересная и хочется собрать в одном месте ценные анализаторы, которую будут полезны здесь и сейчас в моих личных проектах, и проектах моих коллег.

Да, и здесь рассмотрена лишь часть правил, поддерживаемых ErrorProne.NET, так что ждите продолжение в следующей части;)

Ссылки

З. Ы. Если есть идеи рулов, которые могли бы существенно облегчить вам жизнь и сделать ваш код чище/проще/понятней – пишите, обсудим!

8 комментариев:

  1. Хотелось бы еще видеть этот анализатор в виде плагина к решарперу.

    ОтветитьУдалить
    Ответы
    1. К сожалению (а может к счастью) API у R# и Розлина очень сильно отличаются. Это значит, что нужно все реализовывать дважды.

      Удалить
  2. Спасибо, еще один анализатор в копилку, нужно больше анализаторов, хороших и разных!

    Вот пара других анализаторов для вдохновения: http://code-cracker.github.io , https://github.com/icsharpcode/RefactoringEssentials . Вряд ли копи-пастить анализаторы оттуда есть смысл, но может какие идеи производные появятся.

    А вот отсюда можно и скопипастить: https://github.com/DotNetAnalyzers Многие идеи очень хорошие, но слишком глючно реализованы, чтобы ими пользоваться, и авторы явно их допиливать не собираются.

    ОтветитьУдалить
    Ответы
    1. Максим, а можно подробнее по поводу интересных идей? А то как-то не очень хочется их все перелопачивать, так что если ты уже знаешь хорошие фичи оттуда, то дай мне о них знать. А я обещаю реализовать их по нормальному:))

      Удалить
    2. Окей. Я, правда, не знаю, будут ли в списке идей какие-то новые и оригинальные на фоне R#, т.к. им не пользуюсь. Упорядочу примерно по видящейся мне полезности:

      * Свойство получает значение самого себя в геттере. Подозреваю, что это частая ошибка, из-за привычки интелли-сенса предлагать свойство (имя с большой буквы) вместо поля (имя с маленькой буквы).
      * Событие используется без проверки на null или ?. оператора.
      * LINQ сложнее, чем необходимо. Например, .Where(predicate).First() следует заменить на .First(predicate).
      * Аргумент по-умолчанию есть в виртуальном методе, но отсутствует или отличается в переопределении.
      * Аргумент невиртуального метода вообще не используется в нем, аналогично с локальной переменной.
      * Строка строится в цикле с использованием конкатенации вместо билдера.
      * Локальную переменную можно пометить как const.
      * Проверку флага перечисления битовыми операциями лучше заменить на HasFlag
      * Связанные с IDisposable грабли.
      * Типичные грабли в async/await.

      Удалить
    3. В простых случаях можно легко и с пользой детектить ошибки выхода за границу массива. Скажем, если цикл идет до i < arr.Length, то в теле цикла явно не стоит обращаться к arr[i+1], а если цикл начинается с i = 0, то не должно быть arr[i-1], и так далее.

      Удалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить