Zaimplementuj analizę statyczną w procesie, zamiast szukać w niej błędów

Do napisania tego artykułu zainspirowała mnie duża liczba materiałów dotyczących analizy statycznej, które są coraz częściej spotykane. Po pierwsze, to Blog studia PVS, która aktywnie promuje się na Habré, publikując recenzje błędów znalezionych przez ich narzędzie w projektach open source. Niedawno wdrożone studio PVS Wsparcie dla Javy, i oczywiście twórcy IntelliJ IDEA, których wbudowany analizator jest prawdopodobnie najbardziej zaawansowany obecnie dla Javy, nie mógł trzymać się z daleka.

Czytając takie recenzje, ma się wrażenie, że mówimy o magicznym eliksirze: naciśnij przycisk, a oto lista wad na twoich oczach. Wydaje się, że w miarę doskonalenia analizatorów automatycznie będzie pojawiać się coraz więcej błędów, a produkty skanowane przez te roboty będą stawały się coraz lepsze, bez żadnego wysiłku z naszej strony.

Ale nie ma magicznych eliksirów. Chciałbym porozmawiać o tym, o czym zwykle nie mówi się w postach typu „oto, co nasz robot potrafi znaleźć”: czego nie potrafią analizatory, jaka jest ich prawdziwa rola i miejsce w procesie dostarczania oprogramowania oraz jak je poprawnie wdrożyć.

Zaimplementuj analizę statyczną w procesie, zamiast szukać w niej błędów
Ratcheta (źródło: wikipedia).

Czego nigdy nie potrafią analizatory statyczne

Czym z praktycznego punktu widzenia jest analiza kodu źródłowego? Zasilamy niektóre źródła iw krótkim czasie (dużo krótszym niż przeprowadzanie testów) uzyskujemy informacje o naszym systemie. Podstawowym i matematycznie nie do pokonania ograniczeniem jest to, że możemy w ten sposób uzyskać tylko dość wąską klasę informacji.

Najbardziej znanym przykładem problemu, którego nie można rozwiązać za pomocą analizy statycznej, jest zatrzymaj problem: jest to twierdzenie, które dowodzi, że nie jest możliwe opracowanie ogólnego algorytmu, który określałby na podstawie kodu źródłowego programu, czy będzie się on zapętlał, czy zakończy w skończonym czasie. Rozszerzeniem tego twierdzenia jest Twierdzenie Rice'a, który stwierdza, że ​​dla dowolnej nietrywialnej właściwości funkcji obliczalnych ustalenie, czy dowolny program ocenia funkcję o takiej właściwości, jest problemem nierozwiązywalnym algorytmicznie. Na przykład nie da się napisać analizatora, który z dowolnego kodu źródłowego potrafi określić, czy analizowany program jest implementacją algorytmu obliczającego, powiedzmy, liczbę całkowitą do kwadratu.

Tak więc funkcjonalność analizatorów statycznych ma ograniczenia nie do pokonania. Analizator statyczny nigdy nie będzie w stanie określić we wszystkich przypadkach takich rzeczy, jak na przykład wystąpienie „wyjątku wskaźnika zerowego” w językach dopuszczających wartości zerowe, ani we wszystkich przypadkach określić wystąpienia „nie znaleziono atrybutu” w językach z dynamiczne pisanie. Wszystko, co może zrobić najbardziej zaawansowany analizator statyczny, to wyróżnić przypadki szczególne, których liczba wśród wszystkich możliwych problemów z kodem źródłowym to bez przesady kropla w morzu potrzeb.

Analiza statyczna to nie szukanie błędów

Z powyższego wynika wniosek: analiza statyczna nie jest sposobem na zmniejszenie liczby defektów w programie. Zaryzykowałbym stwierdzenie, że przy pierwszym zastosowaniu w twoim projekcie znajdzie „zabawne” miejsca w kodzie, ale najprawdopodobniej nie znajdzie żadnych defektów, które wpływają na jakość twojego programu.

Przykłady defektów automatycznie wykrytych przez analizatory są imponujące, ale nie powinniśmy zapominać, że te przykłady zostały znalezione poprzez skanowanie dużego zestawu dużych baz kodu. Na tej samej zasadzie włamywacze, którzy są w stanie wypróbować kilka prostych haseł na dużej liczbie kont, w końcu znajdują te konta, które mają proste hasło.

