PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Jednym z najbardziej aktualnych scenariuszy wykorzystania analizatora PVS-Studio jest jego integracja z systemami CI. Mimo że analizę projektu PVS-Studio można włączyć w praktycznie każdej systemie continuous integration w zaledwie kilka komend, nadal staramy się uczynić ten proces jeszcze wygodniejszym. W PVS-Studio dodano wsparcie dla konwersji wyjścia analizatora do formatu dla TeamCity — TeamCity Inspections Type. Zobaczmy, jak to działa.

Informacje o wykorzystywanym oprogramowaniu

PVS-Studio — statyczny analizator kodu w C, C++, C# i Java, zaprojektowany w celu ułatwienia zadania wyszukiwania i naprawiania różnorodnych błędów. Analizator można używać w Windows, Linux i macOS. W tym artykule będziemy intensywnie wykorzystywać nie tylko sam analizator, ale również niektóre narzędzia z jego dystrybucji.

CLMonitor — jest to serwer monitorujący, który śledzi uruchamianie kompilatorów. Należy go uruchomić bezpośrednio przed rozpoczęciem budowy projektu. W trybie śledzenia serwer będzie przechwytywać uruchomienia wszystkich obsługiwanych kompilatorów. Warto zauważyć, że narzędzia te można używać wyłącznie do analizy projektów C/C++.

PlogConverter – narzędzie do konwersji raportu analizatora do różnych formatów.

Informacje o badanym projekcie

Spróbujmy tej funkcjonalności na praktycznym przykładzie – przeanalizujmy projekt OpenRCT2.

OpenRCT2 — otwarta implementacja gry RollerCoaster Tycoon 2 (RCT2), rozszerzająca ją o nowe funkcje i naprawiająca błędy. Rozgrywka koncentruje się na budowie i utrzymaniu parku rozrywki, w którym znajdują się atrakcje, sklepy i obiekty. Gracz musi starać się osiągnąć zysk i utrzymać dobrą reputację parku, jednocześnie dbając o zadowolenie gości. OpenRCT2 pozwala grać zarówno w scenariusze, jak i w trybie piaskownicy. Scenariusze wymagają, aby gracz wykonał określone zadanie w ustalonym czasie, podczas gdy piaskownica pozwala graczowi zbudować bardziej elastyczny park bez jakichkolwiek ograniczeń finansowych.

Konfiguracja

Aby zaoszczędzić czas, pominę proces instalacji i zacznę od momentu, gdy na moim komputerze uruchomiony jest serwer TeamCity. Musimy przejść do: localhost:{port określony w procesie instalacji} (w moim przypadku, localhost:9090) i wprowadzić dane do logowania. Po zalogowaniu przywita nas:

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Naciśnij przycisk Utwórz projekt. Następnie wybierzmy Ręcznie, wypełnijmy pola.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Po naciśnięciu przycisku Utwórz, widzimy okno z ustawieniami.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Naciśnijmy Utwórz konfigurację budowy.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Wypełniamy pola, naciskamy Utwórz. Widzimy okno z propozycją wyboru systemu kontroli wersji. Ponieważ źródła znajdują się już lokalnie, klikamy Pomiń.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Na koniec przechodzimy do ustawień projektu.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Dodajmy kroki budowy, w tym celu klikamy: Kroki budowy -> Dodaj krok budowy.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Tu wybierzemy:

  • Typ uruchamiacza -> Wiersz poleceń
  • Uruchom -> Skrypt niestandardowy

Ponieważ będziemy przeprowadzać analizę podczas kompilacji projektu, budowa i analiza muszą być jednym krokiem, więc wypełnimy pole Skrypt niestandardowy:

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
O osobnych krokach zatrzymamy się później. Ważne jest, aby ładowanie analizatora, budowa projektu, jego analiza, generowanie raportu i jego formatowanie zajmowały w sumie jedenaście linii kodu.

