PVS-Studio — это программа, которая ищет ошибки в исходном коде проектов C++ и C#, которые компилятор не видит, но которые почти наверняка являются ошибками программирования.

Примечание. Эта статья изначально была опубликована на русском языке в блоге blog.harrix.org. Оригинальная и переведенная версии размещены на нашем сайте с разрешения автора.

Введение

Ко мне обратились ребята из команды PVS-Studio с предложением о сотрудничестве. Я много читал об их продукте на страницах Хабрахабра, но ни разу не пробовал. Итак, я предложил следующее: мне дадут лицензию на продукт, а я просканирую свои программы и напишу обзор инструмента, где расскажу, как использую анализатор, как проверяются коды и так далее. . Они сказали да.

Итак, то, что вы увидите в этой статье, — это, как сейчас модно называть, честный обзор без всяких выдумок от обычного среднестатистического программиста, который занимается «академическим» программированием, а не прикладным программированием. Другими словами, я не гуру из какой-то крупной компании, работающей над сложными проектами, который разбирается в тоннах утилит, разбирается в оптимизации компиляторов и тому подобном.

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

Вот так.

Установка

Установка не вызвала никаких проблем. На домашней странице сайта PVS-Studio есть большая кнопка Скачать и попробовать, которая ведет на страницу со ссылкой для скачивания, которую вы точно не пропустите.

Установка полностью стандартна; даже особых опций на выбор нет. Однако в своих статьях я всегда стараюсь описать даже самые простые шаги. Итак, вот вам скриншоты:

Процесс установки PVS-Studio

Шаг 1.

Шаг 2.

Шаг 3.

Шаг 4.

Шаг 5.

Шаг 6.

Шаг 7.

Как все это не удалось

Сразу говорю, никакой документации я сначала не читал. Я просто установил программу и подумал: «Ну и что дальше?» Я обнаружил следующие новые пункты в меню «Пуск»:

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

Честно говоря, я немного забеспокоился. Видите ли, я в основном работаю в Qt и держу Visual Studio скорее как обучающую программу для своих студентов.

В ПОРЯДКЕ. Возможно, мне следует попробовать другой пункт меню, Автономный?

Так-то лучше. Теперь важное замечание. Алгоритм работы с анализатором ожидал, таков: я открываю его, загружаю исходники моего проекта, и он находит за меня ошибки. Это предположение оказалось совершенно неверным, но об этом мы поговорим позже.

Итак, сначала я попытался загрузить один из своих файлов (беспокоил тот факт, что он позволял выбрать только один файл за раз).

Вот оно, а что дальше? Нет больше больших или красочных кнопок.

В главном меню есть только один пункт, похожий на то, что мне нужно:

При нажатии на нее открывается следующее окно.

И вот тут я поступил глупо. Вместо того, чтобы читать текст, я начал нажимать на кнопки. Когда я нажал Выбрать, программа запросила несколько файлов *.suppress, что явно не то, что мне было нужно. Слово Компилятор привлекло мое внимание. Хорошо, поэтому я должен нажать Начать мониторинг.

Я думал, что программа сканирует мой компьютер на наличие компиляторов, так что это должно занять довольно много времени. И это действительно произошло (я ждал несколько часов), но я был рад видеть, что он наконец начал что-то находить:

Только через некоторое время я понял причину: я работал со своими проектами и компилировал их, пока шел процесс мониторинга.

