﻿# Философия статического анализатора PVS\-Studio

Нам регулярно поступают предложения и рекомендации по улучшению диагностических возможностей анализатора\. Большинство предложений мы помещаем во внутренний todo\-список и со временем реализовываем\. Мы благодарны нашим пользователям за присылаемые отзывы и предложения и пользуясь случаям, я хочу сказать им спасибо\. Спасибо\! Итак, многое мы реализуем, но не всё\. Дело в том, что некоторые виды предлагаемых диагностик не укладываются в философию разрабатываемого нами анализатора PVS\-Studio\. Чтобы нашим пользователям была понятна наша позиция и как мы приходим к заключению, что можно реализовывать, а что нет, я решил написать эту подробную заметку\.

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

Есть два философских подхода в реализации статических анализаторов кода:

* Ругаемся на всё, про что не можем сказать, что оно правильное\.
* Ругаемся на то, которое по какой\-то причине скорее всего неправильное\.

Первый подход позволяет обнаружить больше ошибок, чем второй\. Однако, мы считаем, что это путь в никуда, так как количество ложных срабатываний превращает анализатор в инструмент, которым невозможно пользоваться\. Разрабатывая PVS\-Studio, мы придерживаемся второго подхода и ругаемся только в случае, когда для этого есть основания или подозрения\.

К нам поступают предложения по диагностикам следующего вида:

* Следует выдавать предупреждение на A / B, если нет уверенности, что B \!\= 0\.
* Следует выдавать предупреждение, если нет уверенности, что при вызове функции strcpy\(DST, SRC\) буфер DST достаточного размера\.
* Следует выдавать предупреждение при разыменовывании указателя, если нет уверенности, что pointer \!\= NULL\.
* Следует выдавать предупреждение на sqrt\(X\), если нет уверенности, что X \>\= 0\.
* И так далее\.

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

Здесь проявляет себя [теория разбитых окон](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D0%B8%D1%8F_%D1%80%D0%B0%D0%B7%D0%B1%D0%B8%D1%82%D1%8B%D1%85_%D0%BE%D0%BA%D0%BE%D0%BD)\. Стоит сделать пару диагностик в духе философии "ругаемся на всё, про что не можем сказать, что оно правильное" и процесс будет необратим\. Подобные диагностики открывают ящик Пандоры\. Например, непонятно как отказаться от реализации поиска сложения двух переменных _"A\+B"_ типа _int_, если нет уверенности, что не возникнет переполнение\. И так далее и так далее\.

Идя по этому пути, анализатор начнёт выдавать предупреждение на код:

```cpp
void Foo(int &i)
{
  i++;  // Возможно переполнение.
}
```

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

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

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

В том\-то всё и дело, что остановиться не получится\. Если мы сделаем для одного пользователя поиск указателей, то как мы обоснуем другому пользователю, что не будем искать непроверенные деления? Разыменовывание нулевого указателя не более серьезно, чем деление на 0\.

Означает ли это, что анализатор PVS\-Studio не ищет разыменование нулевых указателей или деление на ноль? Конечно же ищет\.

Но поиск ошибок реализован согласно философии "ругаемся на то, которое по какой\-то причине скорее всего неправильное"\. Другими словами, должны существовать некие признаки, которые указывают на то, что код может содержать ошибку\.

Давайте рассмотрим это на примерах\.

```cpp
void F(int *P)
{
  *P = 1;
}
```

Анализатор PVS\-Studio не ругается на этот код, так как у него нет для этого оснований\. То, что указатель _P_ используется без проверки, вовсе не означает, что программа содержит ошибку\.

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

```cpp
void F(int *P)
{
  *P = 1;
}
void Foo()
{
  F(0);
}
```

PVS\-Studio: V522 Dereferencing of the null pointer 'P' might take place\. The null pointer is passed into 'F' function\. Inspect the first argument\. Check lines: 'simple\.cpp:69'\. simple\.cpp 64