Ostatnią rzeczą, którą musimy zrobić, jest ustawienie zmiennych środowiskowych, którymi oznaczyłem niektóre ścieżki dla lepszej czytelności. W tym celu przechodzimy do: Parametry -> Dodaj nowy parametr i dodamy trzy zmienne:

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Pozostaje kliknąć przycisk Run w prawym górnym rogu. Podczas gdy trwa budowa i analiza projektu, opowiem wam o skrypcie.

Sam skrypt

Na początek musimy pobrać najnowszą wersję PVS-Studio. W tym celu użyjemy menedżera pakietów Chocolatey. Dla tych, którzy chcą się o tym dowiedzieć więcej, istnieje odpowiednia artykuł:

choco install pvs-studio -y

Następnie uruchomimy narzędzie monitorowania budowy projektu CLMonitor.

%CLmon% monitor –-attach

Następnie przeprowadzimy budowę projektu, gdzie jako zmienna środowiskowa MSB będzie ścieżka do wersji MSBuild, która jest mi potrzebna do budowy.

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

Wprowadzimy login i klucz licencji PVS-Studio:

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

Po zakończeniu budowy uruchomimy ponownie CLMonitor, aby wygenerować przetworzone pliki i przeprowadzić analizę statyczną:

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

Po tym skorzystamy z kolejnego narzędzia z naszego pakietu. PlogConverter przekształca raport z formatu standardowego do formatu specyficznego dla TeamCity. Dzięki temu będziemy mogli zobaczyć go bezpośrednio w oknie budowy.

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

Ostatnim krokiem będzie wypisanie sformatowanego raportu do stdout, gdzie zostanie przechwycony przez parser TeamCity.

type "C:tempptest.plog_TeamCity.txt"

Pełny kod skryptu:

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"

Tymczasem budowanie i analiza projektu zakończyły się pomyślnie, możemy przejść do zakładki Projekty i upewnić się o tym.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Teraz kliknijmy na Inspections Total, aby przejść do przeglądania raportu analityka:

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Ostrzeżenia są grupowane według numerów reguł diagnostycznych. Aby nawigować po kodzie, należy kliknąć numer linii z ostrzeżeniem. Kliknięcie znaku zapytania w prawym górnym rogu otworzy nową zakładkę z dokumentacją. Można również nawigować po kodzie, klikając numer linii z ostrzeżeniem analityka. Nawigacja z zdalnego komputera jest możliwa przy użyciu SourceTreeRoot znacznika. Osoby zainteresowane trybem pracy analityka mogą zapoznać się z odpowiednią sekcją dokumentacji.

Przegląd wyników pracy analityka

Po zakończeniu wdrażania i konfiguracji budowy, proponuję przyjrzeć się niektórym interesującym ostrzeżeniom wykrytym w badanym projekcie.

Ostrzeżenie N1

V773 [CWE-401] Wyjątek został rzucony bez zwolnienia wskaźnika 'result'. Możliwy jest wyciek pamięci. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....)
{
  Object* result = nullptr;
  ....
  result = CreateObject(entry);
  ....
  if (readContext.WasError())
  {
    throw std::runtime_error("Obiekt zawiera błędy");
  }
  ....
}

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("Nieprawidłowy typ obiektu");
  }
  return result;
}

Analityk zauważył błąd, polegający na tym, że po dynamicznej alokacji pamięci w CreateObject, w przypadku wystąpienia wyjątku pamięć nie jest zwalniana, co prowadzi do wycieku pamięci.

Ostrzeżenie N2

V501 Istnieją identyczne podwyrażenia '(1ULL << WIDX_MONTH_BOX)' po lewej i prawej stronie operatora '|'. 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),
  ....
};

Niewielu, poza statycznym analizatorem, potrafiłoby zdać ten test na baczność. Ten przykład kopiowania i wklejania jest dobry właśnie przez to.

Ostrzeżenia N3

V703 Dziwne jest, że pole 'flags' w klasie pochodnej 'RCT12BannerElement' nadpisuje pole w klasie bazowej 'RCT12TileElementBase'. Sprawdź linie: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

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