Czy to oznacza, że ​​nie należy stosować analizy statycznej? Oczywiście nie! I dokładnie z tego samego powodu, dla którego warto sprawdzać każde nowe hasło, aby dostać się na listę stop „prostych” haseł.

Analiza statyczna to coś więcej niż znajdowanie błędów

W rzeczywistości problemy praktycznie rozwiązywane za pomocą analizy są znacznie szersze. W końcu generalnie analiza statyczna to wszelka kontrola kodów źródłowych przeprowadzana przed ich uruchomieniem. Oto kilka rzeczy, które możesz zrobić:

  • Sprawdzenie stylu kodowania w najszerszym tego słowa znaczeniu. Obejmuje to zarówno sprawdzanie formatowania, jak i szukanie użycia pustych/dodatkowych nawiasów, ustawianie progów dla metryk, takich jak liczba wierszy/złożoność metody cyklicznej itp. - wszystko, co potencjalnie czyni kod bardziej czytelnym i łatwiejszym w utrzymaniu. W Javie tym narzędziem jest Checkstyle, w Pythonie jest to flake8. Programy tej klasy są zwykle nazywane „linterami”.
  • Nie tylko kod wykonywalny może być analizowany. Pliki zasobów, takie jak JSON, YAML, XML, .properties mogą (i powinny!) być automatycznie sprawdzane pod kątem poprawności. Czy nie lepiej dowiedzieć się, że z powodu niektórych niesparowanych cudzysłowów struktura JSON jest zepsuta na wczesnym etapie automatycznej walidacji Pull Request niż podczas wykonywania testów lub w czasie wykonywania? Dostępne są odpowiednie narzędzia: np. YAMLlint, JSONLint.
  • Kompilacja (lub parsowanie dla dynamicznych języków programowania) jest również rodzajem analizy statycznej. Z reguły kompilatory mogą generować ostrzeżenia wskazujące na problemy z jakością kodu źródłowego i nie należy ich ignorować.
  • Czasami kompilacja to coś więcej niż kompilacja kodu wykonywalnego. Na przykład, jeśli masz dokumentację w formacie AsciiDoctor, to w momencie jego przekształcenia w obsługę HTML/PDF AsciiDoctor (Wtyczka Maven) mogą wyświetlać ostrzeżenia, na przykład o niedziałających linkach wewnętrznych. I to jest dobry powód, aby nie akceptować Pull Request ze zmianami w dokumentacji.
  • Sprawdzanie pisowni jest również rodzajem analizy statycznej. Pożytek zaklęcie potrafi sprawdzać pisownię nie tylko w dokumentacji, ale także w kodach źródłowych programów (komentarzy i literałów) w różnych językach programowania, w tym C/C++, Java i Python. Błąd ortograficzny w interfejsie użytkownika lub dokumentacji jest również wadą!
  • Testy konfiguracji (co to jest, patrz to и to raporty), chociaż są wykonywane w środowisku uruchomieniowym testów jednostkowych, takim jak pytest, w rzeczywistości są również rodzajem analizy statycznej, ponieważ nie wykonują kodów źródłowych podczas ich wykonywania.

Jak widać, znalezienie błędów na tej liście zajmuje najmniej ważną rolę, a wszystko inne jest dostępne dzięki wykorzystaniu bezpłatnych narzędzi open source.

Który z tych typów analizy statycznej powinien zostać zastosowany w Twoim projekcie? Oczywiście im więcej, tym lepiej! Najważniejsze jest prawidłowe wdrożenie, co zostanie omówione dalej.

Rurociąg dostaw jako wieloetapowy filtr i analiza statyczna jako jego pierwsza kaskada

Klasyczną metaforą ciągłej integracji jest potok (pipeline), przez który przepływają zmiany – od zmiany kodu źródłowego po dostarczenie do produkcji. Standardowa sekwencja etapów tego potoku wygląda następująco:

  1. analiza statyczna
  2. kompilacja
  3. testy jednostkowe
  4. testy integracyjne
  5. Testy interfejsu użytkownika
  6. kontrola ręczna