Здесь все понятно, раз явно передаём в функцию _NULL_, то это ошибка\. Конечно, на практике подобный код практически не встречается, поэтому рассмотрим что\-то более приближённое к реальности:

```cpp
void F(int *P)
{
  *P = 1;
}
void Foo()
{
  int *X = (int *)malloc(sizeof(int));
  F(X);
  free(X);
}
```

PVS\-Studio: V522 Dereferencing of the null pointer 'P' might take place\. The potential null pointer is passed into 'F' function\. Inspect the first argument\. Check lines: 'simple\.cpp:70'\. simple\.cpp 64

Анализатор вновь выдал предупреждение, но теперь речь идёт о "потенциально нулевом указателе"\. Имеется в виду, что функция _malloc_ может вернуть _NULL,_ и возвращаемый указатель надо обязательно проверить перед использованием\.

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

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

```cpp
void F(int *P)
{
  *P = 1;
}
void Foo()
{
  int *X = (int *)malloc(sizeof(int));
  if (!X)
    return;
  F(X);
  free(X);
}
```

Теперь все хорошо и предупреждений нет\.

А есть ли другие ситуации, когда анализатор выдаст предупреждения? Да\. Для демонстрации я выбрал короткий пример из [списка](https://pvs-studio.ru/ru/blog/examples/v595/) ошибок, найденных в открытых проектах с помощью диагностики V595\. 

```cpp
FName UKismetNodeHelperLibrary::GetEnumeratorName(
  const UEnum* Enum, uint8 EnumeratorValue)
{
  int32 EnumeratorIndex = Enum->GetIndexByValue(EnumeratorValue);
  return (NULL != Enum) ?
         Enum->GetEnum(EnumeratorIndex) : NAME_None;
}
```

PVS\-Studio: V595 The 'Enum' pointer was utilized before it was verified against nullptr\. Check lines: 146, 147\. kismetnodehelperlibrary\.cpp 146

Что заставляет анализатор PVS\-Studio выдать предупреждение на разыменование указателя? То, что после разыменования этот указатель проверяется на равенство _NULL_\. Наличие такой проверки является поводом выдать предупреждение\.

Рассмотрим другой пример\.

```cpp
void F(char *A, char *B)
{
  strcpy(A, B);
}
```

Следует выдавать здесь предупреждение? С нашей точки зрения нет\. Использование функции _strcpy_ само по себе не является ошибкой\.

Если хочется проверить корректность вызова всех таких функций, то анализатор PVS\-Studio тут ни при чём\. Можно просто сделать поиск в программе всех _strcpy_ и изучить соответствующий код\.

Конечно, искать вручную вызов функций _strcpy_ и аналогичных не удобно\. Поэтому Visual C\+\+ предупреждает о наличии таких функций и предлагает заменить их на безопасные аналоги\. Для приведённого выше кода, он выдаст:

warning C4996: 'strcpy': This function or variable may be unsafe\. Consider using strcpy\_s instead\. To disable deprecation, use \_CRT\_SECURE\_NO\_WARNINGS\. See online help for details\.

Когда анализатор PVS\-Studio выдаёт предупреждение на _strcpy_? Когда для этого есть повод\. Пример:

```cpp
void F()
{
  size_t bad_buf_size = strlen("string"); // забыли + 1
  char *A = (char *)malloc(bad_buf_size);
  if (A)
    strcpy(A, "string");
  free(A);
}
```

V512 A call of the 'strcpy' function will lead to overflow of the buffer 'A'\. consoleapplication1\.cpp 14

Надеюсь мне удалось пояснить нашу философию\. Исходя из неё, мы делим предложенные диагностики на две категории:

* Опасно всё, что не проверено \- нерационально и не реализуется\.
* Опасно только то, что подозрительно \- постепенно реализуем, если это технически возможно\.

Важно не выдать как можно больше предупреждений\. В этом как раз нет какой\-то особенной заслуги, так как не понятно, как все эти предупреждения потом просматривать и что с ними делать\. Важно помочь пользователю найти ошибки, и мы совершенствуем PVS\-Studio в этом направлении\.