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

Михаил Гельвих
Статей: 12

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

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

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

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

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

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

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

  • класс хранит какой-то ресурс или ссылку на него;
  • для доступа к ресурсу нужна обязательная проверка состояния обёртки.

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

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

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

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

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

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

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

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

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

Для типов:

  • сходство со стандартными классами (например, если класс имеет интерфейс, схожий с одним из стандартных контейнеров);
  • семантику (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" ]
        }
      ]
    }
  ]
}

Заключение

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