Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
>
>
Аннотирование C и C++ кода в формате JS…
menu mobile close menu
Проверка проектов
Сообщения PVS-Studio
Диагностики общего назначения (General Analysis, C++)
Диагностики общего назначения (General Analysis, C#)
Диагностики общего назначения (General Analysis, Java)
Микрооптимизации (C++)
Диагностика 64-битных ошибок (Viva64, C++)
Реализовано по запросам пользователей (C++)
Cтандарт MISRA
Стандарт AUTOSAR
Стандарт OWASP (C++)
Стандарт OWASP (C#)
Проблемы при работе анализатора кода
Дополнительная информация
toggle menu Оглавление

Аннотирование C и C++ кода в формате JSON

11 Окт 2024

Механизм пользовательских аннотаций — это способ разметки типов и функций в формате JSON с целью дать анализатору дополнительную информацию. Благодаря этой информации анализатор сможет находить больше ошибок в коде. Механизм работает только для языков С и С++.

Быстрый старт

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

void DeprecatedFunction(); // should not be used

void foo()
{
  DeprecatedFunction();    // unwanted call site
}

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

{
  "version": 1,
  "annotations": [
    {
      "type": "function",
      "name": "DeprecatedFunction",
      "attributes": [ "dangerous" ]
    }
  ]
}

После этого достаточно подключить файл (все способы подключения рассмотрены здесь):

//V_PVS_ANNOTATIONS, language: cpp, path: %path/to/annotations.json%

void DeprecatedFunction();

void foo()
{
  DeprecatedFunction();    // <= V2016 will be issued here
}

Примечание. По умолчанию диагностическое правило V2016 отключена. Для выдачи предупреждений включите диагностику в настройках.

Способы подключения файла c аннотациями

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

Структура файла с аннотациями

Содержимое файла — JSON-объект, состоящий из двух обязательных полей: version и annotations.

Поле version принимает значение целого типа и задаёт версию механизма. В зависимости от значения файл с разметкой может обрабатываться по-разному. На данный момент поддерживается только одно значение — 1.

Поле annotations — массив объектов "аннотация":

{
  "version": 1,
  "annotations":
  [
    {
      ...
    },
    {
      ...
    }
  ]
}

Аннотации могут быть двух видов:

  • аннотации типов;
  • аннотации функций.

Если аннотация объявляется непосредственно в массиве annotations, то она считается аннотацией верхнего уровня. В ином случае считается, что это вложенная аннотация.

Аннотации типов

Объект аннотации типа состоит из следующих полей:

Поле "type"

Обязательное поле. Принимает строку с одним из значений: "record", "class", "struct", "union". Последние три варианта являются псевдонимами "record" и добавлены для удобства.

Поле "name"

Обязательное поле. Принимает строку с квалифицированным (полным) именем сущности. Анализатор будет искать эту сущность, начиная с глобальной области видимости. Если сущность находится в глобальной области, то "::" в начале имени может быть опущено.

Поле "members"

Опциональное поле. Массив объектов вложенных аннотаций.

Поле "attributes"

Опциональное поле. Массив строк, который задаёт свойства сущности. Для аннотаций типов доступны следующие атрибуты:

Умные указатели

Контейнеры

Другие типы

  • "nullable" — тип имеет семантику nullable-типа. Объекты таких типов могут находиться в одном из двух состояний: "валидный" или "невалидный". Доступ к объекту в состоянии "невалидный" приведёт к ошибке. Примерами таких типов являются указатели и std::optional.
  • "exception" – тип используется для создания объекта, который будет выброшен как исключение. Пример использования можно посмотреть здесь.

Семантика

  • "cheap_to_copy" — объект типа может передаваться в функцию по копии без накладных расходов;
  • "expensive_to_copy" — объект типа следует передавать в функцию только по указателю/ссылке;
  • "copy_on_write" — тип имеет семантику copy-on-write.

Аннотации функций

Объект аннотации функции состоит из следующих полей:

Поле "type"

Обязательное поле. Принимает строку со значением function. Для вложенных аннотаций функций (в поле members аннотаций типов) также становится доступным значение ctor. Оно показывает, что аннотируется конструктор пользовательского типа.

Поле "name"

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

Для аннотаций верхнего уровня указывается квалифицированное (полное) имя, для вложенных — неквалифицированное.

Если функция находится в глобальной области видимости, то '::' в начале имени может быть опущено.

Поле "params"

Опциональное поле. Массив объектов, описывающих формальные параметры. Данное поле совместно с name задаёт сигнатуру функции, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. В случае с функциями-членами анализатор также рассматривает поле qualifiers.

Каждый объект содержит следующие поля:

  • "type" (обязательное) — тип формального параметра в виде строки. Например, первый формальный параметр функции memset имеет тип void *. Его и следует записать в строку. Существует возможность пропустить не интересующие параметры и проаннотировать несколько перегрузок функции с помощью одной аннотации: для этого в типе можно написать символ подстановки:
    • Символ "*" означает, что на его месте может быть 0 или более параметров любого типа. Должен идти последним в списке параметров.
    • Символ "?" означает, что на его месте может быть параметр любого типа.
  • "attributes" (опциональное) — массив строк, который задаёт свойства параметра. Возможные атрибуты параметров описаны далее в документации.
  • "constraint" (опциональное) — объект, содержащий информацию об ограничениях параметра. Если анализатор находит возможное нарушение ограничений, то пользователю будет выдано предупреждение V1108. Возможные поля объекта описаны далее в документации.

Если аннотацию нужно применить для всех перегрузок вне зависимости от параметров, то поле можно опустить:

// 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": []
  ....
}

Возможные значения атрибутов параметров

#

Название атрибута

Описание атрибута

1

immutable

Подсказывает анализатору, что после вызова функции переданный аргумент не был модифицирован. Например, функция printf имеет побочные эффекты (печать в stdout), однако не производит модификации переданных аргументов.

2

not_null

Действует только для параметров nullable-типа. В функцию необходимо передавать аргумент в состоянии "валидный".

3

unique_arg

Передаваемые аргументы должны отличаться. Например, нет смысла передавать в std::swap два одинаковых аргумента.

4

format_arg

Параметр обозначает форматную строку. Анализатор будет проверять аргументы согласно форматной спецификации printf.

5

pointer_to_free

Указатель, по которому в функции будет освобождена память с помощью free. Указатель может быть нулевым.

6

pointer_to_gfree

Указатель, по которому в функции будет освобождена память с помощью g_free. Указатель может быть нулевым.

7

pointer_to_delete

Указатель, по которому в функции будет освобождена память с помощью 'operator delete'. Указатель может быть нулевым.

8

pointer_to_delete[]

Указатель, по которому в функции будет освобождена память с помощью 'operator delete[]'. Указатель может быть нулевым.

9

pointer_to_unmap

Указатель, по которому в функции будет освобождена память с помощью 'munmap'. Указатель может быть нулевым.

10

taint_source

Данные, возвращающиеся через параметр, получены из недостоверного источника.

11

taint_sink

Данные, передаваемые через параметр, могут привести к эксплуатации уязвимости, если они получены из недостоверного источника.

12

non_empty_string

Параметр должен принимать любую непустую строку.

Возможные поля ограничений параметров

Все поля ограничений — опциональные. Далее приведён список полей, которые задают условия ограничения.

Поля, задающие список разрешённых и запрещённых значений параметра:

  • Поле allowed — массив строк. Задаёт список разрешённых интегральных значений, которые может принимать параметр функции. По умолчанию значения, не перечисленные в этом списке, запрещены.
  • Поле disallowed — массив строк. Задаёт список запрещённых интегральных значений, которые может принимать параметр функции. По умолчанию значения, не перечисленные в этом списке, разрешены.

Каждая строка внутри массива — это интервал от минимальной до максимальной границы включительно. Строка с интервалами указывается в формате "x..y", где 'x' и 'y' — это левая и правая границы соответственно. Одну из границ можно опустить. Тогда строка будет иметь вид "x.." или "..y". В таком случае интервал будет от 'x' до плюс бесконечности и от минус бесконечности до 'y' соответственно.

Примеры интервалов:

  • "0..10" — строка, задающая интервал от 0 до 10 включительно.
  • "..10" — строка, задающая интервал от минус бесконечности до 10 включительно.
  • "0.." — строка, задающая интервал от 0 до плюс бесконечности.

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

Если поля allowed и disallowed указаны одновременно, то анализатор вычитает "disallowed" интервалы из "allowed", чтобы получить множество разрешённых значений. Если значения из поля disallowed полностью перекрывают значения из allowed, то пользователю будет выдано предупреждение V019.

Поле "returns"

Опциональное поле. Объект, внутри которого возможно использование только поля attributes — массива строк, который позволяет задать атрибуты возвращаемого значения.

Возможные значения атрибутов возвращаемого результата

#

Название атрибута

Описание атрибута

1

not_null

Функция всегда возвращает объект nullable-типа в состоянии "валидный".

2

maybe_null

Функция может вернуть объект nullable-типа в состоянии "невалидный", и его стоит проверить перед разыменованием.

3

taint_source

Функция может вернуть данные из недостоверного источника.

Поле "template_params"

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

// Code
template <typename T1, class T2>
void MySwap(T1 &lhs, T2 &rhs);

// Annotation
{
  ....
  "template_params": [ "typename T1", "class T2" ],
  "name": "MySwap",
  "params": [
    { "type": "T1 &", attributes: [ "unique_arg" ] },
    { "type": "T2 &", attributes: [ "unique_arg" ] }
  ]
  ....
}

Поле "qualifiers"

Опциональное поле. Позволяет применить аннотацию только к функции-члену с определённым набором cvref-квалификаторов. Доступно только для вложенных аннотаций, у которых поле type имеет значение "function". Данное поле совместно с name и params задаёт сигнатуру нестатической функции-члена, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. Принимает массив строк со следующими возможными значениями: "const", "volatile", "&" или "&&".

Пример:

// Code
struct Foo
{
  void Bar();                // don't need to annotate this overload
  void Bar() const;          // want to annotate this overload
  void Bar() const volatile; // and this one
};

// Annotation
{
  ....
  "type": "record",
  "name": "Foo",
  "members": [
    {
      "type": "function",
      "name": "Bar",
      "qualifiers": [ "const" ]
    },
    {
      "type": "function",
      "name": "Bar",
      "qualifiers": [ "const", "volatile" ]
    }
  ]
  ....
}

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

// Code
struct Foo
{
  void Bar();       // want to annotate this overload
  void Bar() const; // and this one
};

// Annotation
{
  ....
  "type": "record",
  "name": "Foo",
  "members": [
    {
      "type": "function",
      "name": "Bar",
    }
  ]
  ....
}

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

// Code
struct Foo
{
  void Bar();       // want to annotate this overload
  void Bar() const; // but NOT this one
};

// Annotation
{
  ....
  "type": "record",
  "name": "Foo",
  "members": [
    {
      "type": "function",
      "name": "Bar",
      "qualifiers": []
    }
  ]
  ....
}

Поле "attributes"

Опциональное поле. Массив строк, который задаёт свойства сущности.

Возможные атрибуты функций и конструкторов

#

Название атрибута

Описание атрибута

Примечание

1

pure

Функция считается чистой.

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

2

noreturn

Функция не возвращает управление вызывающей функции.

3

nodiscard

Результат функции должен использоваться.

4

nullable_uninitialized

Функция-член пользовательского nullable-типа переводит объект в состоянии "невалидный".

5

nullable_initialized

Функция-член пользовательского nullable-типа переводит объект в состоянии "валидный".

6

nullable_checker

Функция проверяет состояние пользовательского nullable-типа. Если функция возвращает true, то объект считается в состоянии "валидный", не возвращает — "невалидный". Результат функции должен неявно конвертироваться к типу bool.

7

nullable_getter

Функция производит доступ к внутренним данным пользовательского nullable-типа. Объект при этом должен быть в состоянии "валидный".

8

dangerous

Функция помечена как опасная, и код программы не должен содержать её вызова.

Можно использовать в качестве пометки функции как устаревшей (deprecated).

Для выдачи предупреждений требуется включить диагностическое правило V2016 в настройках.

Ниже приведена таблица применимости различных атрибутов с аннотациями функций:

#

Атрибут

Свободная функция

Конструктор

Функция-член

1

pure

2

noreturn

3

nodiscard

4

nullable_uninitialized

5

nullable_initialized

6

nullable_checker

7

nullable_getter

8

dangerous

Схема JSON

JSON схемы поставляется в дистрибутиве, а также доступны по ссылкам ниже:

Примеры

Как проаннотировать свой 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 проверяет состояние объекта nullable-типа. Если объект в состоянии "валидный", то возвращается true, в обратном случае — false. Функция не меняет состояния объекта nullable-типа.
  • Перегрузки функций-членов Value возвращают нижележащий объект. Функции не меняют состояние объекта nullable-типа.

Тогда аннотация класса и его функций-членов будет выглядеть следующим образом:

{
  "version": 1,
  "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-типа

Допустим, есть следующий код:

namespace Foo
{
  template <typaname CharT>
  size_t my_strlen(const CharT *ptr);
}

Функция Foo::my_strlen имеет следующие свойства:

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

Тогда аннотация функции будет выглядеть следующим образом:

{
  "version": 1,
  "annotations":
  [
    {
      "type": "function",
      "name": "Foo::my_strlen",
      "attributes": [ "pure" ],
      "template_params": [ "typename CharT" ],
      "params": [
        {
          "type": "const CharT *",
          "attributes": [ "not_null" ]
        }
      ]
    }
  ]
}

Как разметить пользовательскую функцию форматного ввода/вывода

Допустим, есть следующая функция Foo::LogAtError:

namespace Foo
{
  void LogAtError(const char *, ...);
}

О ней известны следующие факты:

  • Первым параметром она принимает форматную строку согласно спецификации printf. Аргумент не должен быть нулевым.
  • Соответствующие форматной строке аргументы передаются, начиная со второго.
  • Функция не модифицирует переданные аргументы.
  • Функция не возвращает управление после вызова.

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

{
  "version": 1,
  "annotations": [
    {
      "type": "function",
      "name": "Foo::LogAtError",
      "attributes": [ "noreturn" ],
      "params": [
        {
          "type": "const char *",
          "attributes" : [ "format_arg", "not_null", "immutable" ]
        },
        {
          "type": "...",
          "attributes": [ "immutable" ]
        }
      ]
    }
  ]
}

Как пользоваться символом подстановки для аннотирования нескольких перегрузок

Допустим, что в предыдущем примере программист добавил несколько перегрузок функции Foo::LogAtExit:

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" ]
        }
      ]
    }
  ]
}