Пару часов спустя я почувствовал, что инструмент нашел достаточное количество компиляторов и остановил его. Однако никаких результатов это не дало. Что мне делать тогда? Блин, теперь надо документацию читать(

Релевантная ссылка оказалась не совсем заметной.

Прочитав статью, я наконец понял, что делать.

Как все получилось

Вот как анализатор на самом деле работает.

Вы запускаете процесс мониторинга в PVS-Studio, а затем запускаете компилятор на своем проекте. По завершении компиляции остановите процесс мониторинга и подождите некоторое время, пока программа выведет журнал анализа.

Я покажу вам, как это работает, на примере тестового приложения Qt 5.7 с MinGW, в котором используется моя библиотека Harrix MathLibrary.

Зайдите в меню анализа.

Начать мониторинг запуска компилятора.

Процесс мониторинга может работать в фоновом режиме.

Скомпилируйте проект:

PVS-Studio обнаружил запущенный экземпляр нашего компилятора.

Остановить мониторинг.

И тут PVS-Studio высыпала кучу предупреждений. Черт. Я надеялся на лучший результат((

Двойной щелчок по предупреждению приведет вас к соответствующему исходному файлу, в котором была обнаружена ошибка.

Когда вы, наконец, уловили идею программы, работать с ней становится легко, но не совсем интуитивно для новичка.

Теперь давайте посмотрим, какие ошибки у нас есть. Они вообще баги?

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

Теперь мне хочется ругаться.

Анализ ошибок

Мы прошли путь моего вознесения к пониманию того, как пользоваться программой. Теперь посмотрим на результаты анализа.

Баги, найденные в самом Qt, меня мало интересуют — за них несут ответственность те, кто разрабатывал компилятор.

А как насчет моих собственных ошибок?

Большинство из более чем 1900 предупреждений относятся к V550:

V550. Странное точное сравнение. Вероятно, лучше использовать сравнение с определенной точностью: fabs(A – B) ‹ Epsilon или fabs(A – B) › Epsilon

И я согласен с этим предупреждением в большинстве случаев. Например, следующий код с (F[i]==F[i+1]) может вызвать проблемы:

//identical elements
//are assigned identical ranks as arithmetic mean
for (i=0;i<VHML_N-1;i++)
{
if (F[i]==F[i+1])
  {
  j=i+1;
  while ((F[i]==F[j])&&(j<VHML_N)) j++;
  Sn=HML_SumOfArithmeticalProgression(i+1,1,j-i);
  Sn/=double(j-i);
  for (k=0;k<VHML_N;k++)
   if (Fitness[k]==F[i]) VHML_ResultVector[k]=Sn;
  i=j-1;
  }
}

Еще хуже идея — проверять крайние положения колеса Максвелла, как это делается в следующем ужасном коде:

//if the wheel is in extreme positions,
if (((x==R)&&(v<0))||((x==l)&&(v>0))) v=-v*(1.-k);

И вот что у меня получилось на следующем фрагменте.

//Calculating arithmetic mean of two samples
xn=HML_Mean(x,VHML_N);
yn=HML_Mean(x,VHML_N);

V656 Переменные «xn», «yn» инициализируются вызовом одной и той же функции. Вероятно, это ошибка или неоптимизированный код. Рассмотрите возможность проверки выражения «HML_Mean(x, VHML_N)». Контрольные строки: 3712, 3713. harrixmathlibrary.h 3713

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

Еще одна глупая ошибка.

int VHML_Result=0;
    if (VHML_N1==VHML_N2)
        for (int i=0;i<VHML_N1;i++)
            if (a[i]!=b[i]) VHML_Result=-1;
            else
                VHML_Result=-1;

V523 Оператор "then" эквивалентен оператору "else". harrixmathlibrary.h 695

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

double HML_LineTwoPoint(double x, double x1, double y1,
                        double x2, double y2, int *solutionis)
{
/*
This function is a two-point linear equation.
Value of y is returned for given x.
Input parameters:
 x - abscissa of point in question;
 x1 - abscissa of first point;
 y1 - ordinate of first point;
 x2 - abscissa of second point;
 y2 - ordinate of second point;
 solutionis - stores the returned solution:
  0 - no solution;
  1 - solution found;
  2 - any number is a solution (the line is parallel to y-axis).
Return value:
 Value of y for given x.
*/
double y=0;
 
if ((x1==x2)&&(y1==y2))
{
  //this is the same point, so any number is a solution
  y=y1;
  *solutionis=2;
}
else
{
  if (y1==y2)
  {
    //this line is parallel to x-axis
    y=y1;
    *solutionis=1;
  }
  else
  {
    if (x1==x2)
    {
      //this line is parallel to y-axis
      if (x==x1)
      {
        y=y1;
        *solutionis=2;
      }
      else
      {
        y=0;
        *solutionis=0;
      }
    }
    else
    {
      y=(x-x1)*(y2-y1)/(x2-x1)+y1;
    }
  }
}
 
*solutionis=1;
return y;
}

V519 Переменной ‘*solutionis’ дважды подряд присваиваются значения. Возможно, это ошибка. Контрольные строки: 1788, 1821. harrixmathlibrary.cpp 1821

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

if (VHML_N>0) VHML_Result=0;
 
...
 
//Evaluating real-vector objective function
VHML_Result=VHML_TempFunction(VHML_TempDouble3,RealLength);
 
return VHML_Result;

V519 Переменной VHML_Result дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 385, 395. harrixmathlibrary.cpp 395

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

template <class T> void HML_Swap(T &a, T &b)
{
/*
This function swaps values of two numbers.
Input parameters:
a - first number;
b - second number.
Return value:
None.
*/
T x;
x = b;
b = a;
a = x;
}
 
template <class T> void HML_NumberInterchange(T &a, T &b)
{
/*
This function swaps values of two numbers.
Input parameters:
a - first number;
b - second number.
Return value:
None.
*/
T x;
x = b;
b = a;
a = x;
}

V524 Странно, что тело функции HML_Swap полностью эквивалентно телу функции HML_NumberInterchange. harrixmathlibrary.h 2349

А вот классическая ошибка, связанная с отсутствующим преобразованием типов.

double HML_TestFunction_HyperEllipsoid(double *x, int VHML_N)
{
/*
Function of multiple variables: Hyperellipsoid.
Test function for real optimization.
Input parameters:
x - pointer to original array;
VHML_N - size of array x.
Return value:
Value of test function at point x.
*/
double VHML_Result=0;
 
for (int i=0;i<VHML_N;i++)
VHML_Result += (i+1)*(i+1)*x[i]*x[i];
 
return VHML_Result;
}

V636 Выражение «(i + 1) * (i + 1)» было неявно преобразовано из типа «int» в тип «double». Рассмотрите возможность использования явного приведения типа, чтобы избежать переполнения. Пример: двойной A = (двойной)(X) * Y;. harrixmathlibrary.cpp 10509

Что касается этого кода, то анализатор выдал ложное предупреждение, так как HML_ProportionalSelectionV2 возвращает случайное значение:

NumberOfParent1=HML_ProportionalSelectionV2(....);
NumberOfParent2=HML_ProportionalSelectionV2(....);

Переменные V656 «NumberOfParent1», «NumberOfParent2» инициализируются посредством вызова одной и той же функции. Вероятно, это ошибка или неоптимизированный код. Контрольные строки: 1106, 1107. harrixmathlibrary.cpp 1107

В библиотеке Harrix QtLibrary обнаружен ряд проблем.

Например, в нем есть функция разбиения строки на слоги. Инструмент дал хороший совет, что я должен объединить условия.

if ((i>=1)&&(i!=N-1))
{
  if ((HQt_GetTypeCharRus(S.at(i-1))==3) &&
     (HQt_GetTypeCharRus(S.at(i))!=0)    &&
     (HQt_GetTypeCharRus(S.at(i+1))!=0))
    cut=true;
}
 
if ((i>=1)&&(i!=N-1))
{
  if ((HQt_GetTypeCharRus(S.at(i-1))==1) &&
     (HQt_GetTypeCharRus(S.at(i))==1)    &&
     (HQt_GetTypeCharRus(S.at(i+1))!=0))
    cut=true;
}

V581 Условные выражения операторов if, расположенных рядом друг с другом, идентичны. Проверить строки: 1140, 1147. harrixqtlibrary.cpp 1147

Цикл в следующем фрагменте содержит логическую переменную in, которая всегда будет true.

int VHQt_Result = -1;
    bool in=false;
    int i=0;
 
    while ((i<StringList.count())&&(in!=true))
    {
        if (StringList.at(i)==String)
            VHQt_Result=i;
        i++;
    }
   return VHQt_Result;

V560 Часть условного выражения всегда истинна: ​​(in != true). harrixqtlibrary.cpp 2342

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

item = new QStandardItem(QString("HML_RealGeneticAlgorith...."));
model->appendRow(item);
 
item = new QStandardItem(QString("HML_RealGeneticAlgorith...."));
model->appendRow(item);

V760 Обнаружены два идентичных блока текста. Второй блок начинается со строки 86. mainwindow.cpp 83

Вердикт

Минусы:

  • Программа не интуитивно понятна; это не легко начать с. Если бы я просто зашел на их сайт, скачал демо-версию и попробовал, то, скорее всего, удалил бы ее, неважно.
  • «Старомодный» дизайн.
  • Подсветка синтаксиса похожа на Notepad++ (и это плюс), но я также привык, что Notepad++ подсвечивает все остальные экземпляры выбранного ключевого слова, а также выделяет соответствующую закрывающую скобку при выборе открывающей.

Плюсы:

  • Программа умеет делать свою работу, а это самое главное. Он может обнаружить множество скрытых ошибок или предупреждений, которые вы вряд ли когда-нибудь заметите.
  • Как только вы разберетесь, как им пользоваться, работа с анализатором станет легкой и комфортной.
  • Инструмент поддерживает несколько компиляторов, в том числе используемые в сборках Qt.

Заключительный вывод: эта программа, безусловно, является обязательной. Очень удобный инструмент для управления качеством вашего кода.

P.S. А я надеялся, что багов не будет(

P.S.S. Более 1900 предупреждений!