
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
— 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.
— 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++.
– 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.
— 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:

Naciśnij przycisk Utwórz projekt. Następnie wybierzmy Ręcznie, wypełnijmy pola.

Po naciśnięciu przycisku Utwórz, widzimy okno z ustawieniami.

Naciśnijmy Utwórz konfigurację budowy.

Wypełniamy pola, naciskamy Utwórz. Widzimy okno z propozycją wyboru systemu kontroli wersji. Ponieważ źródła znajdują się już lokalnie, klikamy Pomiń.

Na koniec przechodzimy do ustawień projektu.

Dodajmy kroki budowy, w tym celu klikamy: Kroki budowy -> Dodaj krok budowy.

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:

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:

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 :
choco install pvs-studio -yNastępnie uruchomimy narzędzie monitorowania budowy projektu CLMonitor.
%CLmon% monitor –-attachNastę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:PublishPortableWprowadzimy 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.

Teraz kliknijmy na Inspections Total, aby przejść do przeglądania raportu analityka:

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ą .
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
[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
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
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
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
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
[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
[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
[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.
Jeśli chcesz podzielić się tym artykułem z anglojęzyczną publicznością, proszę użyj linku do tłumaczenia: Vladislav Stolyarov. .
Źródło: habr.com
