Аннотирование C и C++ кода в формате JSON
- Быстрый старт
- Способы подключения файла c аннотациями
- Структура файла с аннотациями
- Аннотации типов
- Аннотации функций
- Схема JSON
- Примеры
- Как проаннотировать свой nullable тип
- Как добавить контракт "всегда валидный" на параметр функции nullable-типа
- Как разметить пользовательскую функцию форматного ввода/вывода
- Как пользоваться символом подстановки для аннотирования нескольких перегрузок
- Как пометить функцию как опасную для использования (или устаревшую)
- Как пометить функцию как источник/приёмник недостоверных данных
- Как проаннотировать пользовательское исключение, которое не должно создаваться без поясняющего сообщения
Механизм пользовательских аннотаций — это способ разметки типов и функций в формате 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"
Опциональное поле. Массив строк, который задаёт свойства сущности. Для аннотаций типов доступны следующие атрибуты:
Умные указатели
- "unique_ptr" — тип имеет интерфейс std::unique_ptr;
- "shared_ptr" — тип имеет интерфейс std::shared_ptr;
- "auto_ptr" — тип имеет интерфейс std::auto_ptr;
Контейнеры
- "string" — тип имеет интерфейс std::basic_string;
- "string_view" — тип имеет интерфейс std::basic_string_view;
- "array" — тип имеет интерфейс std::array;
- "vector" — тип имеет интерфейс std::vector;
- "map" — тип имеет интерфейс std::map;
- "set" — тип имеет интерфейс std::set;
- "list" — тип имеет интерфейс std::list;
- "unordered" — в сочетании с "set" или "map" задаёт типу интерфейс std::unordered_set или std::unordered_map соответственно.
- "multi" — в сочетании с "map" или "set" задаёт типу интерфейс std::multiset или std::multimap. Если также присутствует "unordered", то типу задаётся семантика std::unordered_multiset или std::unordered_multimap.
Другие типы
- "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"]}]
}
]
}
]
}