Oczywiście, używanie zmiennej o tej samej nazwie w klasie bazowej i w dziedziczącej nie jest zawsze błędem. Jednak technologia dziedziczenia sama w sobie zakłada, że wszystkie pola klasy rodzica znajdują się w klasie potomnej. Deklarując w klasie potomnej pola o tej samej nazwie, wprowadzamy zamieszanie.

Ostrzeżenie N4

V793 Dziwne jest, że wynik wyrażenia 'imageDirection / 8' jest częścią warunku. Być może to wyrażenie powinno być porównywane z czymś innym. libopenrct2 ObservationTower.cpp 38

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

Przyjrzyjmy się temu bliżej. Wyrażenie imageDirection / 8 będzie fałszywe, jeśli imageDirection znajduje się w przedziale od -7 do 7. Druga część: (imageDirection / 8) != 3 sprawdza imageDirection czy znajduje się poza zakresem: od -31 do -24 i od 24 do 31. Uważam za dość dziwne sprawdzanie liczb na przynależność do określonego przedziału w ten sposób, a nawet jeśli w tym fragmencie kodu nie ma błędu, poleciłbym przepisać te warunki na bardziej oczywiste. Zdecydowanie ułatwiłoby to życie osobom, które będą czytać i utrzymywać ten kod.

Ostrzeżenie N5

V587 Dziwny ciąg przypisań tego rodzaju: A = B; B = A;. Sprawdź linie: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

void process_mouse_over(....)
{
  ....
  switch (window->widgets[widgetId].type)
  {
    case WWT_VIEWPORT:
      ebx = 0;
      edi = cursorId;                                 // <=
      // Wydarzenie okna WE_UNKNOWN_0E zostało wywołane tutaj,
      // ale żadne okna faktycznie nie zaimplementowały obsługi,
      // a nie wiadomo, do czego było przeznaczone
      cursorId = edi;                                 // <=
      if ((ebx & 0xFF) != 0)
      {
        set_cursor(cursorId);
        return;
      }
      break;
      ....
  }
  ....
}

Ten fragment kodu najprawdopodobniej został uzyskany w wyniku dekompilacji. Następnie, sądząc po pozostawionym komentarzu, część nie działającego kodu została usunięta. Jednak pozostała para operacji nad cursorId, które również nie mają szczególnego sensu.

Ostrzeżenie N6

V1004 [CWE-476] Wskaźnik 'player' był używany w sposób niebezpieczny po tym, jak został zweryfikowany względem nullptr. Sprawdź linie: 2085, 2094. libopenrct2 Network.cpp 2094

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

Ten kod naprawić jest dość prosto, należy albo trzeci raz sprawdzić player na wskaźnik null, albo umieścić go w ciele operatora warunkowego. Proponuję drugi wariant:

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

Ostrzeżenie N7

V547 [CWE-570] Wyrażenie 'name == nullptr' jest zawsze fałszywe. 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));
    ....
  }
  ....
}

Można jednym krokiem pozbyć się trudnej do odczytania linii kodu i rozwiązać problem z kontrolą na nullptr. Proponuję zmienić kod w następujący sposób:

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

Ostrzeżenie N8

V1048 [CWE-1164] Zmienna 'ColumnHeaderPressedCurrentState' została przypisana tę samą wartość. libopenrct2ui CustomListView.cpp 510

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

Kod wygląda dość dziwnie. Wydaje mi się, że mogło wystąpić literówka albo w warunku, albo przy ponownym przypisaniu zmiennej ColumnHeaderPressedCurrentState wartości false.

Wnioski

Jak widzimy, integracja statycznego analizatora PVS-Studio w swoim projekcie na TeamCity jest dość prosta. Wystarczy napisać tylko jeden mały plik konfiguracyjny. Sprawdzenie kodu pozwoli zidentyfikować problemy od razu po kompilacji, co pomoże je usunąć w chwili, gdy trudność i koszt poprawek są jeszcze niewielkie.

PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2
Jeśli chcesz podzielić się tym artykułem z anglojęzyczną publicznością, proszę użyj linku do tłumaczenia: Vladislav Stolyarov. PVS-Studio i Continuous Integration: TeamCity. Analiza projektu Open RollerCoaster Tycoon 2.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster