Jak dodać analizę statyczną w CMake?

ucgosu.pl 3 lat temu

Języki takie jak C i C++ zakładają, iż programista wie co robi i pozwalają mu na wiele. Są bardzo konserwatywne w zgłaszaniu błędów i warningów. Nieraz obraca się to przeciwko programiście, dlatego sami aktywujemy dodatkowe flagi warningów podczas kompilacji i używamy różnych narzędzi do analizy kodu.

Ale do narzędzi takich jak cppcheck czy clang-tidy musimy podać te same pliki źródłowe, include, define co w głównym buildzie. Konfiguracja jest dosyć trudna. Na szczęście system budowania może nam w tym pomóc. W końcu posiada wszystkie informacje przekazywane kompilatorowi. Dzisiaj zobaczymy jak skonfigurować analizę statyczną w CMake.

Clang scan-build

Zanim przejdziemy do sposobów ingerujących w skrypt budowania najpierw omówimy narzędzie, które nie wymaga żadnych zmian. Jest nim scan-build wchodzący w skład LLVM, czyli narzędzie do statycznej analizy powiązane z kompilatorem clang. Aby go użyć musimy najpierw zainstalować toolchain LLVM.

Aby uruchomić analizę statyczną musimy użyć komendy scan-build a następnie wpisać komendę kompilującą cały projekt. Na przykład:

scan-build ninja -v

Skrypt potrafi rozpoznać wywołania kompilatora gcc lub clang, przechwycić flagi i wykonać statyczną analizę.

Dzięki temu odpada nam konfiguracja wejść do analizy statycznej. Możemy więc od razu uruchomić domyślny zestaw sprawdzeń. Oczywiście każdy taki tool ma spore możliwości konfiguracji i przejrzenie dokumentacji oraz pobawienie się poszczególnymi flagami na pewno nas nie ominie, o ile chcemy to dobrze ustawić. Jednak najgorsza część – podanie plików projektu jest już zrobiona za nas.

Więcej o scan-build przeczytacie w artykule Chrisa Colemana o Clang w embedded.

Statyczna analiza w skrypcie CMake

CMake ma wbudowane wsparcie (ale dopiero po tym jak dane narzędzie zainstalujemy) dla różnych narzędzi do analizy kodu:

Mamy również flagę LINK_WHAT_YOU_USE, która jest realizowana dzięki opcji linkera. Niestety z include-what-you-use będziemy mieć problem, o ile chcemy korzystać na Windowsie, bo nie ma gotowej binarki. Jakąś opcją w takim wypadku jest uruchamianie tego narzędzia tylko na serwerze Continuous Integration.

Tak samo mamy flagi CMAKE_<LANG>_<NAZWA_TOOLA> modyfikująca domyślne ustawienia. Przy początkowych testach pewnie ta flaga będzie lepsza. Docelowo, szczególnie o ile będziemy chcieli dawać różne konfiguracje dla różnych buildów – wtedy lepiej nie modyfikować ustawień globalnie.

Ok to teraz w jaki sposób te flagi CMake działają? Zobaczmy to sobie na przykładzie cppcheck:

set(CMAKE_CXX_CPPCHECK "cppcheck")

Ta linijka dodana do skryptu CMake aktywuje cppcheck dla plików C++ (mamy oddzielną zmienną CMAKE_C_CPPCHECK dla C), a w stringu podajemy instrukcję konsolową uruchamiającą analizę.

Możemy również podać argumenty dla cppchecka po średnikach:

set(CMAKE_CXX_CPPCHECK "cppcheck;--enable=all;--force")

Albo podać tą stałą nie w skrypcie, tylko jako argument w linii komend dla cmake:

cmake .. -GNinja -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;--force"

Analogicznie wygląda to dla innych tooli. Na przykład dla clang-tidy:

set(CMAKE_C_CLANG_TIDY "clang-tidy;"-checks=-*,cert-*,clang-analyzer-*") cmake .. -GNinja -DCMAKE_C_CLANG_TIDY ="clang-tidy;"-checks=-*,cert-*,clang-analyzer-*"

Więcej na ten temat znajdziecie na blogu twórców CMake.

Zaawansowana konfiguracja

No fajnie, ale podawanie dłuższej konfiguracji w jednym stringu w linii komend, albo w jednej zmiennej CMake nie wygląda na zbyt poręczne. Dlatego w praktyce lepiej będzie nieco rozbudować nasze wsparcie dla statycznej analizy.

set(CPPCHECK_CONFIG "--enable=all" "--inconclusive" "--force" "--inline-suppr" "--output-file=cppcheck.out" ) find_program(CMAKE_C_CPPCHECK NAMES cppcheck) if (CMAKE_C_CPPCHECK) list(APPEND CMAKE_C_CPPCHECK ${CPPCHECK_CONFIG}) endif() find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck) if (CMAKE_CXX_CPPCHECK) list(APPEND CMAKE_CXX_CPPCHECK ${CPPCHECK_CONFIG}) endif()

W powyższym fragmencie skryptu umieściłem flagi cppcheck w oddzielnej zmiennej CPPCHECK_CONFIG. Następnie sprawdziłem, czy jest dostępny cppcheck i dodałem swój config cppchecka dla C i C++.

Mój config między innymi włącza wszystkie checki, włącza ingorowanie sprawdzeń w kodzie i określa plik wyjściowy z listą znalezionych problemów. W swoim projekcie możesz potrzebować bardziej szczegółowej listy aktywnych/nieaktywnych checków, czy listy ignorowanych warningów.

W każdym razie teraz konfiguracja statycznej analizy jest łatwiejsza do utrzymania. Możemy również dodać podobną konfigurację dla innych narzędzi:

