Пользовательские аннотации кода для PVS-Studio

от автора

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

Зачем вообще нужна ручная аннотация кода?

Вопрос из заголовка, на самом деле, очень хорош. Действительно, зачем анализатору какие-то подсказки, если он и так видит весь исходный код? И вы абсолютно правы. Мы придерживаемся такого же мнения на этот счёт — если анализатор может что-то сделать сам, то не нужно беспокоить этим пользователя. Именно поэтому анализатор уже знает про наиболее часто используемые библиотеки и паттерны. Увы, далеко не всегда можно достоверно автоматически вывести какие-либо факты о коде.

Прим. автора: у нас в TODO, конечно же, есть задачи на решение проблемы остановки, анализ комментариев в коде о костылях малых архитектурных решениях, а также на чтение мыслей разработчиков. По понятным причинам, прогресс по ним движется крайне медленно.

В реальности приходится идти на различные компромиссы с целью ускорения и оптимизации анализа. Ручное аннотирование — самый простой и надёжный способ «познакомить» анализатор с вашим кодом.

Например, чтобы по-честному решить один из наиболее частых запросов о корректной работе с nullable-классами (к ним же можно отнести различные классы-обёртки, std::optional и т.п.), анализатору необходимо вывести следующие факты:

  • класс хранит какой-то ресурс или ссылку на него;

  • для доступа к ресурсу нужна обязательная проверка состояния обёртки.

Проблема дополнительно усугубляется, если:

  • есть функции, которые могут менять состояние обёртки;

  • есть различные варианты инициализации;

  • для проверки используются нестандартные функции (что-то кроме operator bool, вариаций IsValid и прочего);

  • есть функции, которые можно вызывать и без проверки.

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

