Привет, Хабр! Меня зовут Давид, еще недавно я был стажером YADRO, а сейчас работаю в отделе разработки ПО поддержки сетевой аппаратной части. У нас в команде есть большой проект на более 100 000 строк, написан на C++ (и частично на С). Код переписывался много раз, а за самим проектом на правах легаси особо никто не следил: работает — не трогай. Когда я пришел в команду, у меня была задача: привести код в порядок и отловить ошибки, которые пропускает компилятор — например, возможное разыменование нулевого указателя, неинициализированные переменные или простые опечатки. 

Одним из очевидных решений было использование статического анализатора. Выбрали довольно известное коммерческое решение, но долгие прогоны не привели ни к чему дельному. Решили поэкспериментировать с другими вариантами статических анализаторов, сделав ставку на open source. Поиски привели к инфраструктуре CodeCheсker, которая предоставляет удобный интерфейс запуска и настройки статических анализаторов через аргументы командной строки. С помощью инструмента удалось достичь результатов, которые значительно превысили значения, полученные на коммерческом решении.  

Под катом расскажу, что же такое CodeCheсker, как с ним работать и почему его точно стоит попробовать на большом проекте. 

CodeCheсker: что за инструмент и чем он хорош

CodeCheсker предоставляет удобный интерфейс запуска и конфигурации статических анализаторов на Linux или macOS. С помощью него можно запускать анализаторы кода на C/C++ — Clang-Tidy, Clang Static Analyzer, Cppcheck и GCC Static Analyzer — в любой комбинации. А еще — включать и выключать различные проверки этих анализаторов. Результаты проверок можно изучать в удобном веб-интерфейсе. 

Чем он нам понравился: 

  • С ним просто запускать анализаторы. Статических анализаторов немало, но иногда их неудобно использовать: настраивать конфигурационные файлы долго, нет простого способа запуска чекера и парсинга результатов. CodeChecker исключает эти проблемы, предоставляя удобный интерфейс запуска и конфигурации через аргументы командной строки.

  • Его просто установить. CodeChecker — это python-пакет, ставится просто и не требует никаких настроек. Можно использовать «из коробки».  

Скрытый текст

Поскольку CodeChecker — это python-пакет, устанавливается он самым обычным pip3 (сам пакет доступен на pypi): 

pip3 install codechecker

Можно установить пакет в виртуальном окружении или воспользоваться pipx, который самостоятельно создаст venv: 

pipx install CodeChecker

Еще один вариант — установка через пакетный менеджер Snap:

sudo snap install codechecker --classic

Обратите внимание, что название инструмента в консоли в таком случае нужно писать именно прописными буквами.

Еще больше способов установки (в основном напрямую) описаны в документации CodeChecker. Также его можно запустить в Docker-контейнере или в редакторе Visual Studio Code.

Запускаем анализаторы в CodeChecker

Существует несколько способов запуска анализаторов с помощью CodeChecker, но мы выберем самый простой — через передачу compilation database (файла compile_commands.json). По сути, это просто данные о структуре проекта в одном файле. Их можно передавать, например, своей IDE, если она не видит зависимостей и из-за этого работает некорректно.

Сгенерировать этот файл можно разными способами. Например, если вы используете CMake, то можно просто прописать cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON. в рабочей директории. Но у нас Linux, поэтому воспользуемся, возможно, более сложным способом — утилитой Bear, которая генерирует базу данных компиляции для инструментов clang. Чтобы сгенерить compile_commands.json, заходим в рабочую директорию и пишем: bear -- <ваша-команда-для-билда-проекта>, в нашем случае bear -- make. Готово, в этой же директории получаем compile_commands.json.

Для запуска анализаторов будем использовать команду analyze. В общей команде нужно указать следующее: 

  • путь до файла compile_commands.json,

  • названия анализаторов, которые хотим запустить, 

  • настройки чекеров,

  • папку, куда сгенерировать результат.

Файл compile_commands.json у нас уже есть, разберемся c анализаторами. 

Анализаторы

В CodeChecker доступны четыре анализатора — подробнее о каждом я расскажу позже:

  • Clang-Tidy — ставится дефолтным пакетом или вместе с Сlang,

  • Clang Static Analyzer — ставится вместе с Сlang,

  • Cppcheck — ставится дефолтным пакетом, 

  • GCC Static Analyzer — ставится вместе с GNU Compiler Collection.

GCC нужен от версии 13.0.0. Если в PATH указана другая версия, то можно выставить в переменную окружения CC_ANALYZER_BIN путь до бинарного файла с GCC нужной версии:

CC_ANALYZER_BIN='gcc:<путь-до-бинарного-файла-gcc>'

По дефолту CodeChecker запустит все доступные анализаторы. Прописать конкретный набор анализаторов можно во флаг --analyzers. Например, --analyzers gcc clangsa.