set(CLANG_TIDY_CONFIG "-checks=-*,cert-*,clang-analyzer-*,performance-*,portability-*,readability-*,bugprone-*,misc-*" "--export-fixes=clang-tidy.out" ) find_program(CMAKE_C_CLANG_TIDY NAMES clang-tidy) if (CMAKE_C_CLANG_TIDY) list(APPEND CMAKE_C_CLANG_TIDY ${CLANG_TIDY_CONFIG}) endif() find_program(CMAKE_CXX_CLANG_TIDY NAMES clang-tidy) if (CMAKE_CXX_CLANG_TIDY) list(APPEND CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_CONFIG}) endif()

W powyższym fragmencie ustawiłem dla clang-tidy różne grupy checków i plik wynikowy.

Przykładowa zawartość plików z raportami z analizy może wyglądać tak dla cppcheck:

src\unity.c:2026:17: style: The scope of the variable 'ptrf' can be reduced. [variableScope] const char* ptrf; ^ src\unity.c:1980:23: style: Variable 'lnext' is assigned a value that is never used. [unreadVariable] const char* lnext = lptr; ^ src\unity.c:686:0: style: The function 'UnityAssertBits' is never used. [unusedFunction] ^ src\unity.c:1112:0: style: The function 'UnityAssertDoubleSpecial' is never used. [unusedFunction] ^ src\unity.c:1094:0: style: The function 'UnityAssertDoublesWithin' is never used. [unusedFunction]

a tak dla clang-tidy:

- DiagnosticName: readability-inconsistent-declaration-parameter-name DiagnosticMessage: Message: 'function ''UnityMessage'' has a definition with different parameter names' FilePath: "/src/unity_internals.h" FileOffset: 21469 Replacements: [] Notes: - Message: the definition seen here FilePath: "src\\unity.c" FileOffset: 59181 Replacements: [] - Message: 'differing parameters are named here: (''message''), in definition: (''msg'')' FilePath: "src/unity_internals.h" FileOffset: 21469 Replacements: - FilePath: "src/unity_internals.h" Offset: 21494 Length: 7 ReplacementText: msg Level: Warning BuildDirectory: "test\\cyclic_buffer_mock\\out" - DiagnosticName: readability-magic-numbers DiagnosticMessage: Message: '126 is a magic number; consider replacing it with a named constant' FilePath: "src\\unity.c" FileOffset: 4753 Replacements: [] Level: Warning BuildDirectory: "test\\cyclic_buffer_mock\\out"

Możemy się również pobawić sposobem wyświetlania tych danych. Czasem config pozwala na output html. Czasem chcemy to podpiąć pod jakiś plugin Jenkinsa, czasem mamy jeszcze jakieś inne opcje. W każdym razie każdy tool potrafi zwracać listę błędów w formie tekstowej i zwykle chcemy to później jakoś obrobić.

Statyczna analiza jako opcja

Po zmianach z poprzedniego rozdziału mamy już możliwość łatwego zmieniania opcji samych narzędzi do analizy statycznej. Zostaje jednak jeszcze jedna sprawa – nie zawsze chcemy ją odpalać. No bo w końcu analiza statyczna wydłuża build, kod developerski czasem zawiera tymczasowe instrukcje i chcemy mieć wybór kiedy budować z analizą, a kiedy nie.

Właśnie dlatego warto dodać do skryptu CMake opcje:

option(STATIC_ANALYSIS "Build with static analysis" OFF) if (STATIC_ANALYSIS) include(../../cmake/static-analysis.cmake) endif()

Konfigurację poszczególnych tooli do analizy umieściłem w oddzielnym pliku cmake/static-analysis.cmake i otoczyłem jego dodanie ifem.

W głównym pliku CMakeLists.txt możemy umieścić taki fragment, a następnie utworzyć build z analizą statyczną dodając tą flagę w komendzie cmake:

cmake .. -GNinja -DSTATIC_ANALYSIS=ON

Z kolei o ile nie umieścimy tej flagi – przygotujemy build bez analizy statycznej.

Możemy również pójść dalej i zrobić oddzielne opcje dla wszystkich toola do analizy.

Przykładowy kod

Przykład użycia statycznej analizy w skryptach cmake dodałem do projektu, który używałem już w poprzednich odcinkach z tej serii. Znajdziecie go na moim GitHubie – tag dla tego artykułu static-analysis.

Projekty unit testów test/cyclic_buffer oraz test/cyclic_buffer_mock załączają teraz dodatkowy plik z konfiguracją analizy statycznej zgodną z tym artykułem. Plik konfiguracyjny to cmake/static-analysis.cmake.

Podsumowanie

Dzięki wsparciu narzędzia do budowania możemy dużo łatwiej uporać się z konfiguracją analizy statycznej. Wbudowane wsparcie dla niektórych narzędzi dodatkowo ułatwia nam robotę. Dzięki temu łatwiej sforsować początkową barierę, ponieważ w wielu projektach rezygnuje się z dodatkowych narzędzi do analizy ze względu na trudności z konfiguracją.

Istnieje jeszcze opcja skonfigurowania w CMake dowolnych innych narzędzi jako komendy wywoływane z konsoli z odpowiednimi argumentami. Możemy je dodatkowo opakować w funkcje i moduły do dołączenia. Dzięki temu nie ograniczamy się tylko do tych narzędzi, które oficjalnie wspiera CMake. Jednak tutaj konfiguracja może być już trochę trudniejsza.

Jeżeli interesuje Cię temat narzędzi – koniecznie zapisz się na mój newsletter. Otrzymasz darmowy dokument opisujący różne typy narzędzi. A przy okazji nie przegapisz kolejnych artykułów!

Źródła

Blog Kitware – Static checks with CMake

Better Firmware with LLVM/Clang – Chris Coleman

Clang-tidy

Cppcheck

Idź do oryginalnego materiału