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

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

Информация за използваното софтуерно осигуряване

PVS-Studio — статичен анализатор за C, C++, C# и Java код, предназначен да улесни задачата по търсене и коригиране на различни видове грешки. Анализаторът може да се използва в Windows, Linux и macOS. В тази статия ще използваме не само самия анализатор, но и някои утилити от неговия дистрибутив.

CLMonitor — представлява сървър за мониторинг, който следи стартиранията на компилатори. Трябва да стартирате тази утилита непосредствено преди започване на изграждането на вашия проект. В режим на наблюдение сървърът ще прихваща стартирания на всички поддържани компилатори. Струва си да се отбележи, че тази утилита може да се използва само за анализ на C/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
След натискане на бутона Create, посреща ни прозорец с настройки.

PVS-Studio и Continuous Integration: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2
Натиснете Създаване на конфигурация за изграждане.

PVS-Studio и Continuous Integration: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2
Попълваме полетата, натискаме Create. Ние виждаме прозорец с предложение за избор на система за контрол на версиите. Тъй като изходният код вече е локално, натискаме Пропусни.

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

Тъй като ще провеждаме анализ по време на компилацията на проекта, изграждането и анализът трябва да бъдат една стъпка, затова попълваме полето Custom Script:

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

Последното, което ни остава да направим, е да зададем променливи на средата, за които обозначих някои пътища за подобряване на четимостта им. За целта преминаваме на: Parameters -> Add new parameter и добавяме три променливи:

PVS-Studio и Continuous Integration: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2
Остава да натиснем бутона Run в горния десен ъгъл. Докато протича изграждането и анализът на проекта, ще ви разкажа за скрипта.

Самият скрипт

За начало трябва да изтеглим последната версия на 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
Сега кликнете на Общо проверки, за да преминете към прегледа на доклада на анализатора:

PVS-Studio и Continuous Integration: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2
Предупрежденията са групирани по номера на диагностичните правила. За навигация в кода, кликнете върху номера на реда с предупреждение. Натискането на знака въпрос в горния десен ъгъл ще отвори нов раздел с документация. Можете също да навигирате в кода, като кликнете на номера на реда с предупреждението на анализатора. Навигацията от дистанционен компютър е възможна при използване на SourceTreeRoot маркер. Този, на когото му интересува този режим на работа на анализатора, може да се запознае с съответния раздел документацията.

Преглед на резултатите от анализатора

След като завършихме с разгръщането и настройката на сборката, предлагам да разгледаме някои интересни предупреждения, открити в разглеждания проект.

Предупреждение N1

V773 [CWE-401] Изключение е хвърлено без освобождаване на указателя 'result'. Възможна е утечка на памет. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....)
{
  Object* result = nullptr;
  ....
  result = CreateObject(entry);
  ....
  if (readContext.WasError())
  {
    throw std::runtime_error("Обектът има грешки");
  }
  ....
}

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("Невалиден тип обект");
  }
  return result;
}

Анализаторът откри грешка, свързана с това, че след динамично разпределение на паметта в CreateObject, при възникване на изключение паметта не се освобождава, следователно, настъпва утечка на памет.

Предупреждение N2

V501 Има идентични подизрази '(1ULL << WIDX_MONTH_BOX)' вляво и вдясно на оператора '|'. 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 Странно е, че полето 'flags' в производния клас 'RCT12BannerElement' презаписва полето в основния клас 'RCT12TileElementBase'. Проверете редове: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

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

Разбира се, използването на променлива с едно и също име в основния клас и наследника далеч не винаги е грешка. Въпреки това, технологията на наследяване сама по себе си предполага наличие на всички полета на родителския клас в дъщерния. Обявявайки полета с такова име в наследника, ние внасяме объркване.

Предупреждение N4

V793 Странно е, че резултатът от израза 'imageDirection / 8' е част от условието. Може би това изречение е трябвало да бъде сравнено с нещо друго. 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 Странна последователност от присвоявания от този вид: A = B; B = A;. Проверете редове: 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] Указателят 'player' беше използван небезопасно след проверка срещу nullptr. Проверете редове: 2085, 2094. libopenrct2 Network.cpp 2094

void Network::ProcessPlayerList()
{
  ....
  auto* player = GetPlayerByID(pendingPlayer.Id);
  if (player == nullptr)
  {
    // Добавяне на нов играч.
    player = AddPlayer("", "");
    if (player)                                          // Flags & NETWORK_PLAYER_FLAG_ISSERVER)
      {
        _serverConnection->Player = player;
      }
    }
    newPlayers.push_back(player->Id);                    // <=
  }
  ....
}

Този код е доста лесен за поправка, нужно е или за трети път да проверим player за нулев указател, или да го внесем в тялото на условния оператор. Предлагам втория вариант:

void Network::ProcessPlayerList()
{
  ....
  auto* player = GetPlayerByID(pendingPlayer.Id);
  if (player == nullptr)
  {
    // Добавяне на нов играч.
    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] Изразът 'name == nullptr' винаги е неверен. libopenrct2 ServerList.cpp 102

std::optional 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::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] Променливата 'ColumnHeaderPressedCurrentState' беше присвоена на същата стойност. libopenrct2ui CustomListView.cpp 510

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

Кодът изглежда доста странно. Считам, че е ставало дума за печатна грешка, или в условието, или при повторното присвояване на променливата ColumnHeaderPressedCurrentState стойности неверно.

Извод

Както виждаме, интегрирането на статичния анализатор PVS-Studio в проекта си в TeamCity е доста просто. За целта е достатъчно да напишете само един малък конфигурационен файл. Кодът ще бъде проверен, което ще позволи откриването на проблеми веднага след компилацията, помагайки да се отстранят те, докато сложността и цената на поправките все още са малки.

PVS-Studio и Continuous Integration: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2
Ако искате да споделите тази статия с англоговорящата аудитория, моля, използвайте връзката към превода: Владислав Столяров. PVS-Studio и непрекъсната интеграция: TeamCity. Анализ на проекта Open RollerCoaster Tycoon 2.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster