статический код что это
Статический и динамический код в пультах
Сигнал между брелоком и автоматическими воротами является основой любой современной охранной системы, позволяющей водителям дистанционно открывать гараж или шлагбаум. Радиосигнал каждого дистанционного устройства свободно распространяется во все стороны. В связи с этим его воспринимают не только ваши ворота, но и соседские. Для того чтоб не возникало неразберихи, и пульт приводил в действие только вашу автоматику, сигналы кодируют.
Данные по любому из радиоканалов передаются в виде определенной последовательности, которая носит название пакет данных. Каждый статический и динамический код в пультах несет в себе определенную команду – «поставить на охрану», «закрыть замки», «привести в действие автоматику для открытия». Разберем подробнее, что собой представляет каждый из кодированных сигналов
Статический код
Такую систему кодирования имели самые первые пульты от ворот. Суть работы такого радиосигнала заключался в применении определенных пакетов данных для каждой из команд, которые приводились в действие нажатием той или другой кнопки на брелоке. Определенным недостатком, который проявлялся сразу после начала массовой эксплуатации подобных устройств, стала статистическая вероятность, что свой пульт для шлагбаума мог открыть соседскую дверь. Т.к. всего заложено 4096 комбинаций, по статистике это 1 случай на 1000. Однако. В силу широкой популярности такого рода пультов, подобные ситуации происходили. Кроме того, статичный сигнал оставался ничем не защищенным от код-граберов, который в то время начали массово появляться на черном рынке страны.
Динамический код и его преимущества
Рост спроса на все дистанционные системы управления заставил производителей искать новые способы кодирования сигналов, благодаря чему на рынке Москвы появился принципиально новый брелок от автоматических ворот с динамическим сигналом. Это постоянно изменяющийся пакет данных, повторение которого практически невозможно. Как сам код, так и система декодирования рассчитывается по определенному алгоритму, который закладывается производителем.
Каждое нажатие на брелок несет в себе информацию о количестве предыдущих срабатываний, на основе чего рассчитывается код, который сможет синхронизировать брелок и блок управления автоматикой.
Статический анализ кода
Примечание от переводчика. Изначально эта статья была опубликована на сайте AltDevBlogADay. Но сайт, к сожалению, прекратил своё существование. Более года эта статья оставалась недоступна читателям. Мы обратились к Джону Кармаку, и он сказал, что не против, чтобы мы разместили эту статью на нашем сайте. Что мы с удовольствием и сделали. С оригиналом статьи можно познакомится, воспользовавшись Wayback Machine — Internet Archive: Static Code Analysis.
Поскольку все статьи на нашем сайте представлены на русском и английском языке, то мы выполнили перевод статьи Static Code Analysis на русский язык. А заодно решили опубликовать её на Хабре. Здесь уже публиковался пересказ этой статьи. Но уверен, многим будет интересно прочитать именно перевод.
Самым главным своим достижением в качестве программиста за последние годы я считаю знакомство с методикой статического анализа кода и ее активное применение. Дело даже не столько в сотнях серьезных багов, не допущенных в код благодаря ей, сколько в перемене, вызванной этим опытом в моем программистском мировоззрении в отношении вопросов надежности и качества программного обеспечения.
Сразу нужно заметить, что нельзя все сводить к качеству, и признаться в этом вовсе не означает предать какие-то свои моральные принципы. Ценность имеет создаваемый вами продукт в целом, а качество кода — лишь один из ее компонентов наравне со стоимостью, функциональными возможностями и прочими характеристиками. Миру известно множество супер-успешных и уважаемых игровых проектов, напичканных багами и без конца падающих; да и глупо было бы подходить к написанию игры с той же серьезностью, с какой создают ПО для космических шаттлов. И все же качество — это, несомненно, важный компонент.
Я всегда старался писать хороший код. По своей натуре я похож на ремесленника, которым движет желание непрерывно что-то улучшать. Я прочел груды книг со скучными названиями глав типа «Стратегии, стандарты и планы качества», а работа в Armadillo Aerospace открыла мне дорогу в совершенно иной, отличный от предшествующего опыта мир разработки ПО с повышенными требованиями к безопасности.
Более десяти лет назад, когда мы занимались разработкой Quake 3, я купил лицензию на PC-Lint и пытался применять его в работе: привлекала идея автоматического обнаружения дефектов в коде. Однако необходимость запуска из командной строки и просмотра длинных списков диагностических сообщений отбили у меня охоту пользоваться этим инструментом, и я вскоре отказался от него.
С тех пор и количество программистов, и размер кодовой базы выросли на порядок, а акцент в программировании сместился с языка C на C++. Все это подготовило намного более благодатную почву для программных ошибок. Несколько лет назад, прочитав подборку научных статей о современном статическом анализе кода, я решил проверить, как изменилось положение дел в этой области за последние десять лет с тех пор, как я попробовал работать с PC-Lint.
На тот момент код у нас компилировался на 4-ом уровне предупреждений, при этом выключенными мы оставляли лишь несколько узкоспециальных диагностик. С таким подходом — заведомо рассматривать каждое предупреждение, как ошибку — программисты были вынуждены неукоснительно придерживаться данной политики. И хотя в нашем коде можно было отыскать несколько пыльных уголков, в которых с годами скопился всякий «мусор», в целом он был довольно современным. Мы считали, что у нас вполне неплохая кодовая база.
Coverity
Началось все с того, что я связался с Coverity и подписался на пробную диагностику нашего кода их инструментом. Это серьезная программа, стоимость лицензии зависит от общего количества строк кода, и мы остановились на цене, выраженной пятизначным числом. Показывая нам результаты анализа, эксперты из Coverity отметили, что наша база оказалась одной из самых чистых в своей «весовой категории» из всех, что им доводилось видеть (возможно, они говорят это всем клиентам, чтобы приободрить их), однако отчет, который они нам передали, содержал около сотни проблемных мест. Такой подход сильно отличался от моего предыдущего опыта работы с PC-Lint. Соотношение сигнал/шум в данном случае оказался чрезвычайно высок: большинство из выданных Coverity предупреждений действительно указывали на явно некорректные участки кода, которые могли иметь серьезные последствия.
Этот случай буквально открыл мне глаза на статический анализ, но высокая цена всего удовольствия некоторое время удерживала от покупки инструмента. Мы подумали, что в оставшемся до релиза коде у нас будет не так много ошибок.
Microsoft /analyze
Не исключено, что я, в конце концов, решился бы купить Coverity, но пока я размышлял над этим, Microsoft пресекли мои сомнения, реализовав новую функцию /analyze в 360 SDK. /Analyze прежде был доступен в качестве компонента топовой, безумно дорогой версии Visual Studio, а потом вдруг достался бесплатно каждому разработчику под xbox 360. Я так понимаю, о качестве игр на 360-й платформе Microsoft печется больше, чем о качестве ПО под Windows. 🙂
С технической точки зрения анализатор Microsoft всего лишь проводит локальный анализ, т.е. он уступает глобальному анализу Coverity, однако когда мы его включили, он вывалил горы сообщений — намного больше, чем выдал Coverity. Да, там было много ложных срабатываний, но и без них нашлось немало всяких страшных, по-настоящему жутких бяк.
Я потихоньку приступил к правке кода — прежде всего, занялся своим собственным, затем системным, и, наконец, игровым. Работать приходилось урывками в свободное время, так что весь процесс затянулся на пару месяцев. Однако эта задержка имела и свой побочный полезный эффект: мы убедились, что /analyze действительно отлавливает важные дефекты. Дело в том, что одновременно с моими правками наши разработчики устроили большую многодневную охоту за багами, и выяснилось, что каждый раз они нападали на след какой-нибудь ошибки, уже помеченной /analyze, но еще не исправленной мной. Помимо этого, были и другие, менее драматичные, случаи, когда отладка приводила нас к коду, уже помеченному /analyze. Все это были настоящие ошибки.
В конце концов, я добился, чтобы весь использованный код скомпилировался в запускаемый файл под 360-ю платформу без единого предупреждения при включенном /analyze, и установил такой режим компиляции в качестве стандартного для 360-сборок. После этого код у каждого программиста, работающего на той же платформе, всякий раз при компиляции проверялся на наличие ошибок, так что он мог сразу править баги по мере их внесения в программу вместо того, чтобы потом ими занимался я. Конечно, из-за этого процесс компиляции несколько замедлился, но /analyze — однозначно самый быстрый инструмент из всех, с которыми мне доводилось иметь дело, и, поверьте мне, оно того стоит.
Однажды мы в каком-то проекте случайно выключили статический анализ. Прошло несколько месяцев, и когда я заметил это и снова включил его, инструмент выдал кучу новых предупреждений об ошибках, внесенных в код за это время. Подобным же образом программисты, работающие только под PC или PS3, вносят в репозиторий код с ошибками и пребывают в неведении, пока не получат письмо с отчетом о «неудачной 360-сборке». Эти примеры наглядно демонстрируют, что в процессе своей повседневной деятельности разработчики раз за разом совершают ошибки определенных видов, и /analyze надежно уберегал нас от большей их части.
PVS-Studio
Поскольку мы могли использовать /analyze только на 360-коде, большой объем нашей кодовой базы по-прежнему оставался не покрытым статическим анализом — это касалось кода под платформы PC и PS3, а также всех программ, работающих только на PC.
Следующим инструментом, с которым я познакомился, был PVS-Studio. Он легко интегрируется в Visual Studio и предлагает удобный демо-режим (попробуйте сами!). В сравнении с /analyze PVS-Studio ужасно медлителен, но он сумел выловить некоторое количество новых критических багов, причем даже в том коде, который был уже полностью вычищен с точки зрения /analyze. Помимо очевидных ошибок PVS-Studio отлавливает множество других дефектов, которые представляют собой ошибочные программистские клише, пусть и кажущиеся на первый взгляд нормальным кодом. Из-за этого практически неизбежен некоторый процент ложных срабатываний, но, черт возьми, в нашем коде такие шаблоны нашлись, и мы их поправили.
На сайте PVS-Studio можно найти большое количество замечательных статей об инструменте, и многие из них содержат примеры из реальных open-source проектов, иллюстрирующие конкретно те виды ошибок, о которых идет речь в статье. Я думал, не вставить ли сюда несколько показательных диагностических сообщений, выдаваемых PVS-Studio, но на сайте уже появились намного более интересные примеры. Так что посетите страничку и посмотрите сами. И да — когда будете читать эти примеры, не надо ухмыляться и говорить, что вы бы так никогда не написали.
PC-Lint
В конце концов, я вернулся к варианту с использованием PC-Lint в связке с Visual Lint для интеграции в среду разработки. В соответствии с легендарной традицией мира Unix инструмент можно настроить на выполнение практически любой задачи, однако интерфейс его не очень дружественен и его нельзя просто так «взять и запустить». Я приобрел набор из пяти лицензий, но его освоение оказалось настолько трудоемким, что, насколько я знаю, все остальные разработчики от него в итоге отказались. Гибкость действительно имеет свои преимущества — так, например, мне удалось настроить его для проверки всего нашего кода под платформу PS3, хотя это и отняло у меня немало времени и усилий.
И снова в том коде, который был уже чист с точки зрения /analyze и PVS-Studio, нашлись новые важные ошибки. Я честно старался вычистить его так, чтобы и lint не ругался, но не удалось. Я поправил весь системный код, но сдался, когда увидел, сколько предупреждений он выдал на игровой код. Я рассортировал ошибки по классам и занялся наиболее критичными из них, игнорируя массу других, относящихся больше к стилистических недоработкам или потенциальным проблемам.
Я полагаю, что попытка исправить громадный объем кода по максимуму с точки зрения PC-Lint заведомо обречена на провал. Я написал некоторое количество кода с нуля в тех местах, где послушно старался избавиться от каждого назойливого «линтовского» комментария, но для большинства опытных C/C++-программистов такой подход к работе над ошибками — уже чересчур. Мне до сих пор приходится возиться с настройками PC-Lint, чтобы подобрать наиболее подходящий набор предупреждений и выжать из инструмента максимум пользы.
Выводы
Я немало узнал, пройдя через все это. Боюсь, что кое-какие из моих выводов будут с трудом восприняты людьми, которым не приходилось лично разбирать сотни сообщений об ошибках в сжатые сроки и всякий раз чувствовать дурноту, приступая к их правке, и стандартной реакцией на мои слова будет «ну у нас-то все в порядке» или «все не так плохо».
Первый шаг на этом пути — честно признаться себе, что ваш код кишит багами. Для большинства программистов это горькая пилюля, но, не проглотив ее, вы поневоле будете воспринимать любое предложение по изменению и улучшению кода с раздражением, а то и нескрываемой враждебностью. Вы должны захотеть подвергнуть свой код критике.
Автоматизация необходима. Когда видишь сообщения о чудовищных сбоях в автоматических системах, невозможно не испытать эдакое злорадство, однако на каждую ошибку в автоматизации приходится легион ошибок человеческих. Призывы к тому, чтобы «писать более качественный код», благие намерения о проведении большего числа сеансов инспекции кода, парного программирования и так далее просто-напросто не действуют, особенно когда в проект вовлечены десятки людей и работать приходится в дикой спешке. Громадная ценность статического анализа заключается в возможности при каждом запуске находить хотя бы небольшие порции ошибок, доступных этой методике.
Я обратил внимание, что с каждым обновлением PVS-Studio находил в нашем коде все новые и новые ошибки благодаря новым диагностикам. Отсюда можно заключить, что при достижении кодовой базой определенного размера в ней, похоже, заводятся все допустимые с точки зрения синтаксиса ошибки. В больших проектах качество кода подчиняется тем же статистическим закономерностям, что и физические свойства вещества — дефекты в нем распространены повсюду, и вы можете только стараться свести к минимуму их воздействие на пользователей.
Инструменты статического анализа вынуждены работать «с одной рукой, связанной за спиной»: им приходится делать выводы на основе разбора языков, которые вовсе не обязательно предоставляют информацию для таких выводов, и в целом делать очень осторожные предположения. Поэтому вы должны помогать своему анализатору, насколько возможно — отдавать предпочтение индексации перед арифметикой с указателями, держать граф вызовов в едином исходном файле, использовать явные аннотации и т.п. Все, что может показаться статическому анализатору неочевидным, почти наверняка собьет с толку и ваших коллег-программистов. Характерное «хакерское» отвращение к языкам со строгой статической типизацией («bondage and discipline languages») на деле оказывается недальновидным: потребности крупных, долгоживущих проектов, в разработку которых вовлечены большие команды программистов, кардинально отличаются от мелких и быстрых задач, выполняемых для себя.
Нулевые указатели — это самая насущная проблема в языке C/C++, по крайней у нас. Возможность двойственного использования единого значения в качестве как флага, так и адреса приводит к невероятному числу критических ошибок. Поэтому всегда, когда для этого есть возможность, в C++ следует отдавать предпочтение ссылкам, а не указателям. Хотя ссылка «на самом деле» есть ни что иное как тот же указатель, она связана неявным обязательством о невозможности равенства нулю. Выполняйте проверки указателей на ноль, когда они превращаются в ссылки — это позволит вам впоследствии забыть о данной проблеме. В сфере игростроения существует множество глубоко укоренившихся программистских шаблонов, несущих потенциальную опасность, но я не знаю способа, как полностью и безболезненно перейти от проверок на ноль к ссылкам.
Второй по важности проблемой в нашей кодовой базе были ошибки с printf-функциями. Она дополнительно усугублялась тем, что передача idStr вместо idStr::c_str() практически каждый раз заканчивалась падением программы. Однако, когда мы стали использовать аннотации /analyze для функций с переменным количеством аргументов, чтобы поверки типов выполнялись корректно, проблема была решена раз и навсегда. В полезных предупреждениях анализатора мы встречали десятки таких дефектов, которые могли привести к падению, случись какому-нибудь ошибочному условию запустить соответствующую ветку кода — это, между прочим, говорит еще и том, как мал был процент покрытия нашего кода тестами.
Многие серьезные баги, о которых сообщал анализатор, были связаны с модификациями кода, сделанными долгое время спустя после его написания. Невероятно распространенный пример — когда идеальный код, в котором раньше указатели проверялись на ноль до выполнения операции, изменялся впоследствии таким образом, что указатели вдруг начинали использоваться без проверки. Если рассматривать эту проблему изолированно, то можно было бы пожаловаться на высокую цикломатическую сложность кода, однако если разобраться в истории проекта, выяснится, что причина скорее в том, что автор кода не сумел четко донести предпосылки до программиста, который позже отвечал за рефакторинг.
Человек по определению не способен удерживать внимание на всем сразу, поэтому в первую очередь сосредоточьтесь на коде, который будете поставлять клиентам, а коду для внутренних нужд уделяйте меньшее внимание. Активно переносите код из базы, предназначенной для продажи, во внутренние проекты. Недавно вышла статья, где рассказывалось, что все метрики качества кода во всем своем многообразии практически так же идеально коррелируют с размером кода, как и коэффициент ошибок, что позволяет по одному только размеру кода с высокой точностью предсказать количество ошибок. Так что сокращайте ту часть своего кода, которая критична с точки зрения качества.
Если вас не напугали до глубины души все те дополнительные трудности, которые несет в себе параллельное программирование, вы, похоже, просто не вникли в этот вопрос как следует.
Невозможно провести достоверные контрольные испытания при разработке ПО, но наш успех от использования анализа кода был настолько отчетливым, что я могу позволить себе просто заявить: не использовать анализ кода — безответственно! Автоматические консольные логи о падениях содержат объективные данные, которые ясно показывают, что Rage, даже будучи по многим показателям первопроходцем, оказался намного стабильнее и здоровее, чем большинство самых современных игр. Запуск Rage на PC, к сожалению, провалился — готов поспорить, что AMD не используют статический анализ при разработке своих графических драйверов.
Вот вам готовый рецепт: если в вашей версии Visual Studio есть встроенный /analyze, включите его и попробуйте поработать так. Если бы меня попросили выбрать из множества инструментов один, я бы остановился именно на этом решении от Microsoft. Всем остальным, кто работает в Visual Studio, я советую хотя бы попробовать PVS-Studio в демо-режиме. Если вы разрабатываете коммерческое ПО, приобретение инструментов статического анализа будет одним из лучших способов вложения средств.
И напоследок комментарий из твиттера:
Дэйв Ревелл @dave_revell Чем больше я применяю статический анализ на своем коде, тем больше недоумеваю, как компьютеры вообще запускаются.
Статический анализ PHP-кода на примере PHPStan, Phan и Psalm
Компания Badoo существует уже более 12 лет. У нас очень много PHP-кода (миллионы строк) и наверняка даже сохранились строки, написанные 12 лет назад. У нас есть код, написанный ещё во времена PHP 4 и PHP 5. Мы выкладываем код два раза в день, и каждая выкладка содержит примерно 10—20 задач. Помимо этого, программисты могут выкладывать срочные патчи — небольшие изменения. И в день таких патчей у нас набирается пара десятков. В общем, наш код меняется очень активно.
Мы постоянно ищем возможности как для ускорения разработки, так и для повышения качества кода. И вот однажды мы решили внедрить статический анализ кода. Что из этого получилось, читайте под катом.
Strict types: почему мы пока его не используем
Однажды у нас в корпоративном PHP-чатике развернулась дискуссия. Один из новых сотрудников рассказал, как на предыдущем месте работы они внедрили обязательный strict_types + скалярные type hints для всего кода — и это значительно снизило количество багов на продакшене.
Большинство старожилов чата было против такого нововведения. Основной причиной было то, что у PHP нет компилятора, который бы на этапе компиляции проверял соответствие всех типов в коде, и если у вас не 100%-ное покрытие кода тестами, то всегда есть риск, что ошибки всплывут на продакшене, чего мы не хотим допускать.
Конечно же, strict_types найдёт определённый процент багов, вызванных несоответствием типов и тем, как PHP «молча» конвертирует типы. Но многие опытные PHP-программисты уже знают, как работает система типов в PHP, по каким правилам происходит конвертация типов, и в большинстве случае пишут корректный, работающий код.
Но сама идея иметь некую систему, показывающую, где в коде есть несовпадение типов, нам понравилась. Мы задумались об альтернативах strict_types.
Сначала мы даже хотели пропатчить PHP. Нам хотелось, чтобы если функция принимает какой-то скалярный тип (скажем, int), а на вход пришёл другой скалярный тип (например, float), то не кидался бы TypeError (который по сути своей исключение), а происходила бы конвертация типа, а также логирование этого события в error.log. Это позволило бы нам найти все места, где наши предположения о типах неверные. Но такой патч нам показался делом рискованным, да ещё могли возникнуть проблемы с внешними зависимостями, не готовыми к такому поведению.
Мы отказались от идеи пропатчить PHP, но по времени всё это совпало с первыми релизами статического анализатора Phan, первые коммиты в котором были сделаны самим Расмусом Лердорфом. Так мы пришли к идее попробовать статические анализаторы кода.
Что такое статический анализ кода
Статические анализаторы кода просто читают код и пытаются найти в нём ошибки. Они могут выполнять как очень простые и очевидные проверки (например, на существование классов, методов и функций, так и более хитрые (например, искать несоответствие типов, race conditions или уязвимости в коде). Ключевым является то, что анализаторы не выполняют код — они анализируют текст программы и проверяют её на типичные (и не очень) ошибки.
Наиболее очевидным примером статического анализатора PHP-кода являются инспекции в PHPStorm: когда вы пишете код, он подсвечивает неправильные вызовы функций, методов, несоответствие типов параметров и т. п. При этом PHPStorm не запускает ваш PHP-код — он его только анализирует.
Замечу, что в данной статье речь идёт именно об анализаторах, которые ищут ошибки в коде. Есть и другой класс анализаторов — они проверяют стиль написания кода, цикломатическую сложность, размеры методов, длину строк и т. п. Такие анализаторы мы здесь не рассматриваем.
Хотя не всё, что находят рассматриваемые нами анализаторы, является именно ошибкой. Под ошибкой я имею ввиду код, который создаст Fatal на продакшене. Очень часто, то, что находят анализаторы, — это скорее неточность. Например, в PHPDoc может быть указан неправильный тип параметра. На работу кода эта неточность не влияет, но впоследствии код будет эволюционировать — другой программист может допустить ошибку.
Существующие анализаторы PHP-кода
Существует три популярных анализатора PHP-кода:
Со стороны пользователя все три анализатора одинаковы: вы устанавливаете их (скорее всего, через Composer), конфигурируете, после чего можно запустить анализ всего проекта или группы файлов. Как правило, анализатор умеет красиво выводить результаты в консоль. Также можно выводить результаты в формате JSON и использовать их в CI.
Все три проекта сейчас активно развиваются. Их maintainer-ы очень активно отвечают на issues в GitHub. Зачастую в первые сутки после создания тикета на него как минимум реагируют (комментируют или ставят тег типа bug/enhancement). Многие найденные нами баги были исправлены в течение пары дней. Но особенно мне нравится то, что maintainer-ы проектов активно между собой общаются, репортят друг другу баги, отправляют pull requests.
Мы внедрили и используем все три анализатора. У каждого есть свои нюансы, свои баги. Но использование трёх анализаторов одновременно облегчает понимание того, где реальная проблема, а где — ложное срабатывание.
Что умеют анализаторы
У анализаторов много общих возможностей, поэтому сначала рассмотрим, что умеют они все, а затем перейдём к особенностям каждого из них.
Стандартные проверки
Конечно же, анализаторы осуществляют все стандартные проверки кода на предмет того, что:
На первый взгляд может показаться, что хорошие программисты таких ошибок не делают, но иногда мы торопимся, иногда копипастим, иногда мы просто невнимательны. И вот в таких случаях эти проверки очень сильно спасают.
Проверки типов данных
Конечно же, статические анализаторы осуществляют и стандартные проверки, касающиеся типов данных. Если в коде написано, что функция принимает, скажем, int, то анализатор проверит, нет ли мест, где бы в эту функцию передавался объект. У большинства анализаторов можно настроить строгость проверки и имитировать strict_types: проверять, что в эту функцию не передаются строки или Boolean.
Кроме стандартных проверок, анализаторы ещё много чего умеют
Во всех анализаторах поддерживается концепция Union types. Допустим, у вас есть функция типа:
Средствами PHP такой тип параметра функции описать нельзя. Но в PHPDoc это возможно, и многие редакторы (например, PHPStorm) его понимают.
В статических анализаторах такой тип называется union type, и они очень хорошо умеют проверять такие типы данных. Например, если вышеупомянутую функцию мы написали бы так (без проверки на Boolean ):
анализаторы бы увидели, что в strtoupper может прийти либо строка, либо Boolean, и вернули бы ошибку — в strtoupper нельзя передавать Boolean.
Этот тип проверок помогает программистам правильно обрабатывать ошибки или ситуации, когда функция не может вернуть данные. Мы ведь часто пишем функции, которые могут вернуть какие-то данные или null :
В самом языке PHP довольно много функций, которые могут вернуть либо какое-то значение, либо false. Если бы мы писали такую функцию, то как бы мы задокументировали её тип?
Очень часто массивы в PHP используются как тип record — структуру с чётким списком полей, где каждое поле имеет свой тип. Конечно, многие программисты уже используют для этого классы. Но у нас в Badoo много legacy-кода, и там активно используются массивы. А ещё бывает, что программисты ленятся заводить отдельный класс для какой-то разовой структуры, и в таких местах также часто используют массивы.
Проблема таких массивов заключается в том, что чёткого описания этой структуры (списка полей и их типов) в коде нет. Программисты могут делать ошибки, работая с такой структурой: забывать обязательные поля или добавлять «левые» ключи, ещё больше запутывая код.
Анализаторы позволяют заводить описание таким структурам:
Если не описывать типы, то анализаторы будут пытаться «угадать» структуру массива, но, как показывает практика, с нашим кодом у них это не очень получается. 🙂
У этого подхода есть один недостаток. Допустим, у вас есть структура, которая активно используется в коде. Нельзя в одном месте объявить некоторый псевдотип и потом везде его использовать. Вам придётся везде в коде прописать PHPDoc с описанием массива, что очень неудобно, особенно если в массиве много полей. Также проблематично будет потом редактировать этот тип (добавлять и удалять поля).
Описание типов ключей массивов
В PHP ключами массива могут быть целые числа и строки. Иногда типы могут быть важны для статического анализа (да и для программистов). Статические анализаторы позволяют описывать ключи массива в PHPDoc:
PHPStorm поддерживает такой формат описания массивов начиная с версии 2018.3.
Своё пространство имён в PHPDoc
PHPStorm (да и другие редакторы) и статические анализаторы могут по-разному понимать PHPDoc. Например, анализаторы поддерживают вот такой формат:
А PHPStorm его не понимает. Но мы можем написать так:
Проверки, связанные с особенностями PHP
Этот тип проверок лучше пояснить на примере.
Все ли мы знаем, что может вернуть функция explode()? Если бегло посмотреть документацию, кажется, что она возвращает array. Но если изучить более внимательно, то мы увидим, что она может вернуть ещё и false. На самом деле, она может вернуть и null и ошибку, если передать ей неправильные типы, но передача неправильного значения с неправильным типом данных — это уже ошибка, поэтому этот вариант нас сейчас не интересует.
Формально с точки зрения анализатора, если функция может вернуть false или массив, то, скорее всего, потом в коде должна быть проверка на false. Но функция explode() возвращает false, только если разделитель (первый параметр) равен пустой строке. Зачастую он явно прописан в коде, и анализаторы могут проверить, что он не пуст, а значит, в данном месте функция explode() точно возвращает массив и проверка на false не нужна.
Таких особенностей у PHP не так уж мало. Анализаторы постепенно добавляют соответствующие проверки или совершенствуют их, и нам, программистам, уже не надо запоминать все эти особенности.
Переходим к описанию конкретных анализаторов.
PHPStan
Разработка некоего Ondřej Mirtes из Чехии. Активно разрабатывается с конца 2016 года.
Чтобы начать использовать PHPStan, нужно:
(вместо src может быть список конкретных файлов, которые вы хотите проверить).
Поскольку мы не используем Laravel, Symfony, Doctrine и подобные решения и у нас в коде довольно редко используются магические методы, основная фича PHPStan у нас оказалась невостребованной. ;( К тому же из-за того, что PHPStan include-ит все проверяемые классы, иногда его анализ просто не работает на нашей кодовой базе.
Тем не менее для нас PHPStan остаётся полезным:
Update:
Автор PHPStan Ondřej Mirtes тоже прочитал нашу статью и подсказал нам, что PhpStan, также как и Psalm, имеет сайт с «песочницей»: https://phpstan.org/. Это очень удобно для баг-репортов: воспроизводишь ошибку в и даёшь ссылку в GitHub.
Разработка компании Etsy. Первые коммиты от Расмуса Лердорфа.
Из рассматриваемой тройки Phan — единственный настоящий статический анализатор (в том плане, что он не исполняет никакие ваши файлы — он парсит всю вашу кодовую базу, а затем анализирует то, что вы скажете). Даже для анализа нескольких файлов в нашей кодовой базе ему требуется порядка 6 Гб оперативной памяти, и занимает этот процесс четыре—пять минут. Но зато полный анализ всей кодовой базы занимает примерно шесть—семь минут. Для сравнения, Psalm анализирует её за несколько десятков минут. А от PHPStan мы вообще не смогли добиться полного анализа всей кодовой базы из-за того, что он include-ит классы.
Впечатление от Phan двоякое. С одной стороны, это наиболее качественный и стабильный анализатор, он многое находит и с ним меньше всего проблем, когда надо проанализировать всю кодовую базу. С другой стороны, у него есть две неприятные особенности.
Под капотом Phan использует расширение php-ast. По-видимому, это одна из причин того, что анализ всей кодовой базы проходит относительно быстро. Но php-ast показывает внутреннее представление AST-дерева так, как оно отображается в самом PHP. А в самом PHP AST-дерево не содержит информации о комментариях, которые расположены внутри функции. То есть, если вы написали что-то вроде:
Вторая неприятная особенность состоит в том, что Phan плохо анализирует свойства объектов. Вот пример:
В этом примере Phan скажет вам, что в strpos вы можете передать null. Подробнее об этой проблеме можно узнать здесь: https://github.com/phan/phan/issues/204.
Плагины
У Phan хорошо проработанный API для разработки плагинов. Можно добавлять свои проверки, улучшать выведение типов для вашего кода. У этого API есть документация, но особенно классно, что внутри уже есть готовые рабочие плагины, которые можно использовать как примеры.
У нас часто используются фабричные методы, которые на вход принимают константу и по ней создают некоторый объект. Зачастую код выглядит примерно так:
Пример этого плагина более сложный. Но в коде Phan есть хороший пример в vendor/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php.
В целом, мы очень довольны анализатором Phan. Перечисленные выше false-positive мы частично (в простых случаях, с простым кодом) научились фильтровать. После этого Phan стал почти эталонным анализатором. Однако необходимость сразу парсить всю кодовую базу (время и очень много памяти) по-прежнему усложняет процесс его внедрения.
Psalm
Psalm — разработка компании Vimeo. Честно говоря, я даже не знал, что в Vimeo используется PHP, пока не увидел Psalm.
Этот анализатор — самый молодой из нашей тройки. Когда я прочитал новость о том, что Vimeo выпустила Psalm, был в недоумении: «Зачем вкладывать ресурсы в Psalm, если уже есть Phan и PHPStan?» Но выяснилось, что у Psalm есть свои полезные особенности.
Psalm пошёл по стопам PHPStan: ему тоже можно дать список файлов для анализа, и он проанализирует их, а ненайденные классы подключит автолоадом. При этом он подключает только ненайденные классы, а файлы, которые мы попросили проанализировать, не будут include-иться (в этом отличие от PHPStan). Конфиг хранится в XML-файле (для нас это скорее минус, но не очень критично).
У Psalm есть сайт с «песочницей», где можно написать код на PHP и проанализировать его. Это очень удобно для баг-репортов: воспроизводишь ошибку на сайте и даёшь ссылку в GitHub. И, кстати, на сайте описаны все возможные типы ошибок. Для сравнения: в PHPStan у ошибок нет типов, а в Phan они есть, но нет единого списка, с которым можно было бы ознакомиться.
Ещё нам понравилось, что при выводе ошибок Psalm сразу показывает строки кода, где они были найдены. Это сильно упрощает чтение отчётов.
Но, пожалуй, самой интересной особенностью Psalm являются его кастомные PHPDoc-теги, которые позволяют улучшить анализ (особенно определение типов). Перечислим наиболее интересные из них.
@psalm-ignore-nullable-return
Типы для closure
Если вы когда-нибудь интересовались функциональным программированием, то могли заметить, что там часто функция может вернуть другую функцию или принять в качестве параметра какую-то функцию. В PHP подобный стиль может сильно запутать ваших коллег, и одна из причин заключается в том, что в PHP нет стандартов документирования таких функций. Например:
Как программисту понять, какой интерфейс у функции во втором параметре? Какие параметры она должна принимать? Что она должна возвращать?
В Psalm поддерживается синтаксис для описания функций в PHPDoc:
С таким описанием уже понятно, что в my_filter нужно передать анонимную функцию, которая на вход примет int и вернёт bool. И, конечно же, Psalm будет проверять, что у вас в коде сюда передаётся именно такая функция.
Enums
Допустим, у вас есть функция, принимающая строковый параметр, и туда можно передавать только определённые строки:
Psalm позволяет описать параметр этой функции вот так:
Подробнее об enum-ах здесь.
Type aliases
Выше при описании array shapes я упоминал, что, хотя анализаторы и позволяют описывать структуру массивов, пользоваться этим не очень удобно, так как описание массива приходится копировать в разных местах. Правильным решением, конечно же, является использование классов вместо массивов. Но в случае с многолетним legacy это не всегда возможно.
На самом деле, проблема возникает не только с массивами, а с любым типом, который не является классом:
Generics aka templates
Рассмотрим эту возможность на примере. Допустим, у вас есть такая функция:
Как описать тип этой функции? Какой тип она принимает на вход? Что она возвращает?
Для статического анализатора встретить mixed — это катастрофа. Это значит, что нет абсолютно никакой информации о типе и нельзя делать никакие предположения. Но на самом деле, хотя функция identity() и принимает/возвращает любые типы, у неё есть логика: она возвращает тот же тип, который она приняла. А для статического анализатора это уже что-то. Это значит, что в коде:
Но как нам передать эту информацию анализатору? В Psalm для этого есть специальные PHPDoc-теги:
То есть templates позволяют передать Psalm информацию о типе, если класс/метод может работать с любым типом.
Внутри Psalm есть хорошие примеры работы с templates:
Подобный функционал есть и в Phan, но он работает только с классами: https://github.com/phan/phan/wiki/Generic-Types.
В целом, нам Psalm очень понравился. Похоже, что автор пытается «сбоку» прикрутить более умную систему типов и более умные и практически полезные подсказки для анализатора. Нам также понравилось, что Psalm сразу показывает строки кода, в которых найдены ошибки, и мы даже реализовали такое для Phan и PHPStan. Но об этом чуть ниже.
Инспекции кода в PHPStorm
У анализаторов есть общий небольшой недостаток: информацию об ошибке вы получаете не во время написания кода, а намного позже. Обычно вы пишете код, потом открываете консоль и запускаете анализаторы, после чего получаете отчёт.
Для программиста было бы удобнее получать информацию об ошибках в процессе редактирования кода. В этом направлении двигается Phan, который развивает свой language server. Но нам в PHPStorm, увы, неудобно его использовать.
Но, к счастью, у PHPStorm есть свой отличный анализатор (инспекции кода), по качеству соизмеримый с описанными выше решениями. А в дополнение к нему есть крутой плагин — Php Inspections (EA Extended). Главное отличие от анализаторов — удобство для программиста, заключающееся в том, что ошибки видны в редакторе во время написания кода. Кроме того, эти инспекции можно очень тонко настроить. Например, можно в проекте выделить разные scopes файлов и настроить инспекции по-разному для разных scopes.
Ещё отмечу такой полезный плагин, как deep-assoc-completion. Он хорошо понимает структуру массивов и упрощает автокомплит ключей.
Использование анализаторов в Badoo
Как это работает у нас?
Сегодня статический анализ кода используется несколькими командами, но в наших планах внедрить эту практику во всех.
Мы анализируем только изменённые файлы, вплоть до строк. То есть, когда девелопер завершает свою задачу, мы берём его git diff и запускаем анализаторы только для изменённых/добавленных файлов, а из полученного списка ошибок убираем те, которые относятся к старым (неизменённым) строкам. Таким образом мы прячем от девелопера ошибки, которые были сделаны ранее.
Получив отчёты от трёх анализаторов, мы объединяем их в один, где ошибки группируются по файлам и строкам:
Место анализаторов в нашем QA
Мы всячески стараемся снизить количество багов на продакшене:
Статические анализаторы — это, по сути, ещё один инструмент в этом списке, и он хорошо его дополняет. У статических анализаторов есть ряд преимуществ:
Отчёты анализаторов: мнение программистов
Нельзя сказать, что все программисты в восторге от статических анализаторов. Причин тут несколько.
Во-первых, многие испытывают недоверие к анализаторам, считая, что последние способны найти только какие-то примитивные ошибки, которых мы не допускаем.
Во-вторых, как уже было сказано выше, многое в отчётах анализаторов — просто неточности, например, неправильно указанные типы в PHPDoc. Некоторые программисты пренебрежительно относятся к таким ошибкам — код ведь работает.
В-третьих, у некоторых программистов завышенные ожидания. Они думали, что анализаторы будут находить какие-то хитрые баги, а вместо этого они вынуждают их добавлять проверки типов и исправлять PHPDoc. 🙂
Однако польза, принесённая анализаторами, перекрывает все эти незначительные недовольства. И как ни крути, это хорошая инвестиция в будущий код.