Napisanie tego artykułu zainspirowała mnie ogromna liczba materiałów na temat analizy statycznej, które coraz częściej przyciągają moją uwagę. Po pierwsze, to , który aktywnie promuje się na Habrze poprzez przeglądy błędów znalezionych przez ich narzędzie w projektach z otwartym kodem. Niedawno PVS-studio wprowadziło , i oczywiście, deweloperzy IntelliJ IDEA, którego wbudowany analizator jest obecnie prawdopodobnie najbardziej zaawansowany dla Javy, .
Czytając takie przeglądy, ma się wrażenie, że mówimy o magicznym eliksirze: naciśnij przycisk, a oto lista defektów przed oczami. Wydaje się, że w miarę udoskonalania analizatorów, błędów będzie automatycznie znajdować się coraz więcej, a produkty skanowane przez te roboty będą stawać się coraz lepsze, bez żadnych wysiłków z naszej strony.
Ale magicznych eliksirów nie ma. Chciałbym porozmawiać o tym, o czym zazwyczaj nie mówi się w postach typu „oto, co nasz robot potrafi znaleźć”: czego analizatory nigdy nie będą mogły zrobić, jaka jest ich rzeczywista rola i miejsce w procesie dostarczania oprogramowania, oraz jak je wdrażać poprawnie.

