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 -vSkrypt 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:
- cppcheck dzięki flagi <LANG>_CPPCHECK
- clang-tidy dzięki flagi <LANG>_CLANG_TIDY
- cpplint dzięki flagi <LANG>_CPPLINT
- include-what-you-use dzięki flagi <LANG>_INCLUDE_WHAT_YOU_USE
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=ONZ 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