PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Один із найактуальніших сценаріїв використання аналізатора PVS-Studio — його інтеграція із CI системами. І хоча аналіз проекту PVS-Studio практично з-під будь-якої continuous integration системи можна вбудувати всього в кілька команд, ми продовжуємо робити цей процес ще зручніше. У PVS-Studio з'явилася підтримка перетворення виведення аналізатора на формат для TeamCity - TeamCity Inspections Type. Погляньмо, як це працює.

Інформація про ПЗ

ПВС-Студія — статичний аналізатор С, С++, C# та Java коду, призначений для полегшення завдання пошуку та виправлення різноманітних помилок. Аналізатор можна використовувати у Windows, Linux та macOS. У цій статті ми активно використовуватимемо не тільки сам аналізатор, а й деякі утиліти з його дистрибутива.

CLMonitor — є сервером моніторингу, який здійснює відстеження запусків компіляторів. Його необхідно запустити безпосередньо перед початком складання вашого проекту. У режимі відстеження сервер перехоплюватиме запуски всіх компіляторів, що підтримуються. Варто зазначити, що цю утиліту можна використовувати лише для аналізу C/С++ проектів.

PlogConverter – утиліта для конвертації звіту аналізатора у різні формати.

Інформація про досліджуваний проект

Спробуємо цю функціональність на практичному прикладі – проаналізуємо проект OpenRCT2.

OpenRCT2 - Відкрита реалізація гри RollerCoaster Tycoon 2 (RCT2), що розширює її новими функціями і виправляє помилки. Ігровий процес обертається навколо будівництва та утримання парку розваг, у якому знаходяться атракціони, магазини та об'єкти. Гравець повинен постаратися отримати прибуток і підтримувати хорошу репутацію парку, зберігаючи гостей щасливими. OpenRCT2 дозволяє грати як у сценарії, так і в пісочниці. Сценарії вимагають, щоб гравець виконав певне завдання у встановлений час, тоді як пісочниця дозволяє гравцеві побудувати більш гнучкий парк без будь-яких обмежень чи фінансів.

Налаштування

З метою економії часу я, мабуть, опущу процес встановлення та почну з того моменту, коли у мене на комп'ютері запущено сервер TeamCity. Нам потрібно перейти: localhost:{вказаний у процесі встановлення порт}(у моєму випадку, localhost:9090) та ввести дані для авторизації. Після входу нас зустріне:

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Натисніть кнопку Create Project. Далі виберемо Manually, заповнимо поля.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Після натискання на кнопку Створювати, нас зустрічає вікно з налаштуваннями.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Натиснемо Create build configuration.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Заповнюємо поля, натискаємо Створювати. Ми бачимо вікно із пропозицією вибору системи контролю версій. Оскільки вихідники вже лежать локально, тиснемо Пропускати.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Зрештою, ми переходимо до налаштувань проекту.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Додамо кроки збирання, для цього тиснемо: Build steps -> Add build step.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Тут виберемо:

  • Runner type -> Command Line
  • Run -> Custom Script

Так як ми проводитимемо аналіз під час компіляції проекту, складання та аналіз повинні бути одним кроком, тому заповнимо поле Користувацький скрипт:

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
На окремих кроках ми зупинимося згодом. Важливо, щоб завантаження аналізатора, складання проекту, його аналіз, виведення звіту та його форматування зайняло лише одинадцять рядків коду.

Останнє, що нам потрібно зробити, — встановити змінні оточення, якими я позначив деякі шляхи для покращення їхньої читабельності. Для цього перейдемо: Parameters -> Add new parameter і додамо три змінні:

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Залишається натиснути на кнопку прогін в правому верхньому куті. Поки йде складання та аналіз проекту розповім вам про скрипт.

Безпосередньо скрипт

Для початку нам потрібно викачати новий дистрибутив PVS-Studio. Для цього ми використовуємо пакетний менеджер Сhocolatey. Для тих, хто хоче дізнатися про це детальніше, є відповідна стаття:

choco install pvs-studio -y

Далі запустимо утиліту відстеження складання проекту CLMonitor.

%CLmon% monitor –-attach

Потім зробимо складання проекту, як змінне оточення MSB виступає шлях до потрібної мені для збирання версії MSBuild

%MSB% %ProjPath% /t:clean
%MSB% %ProjPath% /t:rebuild /p:configuration=release
%MSB% %ProjPath% /t:g2
%MSB% %ProjPath% /t:PublishPortable

Введемо логін та ключ ліцензії PVS-Studio:

%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%

Після завершення складання ще раз запустимо CLMonitor для генерації препроцесованих файлів та статичного аналізу:

%CLmon% analyze -l "c:ptest.plog"

Після скористаємося ще однією утилітою нашого дистрибутива. PlogConverter перетворює звіт зі стандартного на специфічний для TeamCity формат. Завдяки цьому ми зможемо подивитися його прямо у вікні збирання.

%PlogConverter% "c:ptest.plog" --renderTypes=TeamCity -o "C:temp"

Останньою дією виведемо форматований звіт у stdout, де його підхопить парсер TeamCity

type "C:tempptest.plog_TeamCity.txt"