Zmiany odrzucone w N-tym etapie potoku nie są propagowane do etapu N+1.

Dlaczego właśnie w taki, a nie inny sposób? W testowej części potoku testerzy rozpoznają dobrze znaną piramidę testową.

Zaimplementuj analizę statyczną w procesie, zamiast szukać w niej błędów
Piramida testowa. Źródło: artykuł Martina Fowlera.

Na dole tej piramidy znajdują się testy, które są łatwiejsze do napisania, działają szybciej i nie mają tendencji do fałszywie dodatnich wyników. Dlatego powinno być ich więcej, powinny obejmować więcej kodu i być wykonywane jako pierwsze. Na szczycie piramidy jest odwrotnie, więc liczbę testów integracyjnych i UI należy ograniczyć do niezbędnego minimum. Osoba w tym łańcuchu jest najdroższym, najwolniejszym i najbardziej zawodnym zasobem, więc jest na samym końcu i wykonuje pracę tylko wtedy, gdy poprzednie etapy nie znalazły żadnych wad. Jednak według tych samych zasad rurociąg jest budowany w częściach, które nie są bezpośrednio związane z testowaniem!

Chciałbym zaproponować analogię w postaci wielostopniowego systemu filtracji wody. Na wejście doprowadzana jest woda brudna (zmiana z defektami), na wyjściu musimy dostać wodę czystą, w której eliminowane są wszelkie niepożądane zanieczyszczenia.

Zaimplementuj analizę statyczną w procesie, zamiast szukać w niej błędów
Filtr wielostopniowy. Źródło: Wikimedia Commons

Jak wiadomo, filtry czyszczące są zaprojektowane w taki sposób, że każda kolejna kaskada może odfiltrować coraz mniejszą frakcję zanieczyszczeń. Jednocześnie kaskady oczyszczania zgrubnego charakteryzują się wyższą przepustowością i niższymi kosztami. W naszej analogii oznacza to, że bramki jakości wejściowej są szybsze, wymagają mniejszego wysiłku do uruchomienia i same są bardziej bezpretensjonalne w działaniu - i dokładnie w takiej kolejności są wbudowane. Rolą analizy statycznej, która, jak teraz rozumiemy, jest w stanie usunąć tylko najbardziej rażące defekty, jest rola siatki-"błota" na samym początku kaskady filtrów.

Analiza statyczna sama w sobie nie poprawia jakości produktu końcowego, tak jak „błotna pułapka” nie czyni wody pitnej. A jednak, podobnie jak inne elementy przenośnika, jego znaczenie jest oczywiste. Chociaż w filtrze wielostopniowym stopnie wyjściowe są potencjalnie zdolne do przechwytywania wszystkiego tak samo jak stopnie wejściowe, jasne jest, do jakich konsekwencji doprowadzi próba obejścia się tylko ze stopniami dokładnego oczyszczania, bez stopni wejściowych.

Zadaniem „zbieracza błota” jest odciążenie kolejnych kaskad od wychwytywania bardzo dużych defektów. Na przykład, jako minimum, recenzent kodu nie powinien być rozpraszany przez niepoprawnie sformatowany kod i naruszenia ustalonych standardów kodowania (jak dodatkowe nawiasy lub zbyt głęboko zagnieżdżone gałęzie). Błędy takie jak NPE powinny być wychwytywane przez testy jednostkowe, ale jeśli jeszcze przed testem analizator wskaże nam, że błąd nieuchronnie musi się wydarzyć, znacznie przyspieszy to jego naprawę.

Myślę, że teraz jest jasne, dlaczego analiza statyczna nie poprawia jakości produktu, jeśli jest używana sporadycznie, a powinna być używana stale, aby odfiltrować zmiany z rażącymi defektami. Pytanie, czy użycie analizatora statycznego poprawi jakość twojego produktu, jest mniej więcej równoznaczne z pytaniem: „Czy jakość wody pobranej z brudnego stawu poprawi się, jeśli przepuści się ją przez durszlak?”

Implementacja w starym projekcie