Чекеры

Чекеры — самая важная часть команды запуска, ведь они устанавливают, на что именно будет смотреть анализатор и какие ошибки подмечать.

Есть три способа включить или выключить проверку: 

  • через конкретный чекер, 

  • через группу чекеров, которые просто объединяют чекеры в одну сущность,

  • через соответствующий профиль.

Профили — это наборы групп чекеров. Полезны, потому что групп много и прописывать их всех трудозатратно. Изначально доступны три профиля: default (стандартный набор групп), sensitive (увеличенный набор групп), extreme (почти все группы включены). Но можно собирать и свои профили. Однако важно помнить, что с ростом количества проверок растет и количество false-positive срабатываний. 

Для включения/выключения чекеров используют флаги --enable и --disable и их комбинации. Так, с помощью --enable-all и --disable-all можно включить или выключить все чекеры (за исключением тех, что включаются вручную). Эти флаги применяются последовательно, так что их можно комбинировать для нужных настроек.

Например, после --disable-all --enable alpha.unix.PthreadLock включится только чекер PthreadLock на многопоточность.

Итоговая команда запуска может выглядеть так:

CodeChecker analyze ./compile_commands.json --analyzers clangsa --enable=alpha --enable=sensitive --output ./reports

Здесь мы запускаем Clang Static Analyzer в режиме sensitive с alpha-чекерами. Результаты анализа можно будет посмотреть в папке reports.

Больше про запуски анализаторов можно почитать здесь. Перед тем как парсить результат, давайте ближе познакомимся с самими анализаторами. 

Доступные анализаторы и как их лучше использовать

Clang Static Analyzer 

Из четырех анализаторов, входящих в CodeChecker, этот дал нам больше всего полезного «выхлопа», причем разностороннего. Тут и проверки использования STL (например, использование итератора, вышедшего за конец контейнера), и обнаружение дедлоков при работе с мьютексами, и проверки на NULL dereference. Чтобы посмотреть полезные чекеры и группы чекеров, доступные для этого анализатора, используйте команду: 

clang -cc1 -analyzer-checker-help

Анализатор прорабатывает код довольно быстро, даже с полным листом чекеров, — на наш проект с немалым числом строк кода он тратил 2-3 минуты. А еще он яснее всего подсказывает, где именно в коде он нашел ошибку. 

Так веб-интерфейс подсвечивает, как именно анализатор пришел к ошибке
Так веб-интерфейс подсвечивает, как именно анализатор пришел к ошибке

Кроме того, у анализатора есть уникальная проверка cross-translation unit, которая включается с помощью флага --ctu. Этот режим позволяет ему расширить область видимости анализа на соседние единицы трансляции. Подробнее про конфигурирование этого анализатора можно прочитать здесь.

В итоге большинство багов и опечаток нам помог исправить именно Clang SA.

Clang-Tidy

При использовании Clang-Tidy нужно быть готовым к километровому списку ошибок даже на дефолтных настройках чекеров. Анализатор ругается буквально на каждый чих, будь то неявный каст у целочисленных типов или неиспользованное значение snprintf(). Это не делает его бесполезным, но надо быть готовым, что результаты придется разгребать и выцеплять «пинцетом» действительно полезную информацию. 

Еще Clang-Tidy — самый долгий из анализаторов: с дефолтными чекерами проверка может занять 30 минут. А с включением всех чекеров анализ кода может зависнуть на долгое время. Например, в наших тестах он не «проснулся» даже за час.

Но если есть время, можно подключить этот анализатор. От него мы получили несколько полезных рекомендаций — в основном из разряда, где стоит «причесать» код. Иногда указывал на ошибки, но реже. Так, он обнаружил пропуски скобок в логических операциях и пропущенный break у switch.

Чекеры для Clang-Tidy можно увидеть с помощью команды:

clang-tidy --list-checks

CppCheck 

На бумаге CppCheck считают сильным анализатором, который минимизирует false-positive срабатывания. На деле он указывает на синтаксические ошибки там, где их нет, — например, при использовании varargs (variable arguments list). И в целом, дает довольно мало полезного знания о коде. 

При анализе кода нашего проекта мы отметили буквально два места, где он действительно нашел ошибку в синтаксе, связанную с форматной строкой. Все это утяжеляется тем, что работает анализатор не очень быстро. В общем, рекомендую включать, только если есть свободное время — так, на всякий случай.

GCC Static Analyzer

Этот анализатор для нашего проекта не подошел совсем, потому что GCC SА не заточен под  C++, а большая часть проекта написана именно на «плюсах». В теории код на C этот анализатор должен анализировать хорошо и выдавать что-то полезное. Если это ваш случай, проверьте ради эксперимента. Но в плюсах, увы, он ничего не замечает, причем не имеет значения, путь до какого бинарника мы ему передаем.