Повний код скрипту:

choco install pvs-studio -y
%CLmon% monitor --attach
set platform=x64
%MSB% %ProjPath% /t:clean
%MSB% %ProjPath% /t:rebuild /p:configuration=release
%MSB% %ProjPath% /t:g2
%MSB% %ProjPath% /t:PublishPortable
%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%
%CLmon% analyze -l "c:ptest.plog"
%PlogConverter% "c:ptest.plog" --renderTypes=TeamCity -o "C:temp"
type "C:tempptest.plog_TeamCity.txt"

Тим часом, складання та аналіз проекту успішно завершилися, ми можемо перейти на вкладку Завдання і переконатися в цьому.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Тепер клікнемо на Inspections Total, Щоб перейти до перегляду звіту аналізатора:

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Попередження згруповано за номерами діагностичних правил. Для здійснення навігації за кодом потрібно натиснути на номер рядка з попередженням. Натиснувши на знак питання у верхньому правому куті, відкриє вам нову вкладку з документацією. Також можна здійснити навігацію за кодом, натиснувши номер рядка з попередженням аналізатора. Навігація з віддаленого комп'ютера можлива при застосуванні SourceTreeRoot маркер. Той, кому цікавий даний режим роботи аналізатора, може ознайомитись із відповідним розділом документації.

Перегляд результатів роботи аналізатора

Після того, як ми закінчили з розгортанням та налаштуванням складання, пропоную подивитися на деякі цікаві попередження, виявлені у досліджуваному проекті.

Попередження N1

V773 [CWE-401] Висновок був невдалий безперенесення 'result' pointer. A memory leak is possible. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....)
{
  Object* result = nullptr;
  ....
  result = CreateObject(entry);
  ....
  if (readContext.WasError())
  {
    throw std::runtime_error("Object has errors");
  }
  ....
}

Object* CreateObject(const rct_object_entry& entry)
{
  Object* result;
  switch (entry.GetType())
  {
    case OBJECT_TYPE_RIDE:
      result = new RideObject(entry);
      break;
    case OBJECT_TYPE_SMALL_SCENERY:
      result = new SmallSceneryObject(entry);
      break;
    case OBJECT_TYPE_LARGE_SCENERY:
      result = new LargeSceneryObject(entry);
      break;
    ....
    default:
      throw std::runtime_error("Invalid object type");
  }
  return result;
}

Аналізатор помітив помилку, що полягає в тому, що після динамічного виділення пам'яті CreateObject, у разі виключення пам'ять не очищається, відповідно, виникає витік пам'яті.

Попередження N2

V501 Вони є identical sub-expressions '(1ULL << WIDX_MONTH_BOX)' до лівого і правого '|' Operator. libopenrct2ui Cheats.cpp 487

static uint64_t window_cheats_page_enabled_widgets[] = 
{
  MAIN_CHEAT_ENABLED_WIDGETS |
  (1ULL << WIDX_NO_MONEY) |
  (1ULL << WIDX_ADD_SET_MONEY_GROUP) |
  (1ULL << WIDX_MONEY_SPINNER) |
  (1ULL << WIDX_MONEY_SPINNER_INCREMENT) |
  (1ULL << WIDX_MONEY_SPINNER_DECREMENT) |
  (1ULL << WIDX_ADD_MONEY) |
  (1ULL << WIDX_SET_MONEY) |
  (1ULL << WIDX_CLEAR_LOAN) |
  (1ULL << WIDX_DATE_SET) |
  (1ULL << WIDX_MONTH_BOX) |  // <=
  (1ULL << WIDX_MONTH_UP) |
  (1ULL << WIDX_MONTH_DOWN) |
  (1ULL << WIDX_YEAR_BOX) |
  (1ULL << WIDX_YEAR_UP) |
  (1ULL << WIDX_YEAR_DOWN) |
  (1ULL << WIDX_DAY_BOX) |
  (1ULL << WIDX_DAY_UP) |
  (1ULL << WIDX_DAY_DOWN) |
  (1ULL << WIDX_MONTH_BOX) |  // <=
  (1ULL << WIDX_DATE_GROUP) |
  (1ULL << WIDX_DATE_RESET),
  ....
};

Мало хто, окрім статичного аналізатора, зміг би пройти цей тест на уважність. Цей приклад копіпасти гарний саме цим.

Попередження N3

V703 It is odd that the 'flags' field in derived class 'RCT12BannerElement' overwrites field в base class 'RCT12TileElementBase'. Check lines: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

struct RCT12SpriteBase
{
  ....
  uint8_t flags;
  ....
};
struct rct1_peep : RCT12SpriteBase
{
  ....
  uint8_t flags;
  ....
};

Звичайно, використання змінної з одним ім'ям у базовому класі та у спадкоємці далеко не завжди є помилкою. Проте технологія спадкування сама собою передбачає наявність всіх полів батьківського класу в дочірньому. Оголосивши ж у спадкоємця поля з таким самим ім'ям, ми вносимо плутанину.

Попередження N4

V793 It is odd that the result of the 'imageDirection / 8' statement is apart of the condition. Perhaps, this statement should have been compared with something else. libopenrct2 ObservationTower.cpp 38