Ważne pytanie praktyczne: jak wprowadzić analizę statyczną do procesu ciągłej integracji jako „bramę jakości”? W przypadku testów automatycznych wszystko jest oczywiste: istnieje zestaw testów, niepowodzenie któregokolwiek z nich jest wystarczającym powodem, aby sądzić, że montaż nie przeszedł przez bramkę jakości. Próba zainstalowania bramki w ten sam sposób na podstawie wyników analizy statycznej kończy się niepowodzeniem: w starszym kodzie jest zbyt wiele ostrzeżeń analitycznych, nie chcesz ich całkowicie ignorować, ale nie można też zatrzymać dostarczania produktu tylko dlatego, że zawiera ostrzeżenia analizatora.

Przy pierwszym uruchomieniu analizator generuje ogromną liczbę ostrzeżeń na każdym projekcie, z których zdecydowana większość nie jest związana z prawidłowym funkcjonowaniem produktu. Niemożliwe jest poprawienie wszystkich tych komentarzy na raz, a wiele z nich nie jest koniecznych. W końcu wiemy, że nasz produkt jako całość działa jeszcze przed wprowadzeniem analizy statycznej!

W rezultacie wiele osób ogranicza się do epizodycznego wykorzystania analizy statycznej lub używa jej tylko w trybie informacyjnym, kiedy raport analizatora jest po prostu generowany podczas budowy. Jest to równoznaczne z brakiem jakiejkolwiek analizy, bo jeśli mamy już dużo ostrzeżeń, to wystąpienie kolejnego (nieważne jak poważnego) przy zmianie kodu przechodzi niezauważone.

Znane są następujące sposoby wprowadzania bramek jakościowych:

  • Ustawia limit całkowitej liczby ostrzeżeń lub liczby ostrzeżeń podzielonej przez liczbę wierszy kodu. To nie działa dobrze, bo taka bramka swobodnie pomija zmiany z nowymi defektami, aż do przekroczenia ich limitu.
  • Naprawienie w pewnym momencie wszystkich starych ostrzeżeń w kodzie jako ignorowanych i niepowodzenie kompilacji, gdy pojawią się nowe ostrzeżenia. Tę funkcjonalność zapewnia PVS-studio i niektóre zasoby internetowe, takie jak Codacy. Nie miałem okazji pracować w PVS-studio, ponieważ jeśli chodzi o moje doświadczenie z Codacy, ich głównym problemem jest to, że definicja tego, co jest „starym”, a co „nowym” błędem, jest dość skomplikowanym algorytmem, który nie zawsze działają poprawnie, zwłaszcza jeśli pliki są mocno modyfikowane lub mają zmienioną nazwę. W mojej pamięci Codacy mógł pomijać nowe ostrzeżenia w pull request, a jednocześnie nie pomijać pull request z powodu ostrzeżeń, które nie były związane ze zmianami w kodzie tego PR.
  • Moim zdaniem najskuteczniejsze rozwiązanie jest opisane w książce Ciągłe dostawy metoda „zapinania”. Główną ideą jest to, że właściwością każdego wydania jest liczba ostrzeżeń analizy statycznej i dozwolone są tylko zmiany, które nie zwiększają całkowitej liczby ostrzeżeń.

Zapadkowy

To działa tak:

  1. W początkowej fazie implementowany jest zapis w metadanych wydania liczby ostrzeżeń w kodzie wykrytych przez analizatory. Tak więc, kiedy budujesz w górę, twój menedżer repozytorium jest napisany nie tylko „wydanie 7.0.2”, ale „wydanie 7.0.2 zawierające 100500 ostrzeżeń w stylu kontrolnym”. Jeśli korzystasz z zaawansowanego menedżera repozytoriów (takiego jak Artifactory), łatwo jest przechowywać takie metadane dotyczące Twojego wydania.
  2. Teraz każde żądanie ściągnięcia w kompilacji porównuje liczbę otrzymanych ostrzeżeń z liczbą w bieżącej wersji. Jeśli PR prowadzi do wzrostu tej liczby, to kod nie przechodzi przez bramkę jakości w analizie statycznej. Jeśli liczba ostrzeżeń maleje lub nie zmienia się, oznacza to, że przechodzi.
  3. W następnym wydaniu przeliczona liczba ostrzeżeń zostanie ponownie zapisana w metadanych wydania.