Бонус: чем можно заменять «слабые» статические анализаторы

Хочется упомянуть еще один статический анализатор, про который мы узнали из описания CodeChecker — Facebook* Infer (*компания Facebook принадлежит организации Meta, признанной экстремистской на территории РФ). Он не запускается из интерфейса CodeChecker, но мы решили опробовать его отдельно. В отличие от предыдущих анализаторов, он может проверять код не только на C/C++, но и на других языках — например, Java и Objective-C. 

Для запуска нам потребуется все тот же compile_commands.json. Просто прописываем: 

infer --compilation-database ./compile_commands.json

И вот, красивый прогресс-бар на время анализа скрашивает наше время ожидания. 

Этот анализатор нам понравился. Facebook Infer показал много мест с неинициализированными и неиспользованными переменными, ситуациями гонки и лишними копированиями значений. Работает быстро, примерно как ClangSA (2-3 минуты), хоть настроек у него и мало. 

Результаты можно посмотреть в .txt-файлике, сгенеренном анализаторм. А если хочется более приятного интерфейса, можно спарсить результаты Facebook Infer с помощью CodeChecker, Как парсить результаты — расскажу далее. 

Парсинг результатов

После успешного анализа результаты необходимо обработать. В CodeChecker это легко сделать с помощью команды parse:

CodeChecker parse --export html --output ./reports_html ./reports

Во флаге --export указываем формат, в котором хотим получить результат, в нашем случае это html. Также прописываем папку, куда сгенерить результаты обработки, и, конечно, указываем путь до папки, где находятся результаты анализа. 

Теперь можно посмотреть результаты и статистику по анализу на красивой html-странице. 

Это statistics.html — таблица соответствия найденных ошибок их количеству. 
Это statistics.html — таблица соответствия найденных ошибок их количеству. 
Это index.html — список конкретных ошибок с описанием и ссылкой на файл, где можно посмотреть, что именно происходит в коде.
Это index.html — список конкретных ошибок с описанием и ссылкой на файл, где можно посмотреть, что именно происходит в коде.
  • В колонке File можно нажать на соответствующую запись — откроется вкладка с кодом. В ней будет поэтапно показано, что не понравилось анализатору и приводит к ошибке. 

  • В колонке Severity указывается степень «тяжести» той или иной ошибки. 

  • В колонке Message описана проблема, иногда с указанием конкретных переменных/структур. 

Результаты анализа можно отсортировать по этим колонкам — например, если хотим группами рассматривать отчеты одной и той же проверки. 

Перед повторным запуском советуем удалить или переместить папку со старым результатом в другую директорию. На время анализа это не повлияет, зато на выходе будет актуальное состояние кода. Если этого не сделать, CodeChecker будет добавлять результаты в старый файл и не уберет уже исправленные ошибки.

Вместо заключения

Статические анализаторы в составе CodeChecker помогли нам найти и исправить более 300 ошибок (без учета false-positive результатов). Для сравнения, коммерческое решение подсветило около 120 проблем, 99% из которых false-positive. А интерфейс инструмента в разы облегчил их использование.

Если хотите максимальной пользы для проекта в короткие сроки, рекомендую запустить ClangSA на sensitive/extreme настройках, а потом дополнительно прогнать код через Facebook Infer. Если есть время и хочется немного «причесать» код, можно подключить и остальные анализаторы — Clang-Tidy, CppCheck и даже GCC SA. Главное, что все это не требует ни покупки лицензий, ни долгих настроек.

Комментарии (5)


  1. lazy_val
    18.09.2024 09:50
    +1

    есть большой проект на более 100 000 строк

    работает — не трогай

    Зачем в итоге понадобилось что-то "анализировать"? Оно работать перестало?


    1. Nullix
      18.09.2024 09:50
      +1

      работает — не трогай

      Возможно оно работает, только потому что-то никто пока не использовал какой-то ультра редкий кейс приводящий к ошибкам. Но при этом, неплохо было бы проанализировать код, и понять что у тебя нет таких кейсов с потенциальными ошибками.


      1. lazy_val
        18.09.2024 09:50

        работает - не трогай

        ))


        1. Nullix
          18.09.2024 09:50

          Но ведь этот принцип "осуждается" в статье.

          за самим проектом на правах легаси особо никто не следил: работает — не трогай


          Никто не следил за проектом, так как все жили по правилу "работает - не трогай", а надо бы и за легаси кодом последить. Вот и решили анализировать.


  1. SIISII
    18.09.2024 09:50
    +1

    Вообще, на такие вещи, как отсутствие break, компиляторы без всяких анализаторов вполне себе ругаться умеют -- надо лишь компилировать со всеми предупреждениями, превращёнными в ошибки (что, естественно, не отменяет пользу от анализаторов -- но не для таких элементарных ошибок, как мне кажется)