void vehicle_visual_observation_tower(...., int32_t imageDirection, ....)
{
  if ((imageDirection / 8) && (imageDirection / 8) != 3)
  {
    ....
  }
  ....
}

Давайте розберемося докладніше. Вираз imageDirection / 8 буде false у тому випадку, якщо imageDirection знаходиться в діапазоні від -7 до 7. Друга частина: (imageDirection / 8)! = 3 перевіряє imageDirection на перебування поза діапазоном: від -31 до -24 і від 24 до 31 відповідно. Мені здається досить дивним перевіряти числа на входження у певний діапазон у такий спосіб і, навіть якщо в даному фрагменті коду немає помилки, я б рекомендував переписати ці умови на більш явні. Це суттєво спростило б життя людям, які читатимуть та підтримуватимуть цей код.

Попередження N5

V587 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

void process_mouse_over(....)
{
  ....
  switch (window->widgets[widgetId].type)
  {
    case WWT_VIEWPORT:
      ebx = 0;
      edi = cursorId;                                 // <=
      // Window event WE_UNKNOWN_0E was called here,
      // but no windows actually implemented a handler and
      // it's not known what it was for
      cursorId = edi;                                 // <=
      if ((ebx & 0xFF) != 0)
      {
        set_cursor(cursorId);
        return;
      }
      break;
      ....
  }
  ....
}

Цей фрагмент коду, швидше за все, було отримано шляхом декомпіляції. Потім, судячи з залишеного коментаря, було видалено частину неробочого коду. Однак залишилася пара операцій над cursorId, які також не мають особливого сенсу.

Попередження N6

V1004 [CWE-476] The 'player' pointer був використаний unsafely after it був verified against nullptr. Check lines: 2085, 2094. libopenrct2 Network.cpp 2094

void Network::ProcessPlayerList()
{
  ....
  auto* player = GetPlayerByID(pendingPlayer.Id);
  if (player == nullptr)
  {
    // Add new player.
    player = AddPlayer("", "");
    if (player)                                          // <=
    {
      *player = pendingPlayer;
       if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
       {
         _serverConnection->Player = player;
       }
    }
    newPlayers.push_back(player->Id);                    // <=
  }
  ....
}

Цей код виправити досить просто, потрібно або втретє перевіряти гравець на нульовий покажчик, або внести їх у тіло умовного оператора. Я запропонував би другий варіант:

void Network::ProcessPlayerList()
{
  ....
  auto* player = GetPlayerByID(pendingPlayer.Id);
  if (player == nullptr)
  {
    // Add new player.
    player = AddPlayer("", "");
    if (player)
    {
      *player = pendingPlayer;
      if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
      {
        _serverConnection->Player = player;
      }
      newPlayers.push_back(player->Id);
    }
  }
  ....
}

Попередження N7

V547 [CWE-570] Expression 'name == nullptr' is always false. libopenrct2 ServerList.cpp 102

std::optional<ServerListEntry> ServerListEntry::FromJson(...)
{
  auto name = json_object_get(server, "name");
  .....
  if (name == nullptr || version == nullptr)
  {
    ....
  }
  else
  {
    ....
    entry.name = (name == nullptr ? "" : json_string_value(name));
    ....
  }
  ....
}

Можна одним махом позбавитися від рядка коду, що важко читати, і вирішити проблему з перевіркою на nullptr. Пропоную змінити код таким чином:

std::optional<ServerListEntry> ServerListEntry::FromJson(...)
{
  auto name = json_object_get(server, "name");
  .....
  if (name == nullptr || version == nullptr)
  {
    name = ""
    ....
  }
  else
  {
    ....
    entry.name = json_string_value(name);
    ....
  }
  ....
}

Попередження N8

V1048 [CWE-1164] The 'ColumnHeaderPressedCurrentState' variable був встановлений саме значення. libopenrct2ui CustomListView.cpp 510

void CustomListView::MouseUp(....)
{
  ....
  if (!ColumnHeaderPressedCurrentState)
  {
    ColumnHeaderPressed = std::nullopt;
    ColumnHeaderPressedCurrentState = false;
    Invalidate();
  }
}

Код має досить дивний вигляд. Мені здається, мала місце помилка або в умові, або при повторному присвоєнні змінної ColumnHeaderPressedCurrentState значення false.

Висновок

Як бачимо, інтегрувати статичний аналізатор PVS-Studio у свій проект на TeamCity досить просто. Для цього достатньо написати лише один маленький файл конфігурації. Перевірка коду дозволить виявляти проблеми відразу після складання, що допоможе усувати їх тоді, коли складність і вартість правок ще малі.

PVS-Studio та Continuous Integration: TeamCity. Аналіз проекту Open RollerCoaster Tycoon 2
Якщо хочете поділитися цією статтею з англомовною аудиторією, прошу використати посилання на переклад: Vladislav Stolyarov. PVS-Studio and Continuous Integration: TeamCity. Analysis of the Open RollerCoaster Tycoon 2 проект.

Джерело: habr.com

Додати коментар або відгук