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

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

![1166_annotations_ru/image1.png](https://import.viva64.com/docx/blog/1166_annotations_ru/image1.png)

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

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


> \_Прим\\\. автора: у нас в TODO, конечно же, есть задачи на решение \[проблемы остановки\]\(https://ru\.wikipedia\.org/wiki/Проблема\_остановки\), анализ комментариев в коде о \~\~костылях\~\~ малых архитектурных решениях, а также на чтение мыслей разработчиков\\\. По понятным причинам, прогресс по ним движется крайне медленно\\\.\_

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

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

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

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

* есть функции, которые могут менять состояние обёртки;
* есть различные варианты инициализации;
* для проверки используются нестандартные функции \(что\-то кроме _operator bool_, вариаций _IsValid_ и прочего\);
* есть функции, которые можно вызывать и без проверки\.

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

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

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

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

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

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

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

**Для типов:**

* сходство со стандартными классами \(например, если класс имеет интерфейс, схожий с одним из стандартных контейнеров\);
* семантику \(cheap\-to\-copy, copy\-on\-write и т\.д\.\);
* прочие свойства \(_nullable_\-тип\)\.

**Для функций:**

* Свойства функции:
  * не возвращает управление;
  * объявлена как устаревшая;
  * чистая ли она;
  * должен ли использоваться её результат и т\.п\.
* Свойства каждого из параметров функции:
  * должен отличаться от другого параметра;
  * _nullable_\-объект должен быть валидным;
  * является источником или стоком taint\-данных и т\.п\.
* Ограничения для каждого из параметров:
  * можно запретить или разрешить передачу определённых целочисленных значений\.
* Свойства возвращаемых значений:
  * taint\-данные;
  * _nullable_\-объект будет валидным и т\.п\.

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

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

```cpp
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_ возвращают нижележащий объект и не меняют состояние объекта\.

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

```cpp
{
  "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_\-типа: 

![1166_annotations_ru/image2.png](https://import.viva64.com/docx/blog/1166_annotations_ru/image2.png)

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

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

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

Для облегчения знакомства с механизмом пользовательских аннотаций мы подготовили коллекцию примеров для наиболее часто встречающихся сценариев\. Например, разметка функций форматного вывода \(С\+\+\), пометка функций как опасных/устаревших \(С\+\+\) и т\.д\. Ознакомиться с ними можно в [соответствующем разделе](https://pvs-studio.ru/ru/docs/manual/6810/#examples) документации\.

### JSON\-схемы

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

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


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

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

![1166_annotations_ru/image3.png](https://import.viva64.com/docx/blog/1166_annotations_ru/image3.png)

Актуальный список доступных языков для аннотаций и схемы для них опубликованы в соответствующем разделе [документации](https://pvs-studio.ru/ru/docs/manual/6810/#json_schemas)\.

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

Далеко не все проблемы можно диагностировать на уровне валидации JSON\-схемы\. Поэтому мы создали диагностическое правило [V019](https://pvs-studio.ru/ru/docs/warnings/v019/), которое подскажет, если что\-то пойдёт не так\. Например: отсутствие файлов аннотаций, ошибка парсинга, пропуск аннотаций из\-за ошибок в них и т\.д\.

![1166_annotations_ru/image4.png](https://import.viva64.com/docx/blog/1166_annotations_ru/image4.png)

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

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

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

```cpp
// Code
void foo();      // dangerous
void foo(int);   // dangerous
void foo(float); // dangerous

// Annotation
{
  ....
  "type": "function",
  "name": "foo",
  "attributes": [ "dangerous" ]
  ....
}
```

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

```cpp
// Code
void foo();      // dangerous
void foo(int);   // ok
void foo(float); // ok

// Annotation
{
  ....
  "type": "function",
  "name": "foo",
  "attributes": [ "dangerous" ],
  "params": []
  ....
}
```

### Гибкость

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

* "\*" \(звёздочка\) — заменяет 0 или более параметров любого типа;
* "?" \(знак вопроса\) — заменяет один параметр любого типа\.

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

```cpp
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, ...);
}
```

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

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

## Заключение

Мы уверены, что наш новый механизм пользовательских аннотаций значительно упростит жизнь разработчикам и повысит точность статического анализа кода\. Приглашаем всех попробовать этот функционал в действии и убедиться в его эффективности\. Для этого достаточно просто скачать анализатор [по ссылке](https://pvs-studio.ru/ru/pvs-studio/download/?promo=custom_annotations)\. Как говорится, лучше один раз попробовать, чем сто раз услышать :\)