Wytrząsacz (źródło: ).
Czego nigdy nie będą mogły zrobić statyczne analizatory
Co to jest, z praktycznego punktu widzenia, analiza kodu źródłowego? Podajemy pewne źródła, a na wyjściu w krótkim czasie (zdecydowanie krótszym, niż przeprowadzenie testów) uzyskujemy pewne informacje o naszym systemie. Głównym i matematycznie nie do pokonania ograniczeniem jest to, że w ten sposób możemy uzyskać jedynie dość wąską klasę informacji.
Najbardziej znanym przykładem problemu, który nie może być rozwiązany za pomocą analizy statycznej — : to twierdzenie, które dowodzi, że nie jest możliwe opracowanie ogólnego algorytmu, który na podstawie kodu źródłowego programu określi, czy będzie on się zapętlał, czy zakończy się w skończonym czasie. Rozszerzeniem tego twierdzenia jest , które w każdym nietrywialnym przypadku obliczalnych funkcji definiują, czy dowolny program oblicza funkcję o takim właściwości, jest problemem algorytmicznie nierozwiązywalnym. Na przykład, niemożliwe jest napisanie analizatora, który po dowolnym kodzie źródłowym określi, czy analizowany program jest implementacją algorytmu, który oblicza, powiedzmy, kwadrat liczby całkowitej.
W związku z tym funkcjonalność statycznych analizatorów ma nieprzezwyciężalne ograniczenia. Statyczny analizator nigdy nie będzie w stanie we wszystkich przypadkach określić takich rzeczy, jak na przykład wystąpienie „null pointer exception” w językach dopuszczających wartość null, ani we wszystkich przypadkach określić wystąpienia „atrybut nie znaleziony” w językach z dynamicznym typowaniem. Wszystko, co może osiągnąć najdoskonalszy statyczny analizator, to wyodrębnić przypadki szczególne, których liczba wśród wszystkich możliwych problemów z Twoim kodem źródłowym jest, nie bójmy się tego powiedzieć, kroplą w morzu.
Analiza statyczna to nie poszukiwanie błędów
Z powyższego wynika wniosek: analiza statyczna to nie środek do zmniejszenia liczby defektów w programie. Ośmielę się stwierdzić: stosując ją pierwszy raz w swoim projekcie, znajdzie w kodzie „ciekawostki”, ale prawdopodobnie nie odkryje żadnych defektów wpływających na jakość działania Twojego programu.
Przykłady defektów, które zostały automatycznie wykryte przez analizatory, imponują, ale nie należy zapominać, że te przykłady zostały znalezione dzięki skanowaniu dużego zbioru rozbudowanych baz kodowych. Na tej samej zasadzie hakerzy, mający możliwość przetestowania kilku prostych haseł na wielu kontach, ostatecznie znajdują te konta, na których jest proste hasło.
Czy to oznacza, że nie należy stosować analizy statycznej? Oczywiście, że nie! I z tej samej przyczyny, dla której warto sprawdzać każde nowe hasło pod kątem obecności na liście „prosty” haseł.
Analiza statyczna to więcej niż poszukiwanie błędów
W rzeczywistości kwestie, które można rozwiązać za pomocą analizy, są znacznie szersze. W końcu analiza statyczna to każda kontrola kodów źródłowych przeprowadzana przed ich uruchomieniem. Oto niektóre rzeczy, które można robić:
- Sprawdzenie stylu kodowania w szerokim tego słowa znaczeniu. Obejmuje to zarówno kontrolę formatowania, jak i poszukiwanie użycia pustych lub zbędnych nawiasów, ustalanie progów dla metryk takich jak liczba linii lub złożoność cyklomatyczna metody itd. — wszystko, co potencjalnie utrudnia czytelność i utrzymanie kodu. W Javie takim narzędziem jest Checkstyle, w Pythonie — flake8. Programy tego typu zazwyczaj nazywane są "linterami".
- Analizie może podlegać nie tylko kod wykonywalny. Pliki zasobów, takie jak JSON, YAML, XML, .properties mogą (i powinny!) być automatycznie sprawdzane pod kątem poprawności. Lepiej dowiedzieć się o tym, że struktura JSON została zaburzona przez jakieś nieparzyste cudzysłowy na wczesnym etapie automatycznej weryfikacji Pull Request, niż podczas wykonywania testów lub w czasie wykonania? Odpowiednie narzędzia są dostępne: na przykład, , .
- Kompilacja (lub parsowanie dla dynamicznych języków programowania) to też rodzaj analizy statycznej. Zazwyczaj komputery są w stanie generować ostrzeżenia sygnalizujące problemy z jakością kodu źródłowego, których nie powinno się ignorować.
- Czasami kompilacja to nie tylko kompilacja kodu wykonywalnego. Na przykład, jeśli masz dokumentację w formacie , to w momencie jej przekształcania w HTML/PDF, przetwornik AsciiDoctor () może generować ostrzeżenia, na przykład o naruszonych linkach wewnętrznych. I to jest istotny powód, aby nie zaakceptować Pull Request z zmianami w dokumentacji.
- Sprawdzanie pisowni to także forma analizy statycznej. Narzędzie jest w stanie sprawdzać pisownię nie tylko w dokumentacji, ale także w kodzie źródłowym programów (komentarzach i literałach) w różnych językach programowania, w tym C/C++, Java i Python. Błąd ortograficzny w interfejsie użytkownika lub dokumentacji to też defekt!
- Testy konfiguracyjne (o tym, czym są, patrz i prezentacje), chociaż są przeprowadzane w środowisku wykonywalnym testów jednostkowych typu pytest, rzeczywiście również są rodzajem analizy statycznej, ponieważ nie wykonują kodów źródłowych w trakcie swojego działania.
Jak widzimy, poszukiwanie błędów w tej liście zajmuje najmniej ważną rolę, a wszystko inne dostępne jest dzięki użyciu bezpłatnych narzędzi open source.
Jakie z tych typów analizy statycznej należy zastosować w Twoim projekcie? Oczywiście, najlepiej wszystkie — im więcej, tym lepiej! Najważniejsze, aby wdrożyć to poprawnie, o czym będzie mowa dalej.
Proces dostarczania jako wieloetapowy filtr i analiza statyczna jako jego pierwszy kaskada
Klasyczną metaforą ciągłej integracji jest rura (pipeline), przez którą przepływają zmiany — od zmiany kodu źródłowego do dostarczenia na produkcję. Standardowa kolejność etapów tego procesu wygląda następująco:
- analiza statyczna
- kompilacja
- testy jednostkowe
- testy integracyjne
- testy UI
- ręczna weryfikacja
Zmiany odrzucone na N-tym etapie procesu nie są przekazywane do etapu N+1.
Dlaczego właśnie w ten sposób, a nie inaczej? W tej części procesu, która dotyczy testowania, testerzy poznają szeroko znaną piramidę testowania.