Как пометить функцию как опасную для использования (или устаревшую)

Допустим, есть две перегрузки функции Foo::Bar:

namespace Foo
{
  void Bar(int i);
  void Bar(double d);
}

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

{
  "version": 1,
  "annotations": [
    {
      "type": "function",
      "name": "Foo::Bar",
      "attributes": [ "dangerous" ],
      "params": [
        {
          "type": "int"
        }
      ]
    }
  ]
}

Как пометить функцию как источник/приёмник недостоверных данных

Допустим, что есть функция, которая возвращает внешние данные через out-параметр и возвращаемое значение.

std::string ReadStrFromStream(std::istream &input, std::string &str)
{
  ....
  input >> str; 
  return str;
  ....
}

Для того, чтобы пометить эту функцию как источник недостоверных данных, необходимо её разметить следующим образом:

{
  "version": 1,
  "annotations": [
    {
      "type": "function",
      "name": "ReadStrFromStream",
      "params": [
        {
          "type": "std::istream &input"
        },
        {
          "type": "std::string &str",
          "attributes": [ "taint_source" ]
        }
      ],
      "returns": { "attributes": [ "taint_source" ] }
    }
  ]
}

Предположим, что существует функция, при попадании в которую недостоверных данных может быть эксплуатирована какая-либо уязвимость.