Tak więc stopniowo, ale stabilnie (jak w przypadku zapadki), liczba ostrzeżeń będzie dążyć do zera. Oczywiście system można oszukać wprowadzając nowe ostrzeżenie, ale poprawiając cudze. Jest to normalne, ponieważ na dłuższą metę daje to skutek: ostrzeżenia są z reguły naprawiane nie pojedynczo, ale od razu przez grupę określonego typu, a wszystkie łatwe do wyeliminowania ostrzeżenia są eliminowane dość szybko.

Ten wykres przedstawia łączną liczbę ostrzeżeń Checkstyle za sześć miesięcy działania takiej „zapadki”. jeden z naszych projektów open source. Liczba ostrzeżeń spadła o rząd wielkości i stało się to naturalnie, równolegle z rozwojem produktu!

Zaimplementuj analizę statyczną w procesie, zamiast szukać w niej błędów

Używam zmodyfikowanej wersji tej metody, osobno licząc ostrzeżenia według modułu projektu i narzędzia analitycznego, w wyniku czego plik YAML z metadanymi zespołu 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żdym zaawansowanym systemie CI mechanizm zapadkowy można zaimplementować w dowolnym narzędziu do analizy statycznej bez polegania na wtyczkach i narzędziach innych firm. Każdy z analizatorów tworzy raport w prostym formacie tekstowym lub XML, który jest łatwy do przeanalizowania. Pozostaje zarejestrować tylko niezbędną logikę w skrypcie CI. Możesz zobaczyć, jak to jest realizowane w naszych projektach open source opartych na Jenkins i Artifactory, możesz tutaj lub tutaj. Oba przykłady są zależne od biblioteki ratchetlib: metoda countWarnings() zlicza znaczniki xml w plikach generowanych przez Checkstyle i Spotbugs w zwykły sposób, oraz compareWarningMaps() implementuje tę samą zapadkę, zgłaszając błąd, gdy liczba ostrzeżeń w którejkolwiek z kategorii wzrośnie.

Możliwa jest interesująca implementacja mechanizmu zapadkowego do analizy pisowni komentarzy, literałów tekstowych i dokumentacji za pomocą aspell. Jak wiadomo, podczas sprawdzania pisowni nie wszystkie słowa nieznane w standardowym słowniku są błędne, można je dodać do słownika użytkownika. Jeśli uczynisz słownik użytkownika częścią kodu źródłowego projektu, wówczas bramkę jakości pisowni można sformułować w następujący sposób: wykonanie aspell ze słownikiem standardowym i słownikiem użytkownika nie powinien znaleźć żadnych błędów ortograficznych.

O znaczeniu naprawienia wersji analizatora

Podsumowując, należy zauważyć, co następuje: bez względu na to, jak zaimplementujesz analizę w swoim rurociągu dostaw, wersja analizatora musi zostać naprawiona. Jeśli pozwolisz analizatorowi na samoistne aktualizowanie się, to przy budowaniu kolejnego pull requesta mogą „pojawić się” nowe defekty, które nie są związane ze zmianami kodu, ale są związane z tym, że nowy analizator po prostu jest w stanie znaleźć więcej defektów – a to przerwie proces akceptowania żądań ściągnięcia. Modernizacja analizatora powinna być działaniem świadomym. Jednak twarda naprawa wersji każdego komponentu zespołu jest ogólnie koniecznym wymogiem i tematem na osobną dyskusję.

odkrycia

  • Analiza statyczna nie znajdzie za Ciebie błędów i nie poprawi jakości Twojego produktu w wyniku jednej aplikacji. Jedynym pozytywnym wpływem na jakość jest jego ciągłe stosowanie podczas procesu dostawy.
  • Znajdowanie błędów wcale nie jest głównym zadaniem analizy, zdecydowana większość przydatnych funkcji jest dostępna w narzędziach open source.
  • Implementuj bramki jakości w oparciu o wyniki analizy statycznej na pierwszym etapie potoku dostarczania, używając mechanizmu zapadkowego dla starszego kodu.

referencje

  1. Ciągłe dostawy
  2. A. Kudryavtsev: Analiza programu: jak zrozumieć, że jesteś dobrym programistą raport o różnych metodach analizy kodu (nie tylko statycznej!)

Źródło: www.habr.com

Dodaj komentarz