Piramida testowa. Źródło: Martina Fowlera.
Na dolnej części tej piramidy znajdują się testy, które łatwiej pisać, które wykonują się szybciej i rzadziej dają fałszywe alarmy. Dlatego powinno ich być więcej, powinny obejmować więcej kodu i być wykonywane jako pierwsze. Na górze piramidy jest odwrotnie, przez co liczba testów integracyjnych i UI powinna być zredukowana do absolutnego minimum. Człowiek w tym łańcuchu jest najdroższym, najwolniejszym i najmniej niezawodnym zasobem, dlatego znajduje się na samym końcu i wykonuje swoje zadanie tylko wtedy, gdy wcześniejsze etapy nie wykryły żadnych wad. Jednak na tych samych zasadach buduje się procesy w częściach, które nie są bezpośrednio związane z testowaniem!
Chciałbym zaproponować analogię w postaci wieloetapowego systemu filtracji wody. Na wejściu podawana jest brudna woda (zmiany z defektami), a na wyjściu powinniśmy otrzymać czystą wodę, wolną od wszelkich niepożądanych zanieczyszczeń.

Wieloetapowy filtr. Źródło:
Jak wiadomo, filtry oczyszczające są projektowane w taki sposób, aby każdy kolejny kaskad mógł oddzielać coraz drobniejsze frakcje zanieczyszczeń. Przy tym kaskady o większej grubości oczyszczenia mają większą przepustowość i niższe koszty. W naszej analogii oznacza to, że wejściowe quality gates mają większą wydajność, wymagają mniej wysiłku, aby je uruchomić i same w sobie są bardziej bezproblemowe w działaniu — i właśnie w takiej kolejności są one ułożone. Rola analizy statycznej, która, jak teraz rozumiemy, jest w stanie wychwycić jedynie najgrubsze wady — jest rolą siatki „brudnej” na samym początku kaskadu filtrów.
Analiza statyczna sama w sobie nie poprawia jakości końcowego produktu, tak jak „brudnik” nie czyni wody pitną. Niemniej jednak, w ogólnej kooperacji z innymi elementami linii produkcyjnej, jej ważność jest oczywista. Choć w wielokaskadowym filtrze wyjściowe kaskady mogą potencjalnie uchwycić to samo, co kaskady wejściowe — oczywiste jest, do jakich konsekwencji doprowadzi próba polegania tylko na kaskadach drobnego oczyszczania, bez kaskad wejściowych.
Celem „brudnika” jest odciążenie kolejnych kaskad od łapania naprawdę grubych wad. Na przykład, przynajmniej osoba przeprowadzająca przegląd kodu nie powinna być rozpraszana przez niewłaściwie sformatowany kod i naruszenia ustalonych norm kodowania (jak zbędne nawiasy czy zbyt głęboko zagnieżdżone gałęzie). Błędy, takie jak NPE, powinny być wykrywane przez testy jednostkowe, ale jeśli już przed testem analizator wskazuje nam, że błąd musi nastąpić — znacznie przyspieszy to jego naprawę.
Myślę, że teraz jasne jest, dlaczego analiza statyczna nie poprawia jakości produktu, jeśli jest stosowana epizodycznie, i powinna być stosowana na stałe w celu oddzielania zmian z grubymi wadami. Pytanie, czy użycie analizy statycznej poprawi jakość Twojego produktu, jest w zasadzie równoważne pytaniu „czy jakość wody pitnej, wziętej z brudnego zbiornika, poprawi się, jeśli przepuści się ją przez sitko?”
Wdrożenie w projekt legacy
Ważne praktyczne pytanie: jak wdrożyć statyczną analizę w proces ciągłej integracji jako "quality gate"? W przypadku testów automatycznych wszystko jest jasne: istnieje zestaw testów, a niepowodzenie któregokolwiek z nich to wystarczający powód, aby uznać, że budowa nie przeszła quality gate. Próba ustalenia gate na podstawie wyników analizy statycznej kończy się niepowodzeniem: w kodzie legacy jest zbyt wiele ostrzeżeń analizy, nie chcemy ich całkowicie ignorować, ale także niemożliwe jest zatrzymanie dostarczania produktu tylko dlatego, że zawiera ostrzeżenia analizatora.
Będąc stosowana po raz pierwszy, na każdym projekcie, analizator wydaje olbrzymią ilość ostrzeżeń, przytłaczająca większość z nich nie ma związku z prawidłowym działaniem produktu. Niemożliwe jest od razu poprawienie wszystkich tych uwag, a wiele z nich nie wymaga poprawek. W końcu wiemy, że nasz produkt ogólnie działa, i to przed wdrożeniem analizy statycznej!
W rezultacie wielu ogranicza się do epizodycznego wykorzystania statycznej analizy, lub używa jej tylko w trybie informacyjnym, gdy podczas budowy generowany jest po prostu raport analizatora. Jest to równoważne braku jakiejkolwiek analizy, ponieważ jeśli mamy już wiele ostrzeżeń, to pojawienie się kolejnego (jak poważne by nie było) przy zmianie kodu pozostaje niezauważone.
Znane są następujące sposoby wprowadzenia quality gates:
- Ustawienie limitu ogólnej liczby ostrzeżeń lub liczby ostrzeżeń podzielonej przez liczbę linii kodu. Działa to słabo, ponieważ taki gate swobodnie przepuszcza zmiany z nowymi defektami, dopóki ich limit nie zostanie przekroczony.
- Zbiorcze ignorowanie wszystkich starych ostrzeżeń w kodzie w pewnym momencie oraz odrzucenie budowy przy wystąpieniu nowych ostrzeżeń. Taką funkcjonalność oferuje PVS-studio oraz niektóre zasoby internetowe, takie jak Codacy. Nie miałem okazji pracować z PVS-studio, natomiast jeśli chodzi o moje doświadczenie z Codacy, to ich głównym problemem jest złożoność ustalania, które błędy są „stare”, a które „nowe” — to dość skomplikowany i nie zawsze skuteczny algorytm, zwłaszcza jeśli pliki są mocno zmieniane lub przemianowywane. Z tego co pamiętam, Codacy mógł pomijać nowe ostrzeżenia w pull requestach, a jednocześnie nie przepuszczać pull requestów z powodu ostrzeżeń, które nie były związane ze zmianami w kodzie danego PR.
- Moim zdaniem, najskuteczniejszym rozwiązaniem jest opisane w książce „metoda zębatego koła” („ratcheting”). Główna idea polega na tym, że właściwością każdej wersji jest liczba ostrzeżeń ze statycznej analizy, a dopuszczalne są jedynie takie zmiany, które nie zwiększają ogólnej liczby ostrzeżeń.
Zębate koło
Działa to w ten sposób:
- Na początkowym etapie realizowane jest zapisywanie w metadanych o wersji liczby ostrzeżeń w kodzie, wykrytych przez analizatory. W ten sposób, podczas budowy głównej gałęzi, do zarządzającego repozytoriów zapisywana jest nie tylko „wersja 7.0.2”, ale „wersja 7.0.2, zawierająca 100500 ostrzeżeń Checkstyle”. Jeśli używasz zaawansowanego menedżera repozytoriów (takiego jak Artifactory), łatwo jest zachować takie metadane o twojej wersji.
- Teraz każdy pull request podczas budowy porównuje liczbę ostrzeżeń z tym, jakie liczby znajdują się w bieżącej wersji. Jeśli PR prowadzi do zwiększenia tej liczby, kod nie przechodzi przez wytyczne jakości w analizie statycznej. Jeśli liczba ostrzeżeń maleje lub nie zmienia się — to przechodzi.
- Podczas następnej wersji ponownie obliczona liczba ostrzeżeń zostanie zapisana w metadanych wersji.
Tak powoli, ale nieubłaganie (jak podczas pracy mechanizmu zębatego), liczba ostrzeżeń będzie dążyć do zera. Oczywiście, system można oszukać, wprowadzając nowe ostrzeżenie, ale poprawiając cudze. To jest w porządku, ponieważ w dłuższym okresie przynosi efekty: ostrzeżenia zwykle są poprawiane nie pojedynczo, lecz od razu grupą określonego typu, a wszystkie łatwe do usunięcia ostrzeżenia dość szybko zostają wyeliminowane.
Na tym wykresie przedstawiono całkowitą liczbę ostrzeżeń Checkstyle w ciągu pół roku pracy takiego „mechanizmu zębatego” na . Liczba ostrzeżeń zmniejszyła się o rząd wielkości, i to w sposób naturalny, równolegle z rozwojem produktu!