Как раз для этого в PVS-Studio, начиная с версии 7.31 (для C и C++) и 7.32 (для C#), появилась возможность ручной аннотации функций и классов. Реализована она с помощью отдельных JSON-файлов.

Вы можете спросить, а зачем было делать систему аннотаций в отдельных файлах, да ещё и в JSON-формате, когда можно было сделать их прямо в коде? Ведь всегда удобнее держать код и его аннотации рядом друг с другом. Согласны, но на это, как и всегда, есть причины:

  1. Многие клиенты используют в своей работе сторонние библиотеки и компоненты, код которых они не могут изменять.

  2. Нередко бывает так, что команды внутри компании хотят использовать разные наборы аннотаций для одного и того же кода;

  3. Минимальные аннотации в коде у нас уже есть.

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

Смотрим в деле

Уже сейчас новая система пользовательских аннотаций позволяет задать:

Для типов:

  • сходство со стандартными классами (например, если класс имеет интерфейс, схожий с одним из стандартных контейнеров);

  • семантику (cheap-to-copy, copy-on-write и т.д.);

  • прочие свойства (nullable-тип).

Для функций:

  • Свойства функции:

    • не возвращает управление;

    • объявлена как устаревшая;

    • чистая ли она;

    • должен ли использоваться её результат и т.п.

  • Свойства каждого из параметров функции:

    • должен отличаться от другого параметра;

    • nullable-объект должен быть валидным;

    • является источником или стоком taint-данных и т.п.

  • Ограничения для каждого из параметров:

    • можно запретить или разрешить передачу определённых целочисленных значений.

  • Свойства возвращаемых значений:

    • taint-данные;

    • nullable-объект будет валидным и т.п.

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

Допустим, у вас есть примерно такой код:

constexpr struct MyNullopt { /* .... */ } my_nullopt;  template <typename T> class MyOptional { public:   MyOptional();   MyOptional(MyNullopt);    template <typename U>   MyOptional(U &&val);  public:   bool HasValue() const;    T& Value();   const T& Value() const;  private:   /* implementation */ }; 

Несколько уточнений по коду:

  • Конструктор по умолчанию и конструктор от типа MyNullopt инициализируют объект в состоянии «невалидный».

  • Шаблон конструктора, принимающий параметр типа U&&, инициализирует объект в состоянии «валидный».

  • Функция-член HasValue проверяет состояние объекта. Если объект в состоянии «валидный», то возвращается true, в обратном случае — false. Функция не меняет состояние объекта.

  • Функции-члены Value возвращают нижележащий объект и не меняют состояние объекта.

С учетом условий выше, мы можем составить следующую аннотацию:

{   "version": 2,   "annotations": [     {       "type": "class",       "name": "MyOptional",       "attributes": [ "nullable" ],       "members": [         {           "type": "ctor",           "attributes": [ "nullable_uninitialized" ]         },         {           "type": "ctor",           "attributes": [ "nullable_uninitialized" ],           "params": [             {               "type": "MyNullopt"             }           ]         },         {           "type": "ctor",           "template_params": [ "typename U" ],           "attributes": [ "nullable_initialized" ],           "params": [             {               "type": "U &&val"             }           ]         },         {           "type": "function",           "name": "HasValue",           "attributes": [ "nullable_checker", "pure", "nodiscard" ]         },         {           "type": "function",           "name": "Value",           "attributes": [ "nullable_getter", "nodiscard" ]         }       ]     }   ] } 

И теперь анализатор предупредит нас об опасном использовании нашего nullable-типа:

Упрощения для комфортной работы

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

Готовые примеры

Для облегчения знакомства с механизмом пользовательских аннотаций мы подготовили коллекцию примеров для наиболее часто встречающихся сценариев. Например, разметка функций форматного вывода (С++), пометка функций как опасных/устаревших (С++) и т.д. Ознакомиться с ними можно в соответствующем разделе документации.

JSON-схемы

Для каждого доступного языка мы сделали JSON-схемы с поддержкой версионирования. Благодаря этим схемам современные текстовые редакторы и IDE могут проводить валидацию, также подсказывать возможные значения и показывать подсказки прямо во время редактирования.

Для этого при составлении собственного файла аннотаций необходимо добавить в него поле $schema, в котором следует указать схему для необходимого языка. Например, для С++ анализатора поле будет выглядеть так:

{     "version": 2,     "$schema": "https://files.pvs-studio.com/media/custom_annotations/v2/cpp-annotations.schema.json",     "annotations": [         { .... }      ] } 

В таком случае, тот же Visual Studio Code сможет помогать вам при составлении аннотаций:

Актуальный список доступных языков для аннотаций и схемы для них опубликованы в соответствующем разделе документации.

Предупреждения анализатора

Далеко не все проблемы можно диагностировать на уровне валидации JSON-схемы. Поэтому мы создали диагностическое правило V019, которое подскажет, если что-то пойдёт не так. Например: отсутствие файлов аннотаций, ошибка парсинга, пропуск аннотаций из-за ошибок в них и т.д.

Удобство составления

Мы постарались сделать систему аннотаций так, чтобы вам приходилось писать как можно меньше. Примером таких упрощений может являться выбор перегрузок функций в С++.

Если вы хотите, чтобы аннотация применилась сразу на все функции с этим именем, то можно просто опустить поле params при описании функции.

// Code void foo();      // dangerous void foo(int);   // dangerous void foo(float); // dangerous  // Annotation {   ....   "type": "function",   "name": "foo",   "attributes": [ "dangerous" ]   .... } 

Если же требуется аннотация именно на функцию без параметров, то нужно явно указать это:

// Code void foo();      // dangerous void foo(int);   // ok void foo(float); // ok  // Annotation {   ....   "type": "function",   "name": "foo",   "attributes": [ "dangerous" ],   "params": []   .... } 

Гибкость

В продолжение прошлого пункта можно также отметить возможность использовать символы подстановки (wildcard). Благодаря им, например, можно не указывать какие-либо параметры функций, если они не имеют смысла для аннотации. Уже сейчас доступны:

  • «*» (звёздочка) — заменяет 0 или более параметров любого типа;

  • «?» (знак вопроса) — заменяет один параметр любого типа.

Рассмотрим, как это можно применить, например, для разметки функций форматированного вывода. Допустим, у нас имеется набор функций для вывода текста:

namespace Foo {   void LogAtExit(const     char *fmt, ...);   void LogAtExit(const  char8_t *fmt, ...);   void LogAtExit(const  wchar_t *fmt, ...);   void LogAtExit(const char16_t *fmt, ...);   void LogAtExit(const char32_t *fmt, ...); } 

В этом случае не нужно делать аннотацию на каждую функцию. Достаточно написать одну, а изменяющийся тип первого параметра заменить на символ подстановки:

{   "version": 1,   "annotations": [     {       "type": "function",       "name": "Foo::LogAtExit",       "attributes": [ "noreturn" ],       "params": [         {           "type": "?",           "attributes" : [ "format_arg", "not_null", "immutable" ]         },         {           "type": "...",           "attributes": [ "immutable" ]         }       ]     }   ] } 

Заключение

Мы уверены, что наш новый механизм пользовательских аннотаций значительно упростит жизнь разработчикам и повысит точность статического анализа кода. Приглашаем всех попробовать этот функционал в действии и убедиться в его эффективности. Для этого достаточно просто скачать анализатор по ссылке. Как говорится, лучше один раз попробовать, чем сто раз услышать 🙂

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Mikhail Gelvikh. User annotations for PVS-Studio.


ссылка на оригинал статьи https://habr.com/ru/articles/848250/