void DoSomethingWithData(std::string &str)
{
  .... // Some vulnerability
}

Чтобы пометить такую функцию как приемник недостоверных данных (функцию-сток), необходимо написать следующую аннотацию:

{
  "version": 1,
  "annotations": [
    {
      {
      "type": "function",
      "name": "DoSomethingWithData",
      "params": [ { "type": "std::string &str",
                    "attributes": [ "taint_sink" ] }]
      }
    }
  ]
}

Как проаннотировать пользовательское исключение, которое не должно создаваться без поясняющего сообщения

В C++ анализаторе есть диагностическое правило V1116, которое срабатывает при обнаружении создания объекта исключения с пустым сообщением. По умолчанию данное правило работает только с типами исключений из стандартной библиотеки C++. Для поддержки пользовательских типов исключений необходимо добавить специальную аннотацию.

Предположим, есть класс, определяющий объекты, которые будут использованы для выброса исключения:

class MyException: public std::runtime_error
{
public:
  MyException(const std::string& what_arg )
  : std::runtime_error(what_arg){}

   MyException(const char *what_arg)
   : std::runtime_error(what_arg){}
};

В аннотации необходимо определить тип исключения с атрибутом 'exception' и затем конструкторы, способные принимать поясняющие сообщения. Для каждого параметра конструктора, который должен принимать сообщение, следует указать атрибут 'non_empty_string'.

Например, аннотация класса 'MyException' может выглядеть так:

{
  "version": 2,
  "language": "cpp",
  "annotations": [
   {
    "type": "class",
    "name": "MyException",
    "attributes": ["exception"],
    "members": [
     {
     "type": "ctor",
     "params": [{ "type": "const char*",
                    "attributes": ["non_empty_string"]}]
     },
     {
     "type": "ctor",
     "params": [{ "type": "const std::string&", 
                  "attributes": ["non_empty_string"]}]
     }
    ]
   }
  ]
}
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте, отфильтровано ли письмо в одну из следующих стандартных папок:

  • Промоакции
  • Оповещения
  • Спам