Stosuję zmodyfikowaną wersję tej metody, osobno zliczając ostrzeżenia w podziale na moduły projektu i narzędzia analityczne; generowany przy tym plik YAML z metadanymi o kompilacji wygląda mniej więcej tak:
celesta-sql:
checkstyle: 434
spotbugs: 45
celesta-core:
checkstyle: 206
spotbugs: 13
celesta-maven-plugin:
checkstyle: 19
spotbugs: 0
celesta-unit:
checkstyle: 0
spotbugs: 0
W każdej zaawansowanej systemie CI „mechanizm zębaty” można zaimplementować dla dowolnych narzędzi analizy statycznej, nie polegając na wtyczkach i zewnętrznych narzędziach. Każdy z analityków generuje własne raporty w prostym formacie tekstowym lub XML, łatwym do analizy. Pozostaje tylko opisać niezbędną logikę w skrypcie CI. Można zobaczyć, jak to jest zaimplementowane w naszych projektach open source na bazie Jenkins i Artifactory, lub . Oba przykłady zależą od biblioteki : metoda countWarnings() w zwykły sposób zlicza znaczniki xml w plikach generowanych przez Checkstyle i Spotbugs, a compareWarningMaps() implementuje ten właśnie mechanizm zębaty, sygnalizując błąd, gdy liczba ostrzeżeń w którejkolwiek z kategorii wzrasta.
Interesująca realizacja «zawleczki» może być użyta do analizy ortografii komentarzy, liter tekstowych i dokumentacji za pomocą aspell. Jak wiadomo, podczas sprawdzania pisowni nie wszystkie słowa nieznane standardowemu słownikowi są błędne, mogą być dodane do słownika użytkownika. Jeśli uczynimy słownik użytkownika częścią kodu źródłowego projektu, to brama jakości w zakresie pisowni może być sformułowana w następujący sposób: wykonywanie aspell z standardowym i użytkowniczym słownikiem znajdować żadnych błędów ortograficznych.
O znaczeniu ustalania wersji analizatora
Na koniec należy podkreślić, że niezależnie od sposobu, w jaki wprowadzisz analizę do swojego procesu dostarczania, wersja analizatora powinna być ustalona. Jeśli dopuścisz do samodzielnej aktualizacji analizatora, podczas budowania kolejnego pull requesta mogą „ujawnić się” nowe defekty, które nie są związane ze zmianą kodu, ale z tym, że nowy analizator po prostu potrafi znajdować więcej defektów — a to popsuć ci proces akceptacji pull requestów. Aktualizacja analizatora powinna być świadomym działaniem. Zresztą, sztywne ustalenie wersji każdej komponenty budowy to w ogóle konieczny wymóg i temat na osobną dyskusję.
Wnioski
- Analiza statyczna nie znajdzie bugów i nie poprawi jakości twojego produktu w wyniku jednorazowego zastosowania. Pozytywny efekt dla jakości daje tylko jej stałe stosowanie w procesie dostarczania.
- Znajdowanie bugów w ogóle nie jest głównym zadaniem analizy, przytłaczająca większość przydatnych funkcji dostępna jest w narzędziach opensource.
- Wprowadzaj bramy jakości na podstawie wyników analizy statycznej już na samym początku procesu dostarczania, korzystając z «zawleczki» dla kodu legacy.
Linki
- referat o różnych metodach analizy kodu (nie tylko statycznej!)
Źródło: habr.com
